diff --git a/common/src/main/java/com/shilapi/xcertplay/AirPlayPersistence.kt b/common/src/main/java/com/shilapi/xcertplay/AirPlayPersistence.kt index 3cdf082..0371d16 100644 --- a/common/src/main/java/com/shilapi/xcertplay/AirPlayPersistence.kt +++ b/common/src/main/java/com/shilapi/xcertplay/AirPlayPersistence.kt @@ -39,6 +39,7 @@ object AirPlayPersistence { private const val KEY_HEVC_ENABLED = "hevc_enabled" private const val KEY_HEVC_SOFTWARE_DECODER = "hevc_software_decoder" private const val KEY_ADVANCED_AUDIO_CHANNEL_MAPPING = "advanced_audio_channel_mapping" + private const val KEY_NAVIGATION_STREAM_TYPE = "navigation_stream_type" private const val KEY_WIRELESS_ENABLED = "wireless_enabled" private const val KEY_WIRELESS_HOTSPOT_MODE = "wireless_hotspot_mode" private const val KEY_MANUAL_HOTSPOT_SSID = "manual_hotspot_ssid" @@ -131,6 +132,16 @@ object AirPlayPersistence { .apply() } + fun loadNavigationStreamType(context: Context): Int = + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + .getInt(KEY_NAVIGATION_STREAM_TYPE, 14) + + fun saveNavigationStreamType(context: Context, streamType: Int) { + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit() + .putInt(KEY_NAVIGATION_STREAM_TYPE, streamType) + .apply() + } + fun loadWirelessEnabled(context: Context): Boolean = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) .getBoolean(KEY_WIRELESS_ENABLED, true) diff --git a/common/src/main/java/com/shilapi/xcertplay/CarPlayHostActivity.kt b/common/src/main/java/com/shilapi/xcertplay/CarPlayHostActivity.kt index 8b38f43..1ee20a3 100644 --- a/common/src/main/java/com/shilapi/xcertplay/CarPlayHostActivity.kt +++ b/common/src/main/java/com/shilapi/xcertplay/CarPlayHostActivity.kt @@ -275,6 +275,7 @@ class CarPlayHostActivity : ComponentActivity() { private var hevcSoftwareDecoderEnabled = false private var advancedAudioChannelMappingSupported = false private var advancedAudioChannelMapping = false + private var navigationStreamType = 14 private var debugLogsEnabled = false private var autoStartOnBoot = false private var manufacturer = AirPlayPersistence.DEFAULT_MANUFACTURER @@ -437,6 +438,7 @@ class CarPlayHostActivity : ComponentActivity() { advancedAudioChannelMapping = advancedAudioChannelMappingSupported && AirPlayPersistence.loadAdvancedAudioChannelMapping(this) + navigationStreamType = AirPlayPersistence.loadNavigationStreamType(this) debugLogsEnabled = AirPlayPersistence.loadDebugLogsEnabled(this) autoStartOnBoot = AirPlayPersistence.loadAutoStartOnBoot(this) manufacturer = AirPlayPersistence.loadManufacturer(this) @@ -561,6 +563,9 @@ class CarPlayHostActivity : ComponentActivity() { requestLocationPermission() } wirelessPermissionsReady = !wirelessEnabled || hasRequiredWirelessPermissions() + advancedAudioChannelMapping = + advancedAudioChannelMappingSupported && + AirPlayPersistence.loadAdvancedAudioChannelMapping(this) if (DiLink51ClusterLayout.automatic(this) && clusterMonitor == null) { clusterMonitor = DiLink51ClusterMonitor(this, ::onClusterActivityState).also { it.start() } } else if (!DiLink51ClusterLayout.automatic(this)) { @@ -960,7 +965,7 @@ class CarPlayHostActivity : ComponentActivity() { settingsSwitchRow( label = "Advanced audio channel mapping", checked = advancedAudioChannelMapping, - description = "Route AAOS audio buses by CarPlay audio type", + description = "Use usage / content-type routing instead of stream type", ) { checked -> advancedAudioChannelMapping = checked appendLog( @@ -2913,6 +2918,7 @@ class CarPlayHostActivity : ComponentActivity() { videoHeight = videoHeight, preferSoftwareHevcDecoder = hevcSoftwareDecoderEnabled, advancedAudioChannelMapping = advancedAudioChannelMapping, + navigationStreamType = navigationStreamType, onScreenStreamActiveChanged = { type, active -> onScreenStreamStateChanged(controllerGeneration, type, active) }, diff --git a/common/src/main/java/com/shilapi/xcertplay/DiPlayActivity.kt b/common/src/main/java/com/shilapi/xcertplay/DiPlayActivity.kt index 45923c3..95d53dc 100644 --- a/common/src/main/java/com/shilapi/xcertplay/DiPlayActivity.kt +++ b/common/src/main/java/com/shilapi/xcertplay/DiPlayActivity.kt @@ -12,12 +12,15 @@ import android.content.res.Configuration import android.graphics.Color import android.graphics.Typeface import android.graphics.drawable.GradientDrawable +import android.media.AudioFormat +import android.media.AudioTrack import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Handler import android.os.Looper import android.provider.Settings +import android.util.Log import android.view.Gravity import android.view.View import android.view.ViewGroup @@ -50,6 +53,9 @@ class DiPlayActivity : ComponentActivity() { private var initialLaunch = true private var notificationTransport = true private var exportInProgress = false + private var navigationStreamType = 14 + private var testToneTrack: AudioTrack? = null + private var toneStop: Runnable? = null private var exportButton: Button? = null private val notificationPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { connect(notificationTransport) @@ -243,6 +249,42 @@ class DiPlayActivity : ComponentActivity() { AirPlayPersistence.saveHideTopBar(this, it); AirPlayPersistence.saveHideBottomBar(this, it) } } + section(content, "Audio routing") { card -> + val channelTitle = label("Navigation stream type", 18, TEXT, true) + val channelHint = label("Tap a number to test. 14 = driver speaker on BYD.", 14, MUTED) + val grid = channelSelector() + val saveButton = button("Save", true) { + AirPlayPersistence.saveNavigationStreamType(this, navigationStreamType) + toast("Saved: $navigationStreamType") + } + fun applyChannelEnabled(enabled: Boolean) { + val alpha = if (enabled) 1f else 0.4f + listOf(channelTitle, channelHint, saveButton).forEach { + it.isEnabled = enabled + it.alpha = alpha + } + for (i in 0 until grid.childCount) { + grid.getChildAt(i).let { child -> + child.isEnabled = enabled + child.alpha = alpha + } + } + } + val aaosSupported = resources.getBoolean(R.bool.config_advanced_audio_channel_mapping) + if (aaosSupported) { + toggle(card, "Advanced audio channel mapping", + "Use usage / content-type routing instead of stream type", + AirPlayPersistence.loadAdvancedAudioChannelMapping(this)) { + AirPlayPersistence.saveAdvancedAudioChannelMapping(this, it) + applyChannelEnabled(!it) + } + } + card.addView(channelTitle) + card.addView(channelHint) + card.addView(grid) + card.addView(saveButton, matchButton(12, 56)) + if (aaosSupported) applyChannelEnabled(!AirPlayPersistence.loadAdvancedAudioChannelMapping(this)) + } if (com.shilapi.xcertplay.hud.BydOutputSettings.available(this)) section(content, "BYD navigation", R.drawable.ic_dp_navigation) { card -> toggle(card, "Navigation on HUD and instrument cluster", "Show phone navigation arrows, distance and street names on supported BYD displays. Vehicle compatibility varies.", @@ -799,6 +841,90 @@ class DiPlayActivity : ComponentActivity() { } private fun openSystem(intent: Intent) { runCatching { startActivity(intent) }.onFailure { toast("Open this setting from your car’s Settings app.") } } private fun toast(message: String) { Toast.makeText(this, message, Toast.LENGTH_LONG).show() } + + private fun playTestTone(streamType: Int) { + toneStop?.let { handler.removeCallbacks(it) } + toneStop = null + testToneTrack?.let { runCatching { it.stop(); it.release() } } + testToneTrack = null + val pcm = assets.open("navigation_test.pcm").use { it.readBytes() } + val track = AudioTrack( + streamType, + 44100, + AudioFormat.CHANNEL_OUT_MONO, + AudioFormat.ENCODING_PCM_16BIT, + pcm.size, + AudioTrack.MODE_STREAM + ) + if (track.state != AudioTrack.STATE_INITIALIZED) { + Log.w("DiPlay", "playTestTone streamType=$streamType state=${track.state}") + track.release() + toast("Stream type $streamType is unavailable here (state=${track.state})") + return + } + track.write(pcm, 0, pcm.size) + track.play() + Log.i("DiPlay", "playTestTone streamType=$streamType state=${track.state} playState=${track.playState}") + testToneTrack = track + val stop = Runnable { + track.stop() + track.release() + if (testToneTrack === track) testToneTrack = null + toneStop = null + } + toneStop = stop + handler.postDelayed(stop, 4500) + } + + private val channelButtons = mutableListOf