Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
<!-- Permissions -->
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />

<application
android:allowBackup="true"
Expand All @@ -27,6 +30,16 @@
</intent-filter>
</activity>

<receiver
android:name=".BootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>

</application>

</manifest>
249 changes: 118 additions & 131 deletions app/src/main/java/com/example/boostx/ActivityMain.kt

Large diffs are not rendered by default.

239 changes: 239 additions & 0 deletions app/src/main/java/com/example/boostx/AudioController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
package com.example.boostx

import android.content.Context
import android.media.AudioDeviceInfo
import android.media.AudioFormat
import android.media.AudioManager
import android.media.audiofx.DynamicsProcessing
import android.media.audiofx.LoudnessEnhancer
import android.media.audiofx.Visualizer
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.view.KeyEvent

class AudioController(private val context: Context) {
private val audioManager: AudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
private var loudnessEnhancer: LoudnessEnhancer? = null
private var dynamicsProcessing: DynamicsProcessing? = null
private var visualizer: Visualizer? = null

var audioSessionID: Int = 0
private set

private var lastDeviceId: Int? = null
private var hasRestarted = false
private val handler = Handler(Looper.getMainLooper())

private var currentBoostLevel: Int = 0

init {
setupEffects()
audioSessionID = audioManager.generateAudioSessionId()
}

private fun setupEffects() {
try {
loudnessEnhancer?.release()
// Priority 0, Session 0 (Global)
loudnessEnhancer = LoudnessEnhancer(0)
} catch (e: Exception) {
e.printStackTrace()
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
try {
dynamicsProcessing?.release()
// DynamicsProcessing is often more reliable on Android 9+ / MediaTek devices
val config = DynamicsProcessing.Config.Builder(
DynamicsProcessing.VARIANT_FAVOR_FREQUENCY_RESOLUTION,
2,
false, 0,
false, 0,
false, 0,
true
).build()
dynamicsProcessing = DynamicsProcessing(0, 0, config)
} catch (e: Exception) {
e.printStackTrace()
}
}

try {
visualizer?.release()
// Visualizer(0) trick to keep global session active on some devices
visualizer = Visualizer(0)
// Adding a listener (even empty) can help keep the session active on some devices
visualizer?.setDataCaptureListener(object : Visualizer.OnDataCaptureListener {
override fun onWaveFormDataCapture(v: Visualizer?, w: ByteArray?, s: Int) {}
override fun onFftDataCapture(v: Visualizer?, f: ByteArray?, s: Int) {}
}, Visualizer.getMaxCaptureRate() / 2, true, false)
visualizer?.enabled = false
} catch (e: Exception) {
e.printStackTrace()
}
}

fun getMaxVolume(): Int = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)

fun setInitialVolume() {
val maxVolume = getMaxVolume()
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, maxVolume, 0)
}

fun applyBoost(level: Int) {
currentBoostLevel = level
try {
val gainMB = level * 30

// Ensure effects are initialized
if (loudnessEnhancer == null && (Build.VERSION.SDK_INT < Build.VERSION_CODES.P || dynamicsProcessing == null)) {
setupEffects()
}

// Apply LoudnessEnhancer boost
loudnessEnhancer?.apply {
enabled = false
setTargetGain(gainMB)
enabled = level > 0
}

// Apply DynamicsProcessing boost (more robust on newer devices)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
try {
dynamicsProcessing?.apply {
val gainDB = level / 5f // 0 to 20 dB boost
val limiter = getLimiterByChannelIndex(0)
limiter.isEnabled = level > 0
limiter.postGain = gainDB
limiter.attackTime = 1f
limiter.releaseTime = 60f
limiter.ratio = 10f
limiter.threshold = -1f

setLimiterAllChannelsTo(limiter)
enabled = level > 0
}
} catch (e: Exception) {
e.printStackTrace()
}
}

// Aggressive toggle for visualizer to "wake up" the session
// Especially needed for Ikko/MediaTek/Android 14+ devices
try {
visualizer?.enabled = false
if (level > 0) {
visualizer?.enabled = true
}
} catch (e: Exception) {
e.printStackTrace()
}

if (level > 0 && !hasRestarted) {
restartAudioPlayback()
hasRestarted = true
}
} catch (e: Exception) {
e.printStackTrace()
}
}

fun applyVolume(level: Int) {
val maxVolume = getMaxVolume()
audioManager.setStreamVolume(
AudioManager.STREAM_MUSIC,
((level.toFloat() / 100) * maxVolume).toInt(),
0
)
}

fun restartAudioPlayback() {
val pauseIntent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PAUSE)
audioManager.dispatchMediaKeyEvent(pauseIntent)
audioManager.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PAUSE))

handler.postDelayed({
val playIntent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY)
audioManager.dispatchMediaKeyEvent(playIntent)
audioManager.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY))
}, 100)
}

fun getOutputDeviceInfo(): String? {
val devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)

val activeDevice = devices.firstOrNull { isActiveOutputDevice(it) }
?: devices.firstOrNull { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER }

if (activeDevice == null) {
if (lastDeviceId != null) {
lastDeviceId = null
return context.getString(R.string.no_device_detected)
}
return null
}

if (activeDevice.id == lastDeviceId) return null
lastDeviceId = activeDevice.id

// Re-initialize effects on device change to ensure they attach to the new output path
setupEffects()
hasRestarted = false // Allow one-time restart for the new device
applyBoost(currentBoostLevel)

val sampleRates = activeDevice.sampleRates.joinToString()
val deviceType = activeDevice.type
return "${context.getString(R.string.device_name_label)}\t\t\t\t${activeDevice.productName ?: "N/A"}\n" +
"${context.getString(R.string.device_type_label)}\t\t\t\t${getDeviceType(deviceType)} (${deviceType})\n" +
"${context.getString(R.string.device_id_label)}\t\t\t\t\t\t${activeDevice.id}\n\n" +
"${context.getString(R.string.channels_label)}\t\t\t\t\t\t\t\t${activeDevice.channelCounts.joinToString().ifEmpty { "N/A" }}\n" +
"${context.getString(R.string.encodings_label)}\t\t\t\t\t\t${getEncodingFormat(activeDevice.encodings).ifEmpty { "N/A" }}\n\n" +
"${context.getString(R.string.sample_rates_label)} ${if (sampleRates.isEmpty()) "\tN/A" else "\n" + sampleRates + "Hz"}\n"
}

private fun isActiveOutputDevice(device: AudioDeviceInfo): Boolean {
return when (device.type) {
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> audioManager.isBluetoothA2dpOn
AudioDeviceInfo.TYPE_WIRED_HEADPHONES, AudioDeviceInfo.TYPE_WIRED_HEADSET -> true
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> audioManager.isSpeakerphoneOn
AudioDeviceInfo.TYPE_USB_DEVICE, AudioDeviceInfo.TYPE_USB_HEADSET -> true
else -> false
}
}

private fun getDeviceType(type: Int): String {
return when (type) {
AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> context.getString(R.string.device_bluetooth)
AudioDeviceInfo.TYPE_WIRED_HEADPHONES, AudioDeviceInfo.TYPE_WIRED_HEADSET -> context.getString(R.string.device_wired_headphones)
AudioDeviceInfo.TYPE_USB_DEVICE, AudioDeviceInfo.TYPE_USB_HEADSET -> context.getString(R.string.device_usb_audio)
AudioDeviceInfo.TYPE_HDMI, AudioDeviceInfo.TYPE_HDMI_ARC -> context.getString(R.string.device_hdmi)
AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> context.getString(R.string.device_speaker)
AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> context.getString(R.string.device_earpiece)
else -> context.getString(R.string.device_unknown)
}
}

private fun getEncodingFormat(formats: IntArray): String {
return formats.joinToString { encodingMap[it] ?: context.getString(R.string.format_unknown) }
}

private val encodingMap = mapOf(
AudioFormat.ENCODING_PCM_16BIT to "PCM 16-bit",
AudioFormat.ENCODING_PCM_8BIT to "PCM 8-bit",
AudioFormat.ENCODING_PCM_FLOAT to "PCM Float",
AudioFormat.ENCODING_AC3 to "Dolby AC3",
AudioFormat.ENCODING_E_AC3 to "Dolby Digital+",
AudioFormat.ENCODING_DTS to "DTS",
AudioFormat.ENCODING_DTS_HD to "DTS-HD",
AudioFormat.ENCODING_AAC_ELD to "AAC ELD",
AudioFormat.ENCODING_AAC_HE_V1 to "AAC HE v1",
AudioFormat.ENCODING_AAC_HE_V2 to "AAC HE v2"
)

fun release() {
loudnessEnhancer?.release()
dynamicsProcessing?.release()
visualizer?.release()
}
}
22 changes: 22 additions & 0 deletions app/src/main/java/com/example/boostx/BootReceiver.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.example.boostx

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent

class BootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED ||
intent.action == "android.intent.action.QUICKBOOT_POWERON") {

val prefs = context.getSharedPreferences("BoostXPrefs", Context.MODE_PRIVATE)
val bootStart = prefs.getBoolean("boot_start", false)

if (bootStart) {
val i = Intent(context, MainActivity::class.java)
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(i)
}
}
}
}
46 changes: 39 additions & 7 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:fontFamily="monospace"
android:text="BoostX"
android:text="@string/app_name"
android:textColor="@color/white"
android:textSize="26sp"
android:textStyle="bold" />
Expand Down Expand Up @@ -64,7 +64,7 @@
android:fontFamily="monospace"
android:paddingStart="14dp"
android:paddingEnd="14dp"
android:text="Output Device: Unknown"
android:text="@string/output_device_unknown"
android:textColor="@color/gray"
android:textSize="16sp" />

Expand All @@ -90,7 +90,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:text="Volume: "
android:text="@string/volume_label"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="normal" />
Expand All @@ -109,7 +109,7 @@
android:id="@+id/volumeSlider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="Volume level slider"
android:contentDescription="@string/volume_slider_desc"
android:stepSize="1"
android:value="50"
android:valueFrom="0"
Expand Down Expand Up @@ -148,7 +148,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:text="Boost: "
android:text="@string/boost_label"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="normal" />
Expand All @@ -171,7 +171,7 @@
android:fontFamily="monospace"
android:hapticFeedbackEnabled="true"
android:minHeight="48dp"
android:text="Gradual"
android:text="@string/gradual_label"
android:textColor="@color/gray"
android:textIsSelectable="false"
android:textSize="16sp"
Expand All @@ -184,7 +184,7 @@
android:id="@+id/boostSlider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="Boost level slider"
android:contentDescription="@string/boost_slider_desc"
android:stepSize="10"
android:value="0"
android:valueFrom="0"
Expand All @@ -206,6 +206,38 @@
app:trackStopIndicatorSize="0dp" />
</LinearLayout>

<!-- Auto Start Switch -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="14dp"
android:paddingEnd="14dp">

<TextView
android:id="@+id/bootStartText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:fontFamily="monospace"
android:text="@string/start_on_boot_label"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="normal" />

<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/bootStartSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:hapticFeedbackEnabled="true"
android:minHeight="48dp"
android:thumbTint="@color/gray"
android:trackTint="@color/gray" />
</LinearLayout>

</LinearLayout>

</ScrollView>
Expand Down
Loading