diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 33b4b29..fdd8501 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,6 +5,9 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/example/boostx/ActivityMain.kt b/app/src/main/java/com/example/boostx/ActivityMain.kt
index 9ad1ecc..950ca18 100644
--- a/app/src/main/java/com/example/boostx/ActivityMain.kt
+++ b/app/src/main/java/com/example/boostx/ActivityMain.kt
@@ -1,16 +1,21 @@
package com.example.boostx
+import android.Manifest
+import android.content.Context
+import android.content.pm.PackageManager
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.Typeface
-import android.media.AudioDeviceInfo
-import android.media.AudioFormat
-import android.media.AudioManager
-import android.media.audiofx.LoudnessEnhancer
+import android.content.Intent
+import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
+import android.os.PowerManager
+import android.provider.Settings
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.Spanned
@@ -19,7 +24,6 @@ import android.text.style.ForegroundColorSpan
import android.text.style.RelativeSizeSpan
import android.text.style.StyleSpan
import android.text.style.URLSpan
-import android.view.KeyEvent
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
@@ -35,33 +39,36 @@ class MainActivity : AppCompatActivity() {
private lateinit var boostSlider: Slider
private lateinit var volumeSlider: Slider
private lateinit var gradualBoostSwitch: MaterialSwitch
+ private lateinit var bootStartSwitch: MaterialSwitch
+ private lateinit var bootStartText: TextView
private lateinit var boostTextView: TextView
private lateinit var volumeTextView: TextView
private lateinit var outputDeviceTextView: TextView
- private lateinit var audioManager: AudioManager
- private var audioSessionID = 0
- private var loudnessEnhancer: LoudnessEnhancer? = null
- private var lastDeviceId: Int? = null
- private var isBoostEnabled = true
+ private lateinit var audioController: AudioController
private val handler = Handler(Looper.getMainLooper())
private val updateRunnable = object : Runnable {
override fun run() {
- updateOutputDeviceInfo()
+ updateOutputDeviceInfoUI()
handler.postDelayed(this, 1800)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- setTheme(R.style.Theme_BoostX) // Apply dark theme
+ setTheme(R.style.Theme_BoostX)
setContentView(R.layout.activity_main)
+ checkPermissions()
+
+ audioController = AudioController(this)
boostSlider = findViewById(R.id.boostSlider)
volumeSlider = findViewById(R.id.volumeSlider)
gradualBoostSwitch = findViewById(R.id.gradualBoostSwitch)
+ bootStartSwitch = findViewById(R.id.bootStartSwitch)
+ bootStartText = findViewById(R.id.bootStartText)
val originalBoostSliderProperties = mutableMapOf(
"valueFrom" to boostSlider.valueFrom,
@@ -79,32 +86,80 @@ class MainActivity : AppCompatActivity() {
handleGradualBoostSwitch(isChecked, originalBoostSliderProperties)
}
+ bootStartSwitch.setOnCheckedChangeListener { _, isChecked ->
+ handleBootStartSwitch(isChecked)
+ }
+
+ val prefs = getSharedPreferences("BoostXPrefs", Context.MODE_PRIVATE)
+ val savedBoost = prefs.getFloat("boost_value", 0f)
+ val savedVolume = prefs.getFloat("volume_value", 100f)
+ val savedGradual = prefs.getBoolean("gradual_boost", false)
+ val savedBootStart = prefs.getBoolean("boot_start", false)
- loudnessEnhancer = LoudnessEnhancer(0)
- audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
- audioSessionID = audioManager.generateAudioSessionId()
+ gradualBoostSwitch.isChecked = savedGradual
+ handleGradualBoostSwitch(savedGradual, originalBoostSliderProperties)
- val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
- audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, maxVolume, 0)
- volumeSlider.value = 100f
+ bootStartSwitch.isChecked = savedBootStart
+ handleBootStartSwitch(savedBootStart)
+
+ volumeSlider.value = savedVolume
boostTextView = findViewById(R.id.boostLevel)
volumeTextView = findViewById(R.id.volumeLevel)
outputDeviceTextView = findViewById(R.id.outputDeviceText)
+ val currentStepSize = boostSlider.stepSize
+ val validatedBoost = if (currentStepSize > 0) {
+ (savedBoost / currentStepSize).roundToInt() * currentStepSize
+ } else {
+ savedBoost
+ }.coerceIn(boostSlider.valueFrom, boostSlider.valueTo)
+
+ boostSlider.value = validatedBoost
+
+ applyBoost(validatedBoost.toInt())
+ applyVolume(savedVolume.toInt())
+
boostSlider.addOnChangeListener { _, value, _ -> applyBoost(value.toInt()) }
- volumeSlider.addOnChangeListener { _, value, _ -> applyVolume(value.toInt(), maxVolume) }
+ volumeSlider.addOnChangeListener { _, value, _ -> applyVolume(value.toInt()) }
findViewById(R.id.infoIcon).setOnClickListener {
showAppInfo()
}
+ checkBatteryOptimizations()
+ }
+
+ private fun checkPermissions() {
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
+ ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO), 1001)
+ }
+ }
+
+ private fun checkBatteryOptimizations() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ val intent = Intent()
+ val packageName = packageName
+ val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
+ if (!pm.isIgnoringBatteryOptimizations(packageName)) {
+ MaterialAlertDialogBuilder(this, R.style.CustomDialogTheme)
+ .setTitle("Battery Optimization")
+ .setMessage("To ensure the app runs smoothly in the background, please disable battery optimization for BoostX.")
+ .setPositiveButton("Settings") { _, _ ->
+ intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
+ intent.data = Uri.parse("package:$packageName")
+ startActivity(intent)
+ }
+ .setNegativeButton("Cancel", null)
+ .show()
+ }
+ }
}
private fun showAppInfo(){
val infoDialog = MaterialAlertDialogBuilder(this, R.style.CustomDialogTheme)
- val title = SpannableString("App Info")
+ val title = SpannableString(getString(R.string.app_info_title))
title.setSpan(StyleSpan(Typeface.BOLD), 0, title.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
infoDialog.setTitle(title)
@@ -113,10 +168,10 @@ class MainActivity : AppCompatActivity() {
val infoBuilder = SpannableStringBuilder()
- infoBuilder.append("Version:\t\t\t\t$versionName\n")
- infoBuilder.append("API Level:\t${Build.VERSION.SDK_INT}\n")
+ infoBuilder.append("${getString(R.string.version_label)}\t\t\t\t$versionName\n")
+ infoBuilder.append("${getString(R.string.api_level_label)}\t${Build.VERSION.SDK_INT}\n")
- val devLabel = "Developer:\t"
+ val devLabel = "${getString(R.string.developer_label)}\t"
val devLink = SpannableString("Om Gupta")
devLink.setSpan(URLSpan("https://github.com/AumGupta"), 0, devLink.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
@@ -124,7 +179,7 @@ class MainActivity : AppCompatActivity() {
infoBuilder.append(devLink)
infoBuilder.append("\n")
- val sourceLabel = "Source:\t\t\t\t\t"
+ val sourceLabel = "${getString(R.string.source_label)}\t\t\t\t\t"
val sourceLink = SpannableString("GitHub")
sourceLink.setSpan(URLSpan("https://github.com/AumGupta/BoostX"), 0, sourceLink.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
@@ -133,49 +188,46 @@ class MainActivity : AppCompatActivity() {
infoBuilder.append("\n\n")
val noteText = SpannableString(
- "Session ID:\t\t$audioSessionID\n" +
- "Package:\t\t\t\t\t\t$packageName\n\n" +
- "Warning: Excessive boost may distort audio or harm speakers."
+ "${getString(R.string.session_id_label)}\t\t${audioController.audioSessionID}\n" +
+ "${getString(R.string.package_label)}\t\t\t\t\t\t$packageName\n\n" +
+ getString(R.string.warning_text)
)
noteText.setSpan(ForegroundColorSpan(Color.GRAY), 0, noteText.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
noteText.setSpan(RelativeSizeSpan(0.85f), 0, noteText.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
infoBuilder.append(noteText)
- // Creating TextView for Dialog
val textView = TextView(this)
textView.text = infoBuilder
textView.setBackgroundColor("#202020".toColorInt())
- textView.setTextColor(Color.WHITE) // Ensuring text is visible on dark background
+ textView.setTextColor(Color.WHITE)
textView.textSize = 16f
textView.movementMethod = LinkMovementMethod.getInstance()
textView.setPadding(48, 48, 48, 48)
val spacer = View(this)
val spacerParams = LinearLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT, // Match width
- 50 // Height of spacer in pixels (adjust as needed)
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ 50
)
spacer.layoutParams = spacerParams
val linearLayout = LinearLayout(this)
linearLayout.orientation = LinearLayout.VERTICAL
- linearLayout.setBackgroundColor("#CCFF00".toColorInt()) // Neon background
- linearLayout.addView(spacer) // Add Spacer on Top
- linearLayout.addView(textView) // Add TextView Below
+ linearLayout.setBackgroundColor("#CCFF00".toColorInt())
+ linearLayout.addView(spacer)
+ linearLayout.addView(textView)
val dialog = infoDialog.setView(linearLayout)
.create()
dialog.show()
-
}
private fun handleGradualBoostSwitch(isChecked:Boolean, originalBoostSliderProperties:MutableMap){
if (isChecked) {
gradualBoostSwitch.thumbTintList = ColorStateList.valueOf("#CCFF00".toColorInt())
gradualBoostSwitch.trackTintList = ColorStateList.valueOf("#666600".toColorInt())
-
gradualBoostSwitch.setTextColor(Color.WHITE)
boostSlider.valueFrom = volumeSlider.valueFrom
@@ -192,7 +244,6 @@ class MainActivity : AppCompatActivity() {
} else {
gradualBoostSwitch.thumbTintList = ColorStateList.valueOf(Color.GRAY)
gradualBoostSwitch.trackTintList = ColorStateList.valueOf(Color.DKGRAY)
-
gradualBoostSwitch.setTextColor(Color.GRAY)
boostSlider.value = (boostSlider.value / 10).roundToInt() * 10f
@@ -209,124 +260,60 @@ class MainActivity : AppCompatActivity() {
boostSlider.isTickVisible = originalBoostSliderProperties["isTickVisible"] as Boolean
}
}
- private fun restartAudioPlayback() {
- isBoostEnabled = false
- audioManager.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PAUSE))
- audioManager.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PAUSE))
- audioManager.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY))
- audioManager.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY))
- audioManager.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PAUSE))
- audioManager.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PAUSE))
- audioManager.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY))
- audioManager.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY))
- }
+ private fun handleBootStartSwitch(isChecked: Boolean) {
+ if (isChecked) {
+ bootStartSwitch.thumbTintList = ColorStateList.valueOf("#CCFF00".toColorInt())
+ bootStartSwitch.trackTintList = ColorStateList.valueOf("#666600".toColorInt())
+ bootStartText.setTextColor(Color.WHITE)
+ } else {
+ bootStartSwitch.thumbTintList = ColorStateList.valueOf(Color.GRAY)
+ bootStartSwitch.trackTintList = ColorStateList.valueOf(Color.DKGRAY)
+ bootStartText.setTextColor(Color.GRAY)
+ }
+ }
private fun applyBoost(level: Int) {
boostTextView.text = "$level%"
boostTextView.setTextColor(if (level > 50) "#F92672".toColorInt() else Color.GRAY)
-
- loudnessEnhancer?.setTargetGain(level * 25)
- loudnessEnhancer?.enabled = true
-
- if (isBoostEnabled) restartAudioPlayback()
+ audioController.applyBoost(level)
}
- private fun applyVolume(level: Int, maxVolume: Int) {
+ private fun applyVolume(level: Int) {
volumeTextView.text = "$level%"
- audioManager.setStreamVolume(AudioManager.STREAM_MUSIC,
- ((level.toFloat() / 100) * maxVolume).toInt(),
- 0)
+ audioController.applyVolume(level)
}
- private fun updateOutputDeviceInfo() {
- val devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
-
- // Prioritize Bluetooth and Wired devices first, then fallback to Speaker
- val activeDevice = devices.firstOrNull { isActiveOutputDevice(it) }
- ?: devices.firstOrNull { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER }
-
- // If no active device found
- if (activeDevice == null) {
- if (lastDeviceId != null) { // Only update UI if previously there was a device
- lastDeviceId = null
- runOnUiThread {
- outputDeviceTextView.text = "No Active Output Device Detected"
- }
- }
- return
- }
-
- // Avoid unnecessary UI updates if the same device is still active
- if (activeDevice.id == lastDeviceId) return
- lastDeviceId = activeDevice.id
-
- val sampleRates = activeDevice.sampleRates.joinToString()
- val deviceType = activeDevice.type
- val info = "Device Name:\t\t\t\t${activeDevice.productName ?: "N/A"}\n" +
- "Device Type:\t\t\t\t${getDeviceType(deviceType)} (${deviceType})\n" +
- "Device ID:\t\t\t\t\t\t${activeDevice.id}\n\n"+
- "Channels:\t\t\t\t\t\t\t\t${activeDevice.channelCounts.joinToString().ifEmpty { "N/A" }}\n" +
- "Encodings:\t\t\t\t\t\t${getEncodingFormat(activeDevice.encodings).ifEmpty { "N/A" }}\n\n"+
- "Sample Rates: ${if (sampleRates.isEmpty()) "\tN/A" else "\n"+sampleRates+"Hz"}\n"
-
- runOnUiThread {
+ private fun updateOutputDeviceInfoUI() {
+ val info = audioController.getOutputDeviceInfo()
+ if (info != null) {
outputDeviceTextView.text = info
}
}
-
- private fun isActiveOutputDevice(device: AudioDeviceInfo): Boolean {
- return when (device.type) {
- AudioDeviceInfo.TYPE_BLUETOOTH_A2DP -> audioManager.isBluetoothA2dpOn
- AudioDeviceInfo.TYPE_WIRED_HEADPHONES, AudioDeviceInfo.TYPE_WIRED_HEADSET -> true
- AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> audioManager.isSpeakerphoneOn
- else -> false
- }
- }
-
- private fun getDeviceType(type: Int): String {
- return when (type) {
- AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> "Bluetooth"
- AudioDeviceInfo.TYPE_WIRED_HEADPHONES, AudioDeviceInfo.TYPE_WIRED_HEADSET -> "Wired Headphones"
- AudioDeviceInfo.TYPE_USB_DEVICE, AudioDeviceInfo.TYPE_USB_HEADSET -> "USB Audio"
- AudioDeviceInfo.TYPE_HDMI, AudioDeviceInfo.TYPE_HDMI_ARC -> "HDMI Output"
- AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> "Device Speaker"
- AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> "Earpiece"
- else -> "Unknown Device"
- }
- }
-
- private fun getEncodingFormat(formats: IntArray): String {
- return formats.joinToString { encodingMap[it] ?: "Unknown Format" }
- }
-
- 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"
- )
-
override fun onResume() {
super.onResume()
- handler.post(updateRunnable) // Start updates when app is active
+ handler.post(updateRunnable)
}
override fun onPause() {
super.onPause()
handler.removeCallbacks(updateRunnable)
+
+ val prefs = getSharedPreferences("BoostXPrefs", Context.MODE_PRIVATE)
+ prefs.edit().apply {
+ putFloat("boost_value", boostSlider.value)
+ putFloat("volume_value", volumeSlider.value)
+ putBoolean("gradual_boost", gradualBoostSwitch.isChecked)
+ putBoolean("boot_start", bootStartSwitch.isChecked)
+ apply()
+ }
}
+
override fun onDestroy() {
super.onDestroy()
- loudnessEnhancer?.release()
+ audioController.release()
handler.removeCallbacks(updateRunnable)
handler.removeCallbacksAndMessages(null)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/example/boostx/AudioController.kt b/app/src/main/java/com/example/boostx/AudioController.kt
new file mode 100644
index 0000000..2f790da
--- /dev/null
+++ b/app/src/main/java/com/example/boostx/AudioController.kt
@@ -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()
+ }
+}
diff --git a/app/src/main/java/com/example/boostx/BootReceiver.kt b/app/src/main/java/com/example/boostx/BootReceiver.kt
new file mode 100644
index 0000000..a0b7cb2
--- /dev/null
+++ b/app/src/main/java/com/example/boostx/BootReceiver.kt
@@ -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)
+ }
+ }
+ }
+}
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 287a9da..8700018 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -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" />
@@ -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" />
@@ -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" />
@@ -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"
@@ -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" />
@@ -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"
@@ -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"
@@ -206,6 +206,38 @@
app:trackStopIndicatorSize="0dp" />
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
new file mode 100644
index 0000000..6691e1d
--- /dev/null
+++ b/app/src/main/res/values-nl/strings.xml
@@ -0,0 +1,36 @@
+
+ BoostX
+ Outputapparaat: Onbekend
+ Volume:
+ Schuifregelaar voor volumeniveau
+ Boost:
+ Geleidelijk
+ Schuifregelaar voor boostniveau
+ Automatisch opstarten
+
+ App Info
+ Versie:
+ API Niveau:
+ Ontwikkelaar:
+ Bron:
+ Sessie ID:
+ Pakket:
+ Waarschuwing: Overmatige boost kan audio vervormen of luidsprekers beschadigen.
+
+ Geen actief outputapparaat gedetecteerd
+ Apparaatnaam:
+ Apparaattype:
+ Apparaat ID:
+ Kanalen:
+ Coderingen:
+ Samplefrequenties:
+
+ Bluetooth
+ Bedrade hoofdtelefoon
+ USB Audio
+ HDMI Uitgang
+ Apparaat luidspreker
+ Oortje
+ Onbekend apparaat
+ Onbekend formaat
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 435f94b..abfa05f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,36 @@
BoostX
+ Output Device: Unknown
+ Volume:
+ Volume level slider
+ Boost:
+ Gradual
+ Boost level slider
+ Start on Boot
+
+ App Info
+ Version:
+ API Level:
+ Developer:
+ Source:
+ Session ID:
+ Package:
+ Warning: Excessive boost may distort audio or harm speakers.
+
+ No Active Output Device Detected
+ Device Name:
+ Device Type:
+ Device ID:
+ Channels:
+ Encodings:
+ Sample Rates:
+
+ Bluetooth
+ Wired Headphones
+ USB Audio
+ HDMI Output
+ Device Speaker
+ Earpiece
+ Unknown Device
+ Unknown Format
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 9c099fc..885d85c 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -18,3 +18,13 @@ android.useAndroidX=true
android.enableJetifier=true
# Kotlin code style for newly created projects: 'official' or 'obsolete'
kotlin.code.style=official
+android.defaults.buildfeatures.resvalues=true
+android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
+android.enableAppCompileTimeRClass=false
+android.usesSdkInManifest.disallowed=false
+android.uniquePackageNames=false
+android.dependency.useConstraints=true
+android.r8.strictFullModeForKeepRules=false
+android.r8.optimizedResourceShrinking=false
+android.builtInKotlin=false
+android.newDsl=false
diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties
new file mode 100644
index 0000000..baa28d1
--- /dev/null
+++ b/gradle/gradle-daemon-jvm.properties
@@ -0,0 +1,13 @@
+#This file is generated by updateDaemonJvm
+toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
+toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
+toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
+toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
+toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/7083b89563e7ce20943037b8cd2b8cc2/redirect
+toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/060bbb778a1f55ea705fdebd2ccfeab9/redirect
+toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/491f83666ae7f4d6ebb28fee72ebb035/redirect
+toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/0d1a1acdc708062093673f65aa9aba4b/redirect
+toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/d09679dc60fe5aa05ef7d03efdefac20/redirect
+toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/ed4e3bf2f5e7c5d9aabc4cbd8acd555e/redirect
+toolchainVendor=JETBRAINS
+toolchainVersion=21
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 6ebb681..7d17f3e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,6 +1,6 @@
[versions]
-agp = "8.7.3"
-kotlin = "2.0.0"
+agp = "9.2.1"
+kotlin = "2.2.10"
coreKtx = "1.15.0"
junit = "4.13.2"
junitVersion = "1.2.1"
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 2a84e18..c61a118 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 495d9c6..fe9a7f2 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -11,6 +11,9 @@ pluginManagement {
gradlePluginPortal()
}
}
+plugins {
+ id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
+}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {