| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- package com.wasp.cardash_launcher
- import android.app.Activity
- import android.content.Intent
- import android.content.IntentFilter
- import android.media.AudioManager
- import android.net.ConnectivityManager
- import android.net.NetworkCapabilities
- import android.os.BatteryManager
- import android.util.Log
- import android.view.KeyEvent
- import android.view.WindowManager
- import io.flutter.plugin.common.BinaryMessenger
- import io.flutter.plugin.common.MethodCall
- import io.flutter.plugin.common.MethodChannel
- /** Battery, Wi-Fi and media-volume bridge. */
- class SystemBridge(private val activity: Activity) : MethodChannel.MethodCallHandler {
- companion object {
- const val CHANNEL = "com.wasp.cardash_launcher/system"
- private const val VOLUME_TAG = "CarDashVolume"
- }
- private var channel: MethodChannel? = null
- private var lastVolumeLog: String? = null
- private val syu = SyuVolumeBridge(activity.applicationContext)
- fun attach(messenger: BinaryMessenger) {
- syu.start()
- channel = MethodChannel(messenger, CHANNEL).also { it.setMethodCallHandler(this) }
- }
- fun dispose() {
- syu.dispose()
- channel?.setMethodCallHandler(null)
- channel = null
- }
- override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
- when (call.method) {
- "getStatus" -> result.success(readStatus())
- "getVolume" -> result.success(readVolume())
- "isMusicPlaying" -> result.success(isMusicPlaying())
- "setKeepScreenOn" -> {
- val enabled = call.argument<Boolean>("enabled")
- if (enabled == null) {
- result.error("bad_arguments", "An enabled flag is required.", null)
- return
- }
- setKeepScreenOn(enabled)
- result.success(true)
- }
- "mediaKey" -> {
- val action = call.argument<String>("action")
- if (action == null) {
- result.error("bad_arguments", "A media key action is required.", null)
- return
- }
- result.success(sendMediaKey(action))
- }
- "setVolume" -> {
- val index = call.argument<Int>("index")
- if (index == null) {
- result.error("bad_arguments", "A volume index is required.", null)
- return
- }
- result.success(setVolume(index))
- }
- else -> result.notImplemented()
- }
- }
- private fun readStatus(): Map<String, Any?> {
- val batteryIntent = activity.registerReceiver(
- null,
- IntentFilter(Intent.ACTION_BATTERY_CHANGED),
- )
- val level = batteryIntent?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
- val scale = batteryIntent?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1
- val percentage = if (level >= 0 && scale > 0) (level * 100f / scale).toInt() else 0
- val status = batteryIntent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
- val charging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
- status == BatteryManager.BATTERY_STATUS_FULL
- val wifi = try {
- val connectivity = activity.getSystemService(ConnectivityManager::class.java)
- val network = connectivity?.activeNetwork
- val capabilities = network?.let { connectivity.getNetworkCapabilities(it) }
- capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
- } catch (_: Throwable) {
- false
- }
- return mapOf(
- "batteryLevel" to percentage,
- "charging" to charging,
- "wifi" to wifi,
- )
- }
- private fun audioManager(): AudioManager =
- activity.getSystemService(AudioManager::class.java)
- private fun readVolume(): Map<String, Any?> {
- val audio = audioManager()
- // Until the MCU reports its current level we keep showing Android's
- // stream value; an absolute write based on the wrong scale could jump
- // the amplifier to full volume.
- val syuActive = syu.connected && syu.level != null
- val max = if (syuActive) {
- SyuVolumeBridge.MAX_LEVEL
- } else {
- audio.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
- }
- val min = if (syuActive) 0 else minStreamVolume(audio, AudioManager.STREAM_MUSIC)
- val index = if (syuActive) {
- syu.level ?: 0
- } else {
- streamVolume(audio, AudioManager.STREAM_MUSIC)
- }
- val snapshot =
- mapOf(
- "index" to index,
- "max" to max,
- "min" to min,
- "fixed" to (if (syuActive) false else audio.isVolumeFixed),
- "source" to when {
- syuActive -> "syu"
- syu.connected -> "syu-wait"
- else -> "system"
- },
- "vendor" to syu.status,
- "streams" to
- mapOf(
- "music" to streamVolume(audio, AudioManager.STREAM_MUSIC),
- "voice" to streamVolume(audio, AudioManager.STREAM_VOICE_CALL),
- "ring" to streamVolume(audio, AudioManager.STREAM_RING),
- "alarm" to streamVolume(audio, AudioManager.STREAM_ALARM),
- "notif" to streamVolume(audio, AudioManager.STREAM_NOTIFICATION),
- "system" to streamVolume(audio, AudioManager.STREAM_SYSTEM),
- "dtmf" to streamVolume(audio, AudioManager.STREAM_DTMF),
- "access" to streamVolume(audio, AudioManager.STREAM_ACCESSIBILITY),
- ),
- )
- val log = snapshot.toString()
- if (log != lastVolumeLog) {
- lastVolumeLog = log
- Log.i(VOLUME_TAG, "read $log")
- }
- return snapshot
- }
- private fun streamVolume(audio: AudioManager, stream: Int): Int {
- return try {
- audio.getStreamVolume(stream)
- } catch (_: Throwable) {
- -1
- }
- }
- private fun minStreamVolume(audio: AudioManager, stream: Int): Int {
- return try {
- audio.getStreamMinVolume(stream)
- } catch (_: Throwable) {
- 0
- }
- }
- private fun isMusicPlaying(): Boolean {
- return try {
- audioManager().isMusicActive
- } catch (_: Throwable) {
- false
- }
- }
- private fun setKeepScreenOn(enabled: Boolean) {
- activity.runOnUiThread {
- try {
- if (enabled) {
- activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
- } else {
- activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
- }
- } catch (_: Throwable) {
- // Window flags are best-effort.
- }
- }
- }
- private fun sendMediaKey(action: String): Boolean {
- val keyCode = when (action) {
- "play" -> KeyEvent.KEYCODE_MEDIA_PLAY
- "pause" -> KeyEvent.KEYCODE_MEDIA_PAUSE
- "playPause" -> KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
- else -> return false
- }
- return try {
- val audio = audioManager()
- audio.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, keyCode))
- audio.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_UP, keyCode))
- true
- } catch (_: Throwable) {
- false
- }
- }
- private fun setVolume(index: Int): Boolean {
- // On FYT/Syu units the amplifier ignores Android's stream volume, so
- // hand the change to the MCU app first.
- if (syu.connected && syu.level != null) {
- val target = index.coerceIn(0, SyuVolumeBridge.MAX_LEVEL)
- if (syu.setLevel(target)) {
- Log.i(VOLUME_TAG, "set syu target=$target")
- return true
- }
- }
- return try {
- val audio = audioManager()
- // A fixed-volume policy means the car amplifier owns the level;
- // software volume changes are intentionally ignored by Android.
- if (audio.isVolumeFixed) {
- Log.i(VOLUME_TAG, "set $index rejected: fixed volume")
- return false
- }
- val max = audio.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
- val target = index.coerceIn(0, max)
- val before = audio.getStreamVolume(AudioManager.STREAM_MUSIC)
- audio.setStreamVolume(AudioManager.STREAM_MUSIC, target, 0)
- var after = audio.getStreamVolume(AudioManager.STREAM_MUSIC)
- Log.i(VOLUME_TAG, "set target=$target before=$before after=$after max=$max")
- if (after == target) return true
- if (after == before) {
- // Some head units only route a volume change through their own
- // panel; asking Android to show it uses a different code path
- // than a silent write.
- audio.setStreamVolume(
- AudioManager.STREAM_MUSIC,
- target,
- AudioManager.FLAG_SHOW_UI,
- )
- after = audio.getStreamVolume(AudioManager.STREAM_MUSIC)
- Log.i(VOLUME_TAG, "set with ui target=$target after=$after")
- if (after == target) return true
- }
- // Some head units ignore absolute volume writes but accept the
- // same relative steps as the hardware volume keys.
- val result = if (after == before) adjustTo(audio, target, after) else false
- Log.i(VOLUME_TAG, "relative fallback result=$result now=${streamVolume(audio, AudioManager.STREAM_MUSIC)}")
- result
- } catch (error: Throwable) {
- Log.i(VOLUME_TAG, "set $index failed: $error")
- false
- }
- }
- private fun adjustTo(audio: AudioManager, target: Int, start: Int): Boolean {
- var current = start
- var remainingSteps = audio.getStreamMaxVolume(AudioManager.STREAM_MUSIC) * 2 + 2
- while (current != target && remainingSteps-- > 0) {
- audio.adjustStreamVolume(
- AudioManager.STREAM_MUSIC,
- if (target > current) AudioManager.ADJUST_RAISE else AudioManager.ADJUST_LOWER,
- 0,
- )
- val next = audio.getStreamVolume(AudioManager.STREAM_MUSIC)
- if (next == current) return false
- current = next
- }
- return current == target
- }
- }
|