| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- 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
- fun attach(messenger: BinaryMessenger) {
- channel = MethodChannel(messenger, CHANNEL).also { it.setMethodCallHandler(this) }
- }
- fun 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()
- val snapshot =
- mapOf(
- "index" to streamVolume(audio, AudioManager.STREAM_MUSIC),
- "max" to audio.getStreamMaxVolume(AudioManager.STREAM_MUSIC),
- "fixed" to audio.isVolumeFixed,
- "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 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 {
- 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)
- val after = audio.getStreamVolume(AudioManager.STREAM_MUSIC)
- Log.i(VOLUME_TAG, "set target=$target before=$before after=$after max=$max")
- 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
- }
- }
|