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 {