SystemBridge.kt 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. package com.wasp.cardash_launcher
  2. import android.app.Activity
  3. import android.content.Intent
  4. import android.content.IntentFilter
  5. import android.media.AudioManager
  6. import android.net.ConnectivityManager
  7. import android.net.NetworkCapabilities
  8. import android.os.BatteryManager
  9. import android.util.Log
  10. import android.view.KeyEvent
  11. import android.view.WindowManager
  12. import io.flutter.plugin.common.BinaryMessenger
  13. import io.flutter.plugin.common.MethodCall
  14. import io.flutter.plugin.common.MethodChannel
  15. /** Battery, Wi-Fi and media-volume bridge. */
  16. class SystemBridge(private val activity: Activity) : MethodChannel.MethodCallHandler {
  17. companion object {
  18. const val CHANNEL = "com.wasp.cardash_launcher/system"
  19. private const val VOLUME_TAG = "CarDashVolume"
  20. }
  21. private var channel: MethodChannel? = null
  22. private var lastVolumeLog: String? = null
  23. fun attach(messenger: BinaryMessenger) {
  24. channel = MethodChannel(messenger, CHANNEL).also { it.setMethodCallHandler(this) }
  25. }
  26. fun dispose() {
  27. channel?.setMethodCallHandler(null)
  28. channel = null
  29. }
  30. override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
  31. when (call.method) {
  32. "getStatus" -> result.success(readStatus())
  33. "getVolume" -> result.success(readVolume())
  34. "isMusicPlaying" -> result.success(isMusicPlaying())
  35. "setKeepScreenOn" -> {
  36. val enabled = call.argument<Boolean>("enabled")
  37. if (enabled == null) {
  38. result.error("bad_arguments", "An enabled flag is required.", null)
  39. return
  40. }
  41. setKeepScreenOn(enabled)
  42. result.success(true)
  43. }
  44. "mediaKey" -> {
  45. val action = call.argument<String>("action")
  46. if (action == null) {
  47. result.error("bad_arguments", "A media key action is required.", null)
  48. return
  49. }
  50. result.success(sendMediaKey(action))
  51. }
  52. "setVolume" -> {
  53. val index = call.argument<Int>("index")
  54. if (index == null) {
  55. result.error("bad_arguments", "A volume index is required.", null)
  56. return
  57. }
  58. result.success(setVolume(index))
  59. }
  60. else -> result.notImplemented()
  61. }
  62. }
  63. private fun readStatus(): Map<String, Any?> {
  64. val batteryIntent = activity.registerReceiver(
  65. null,
  66. IntentFilter(Intent.ACTION_BATTERY_CHANGED),
  67. )
  68. val level = batteryIntent?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
  69. val scale = batteryIntent?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1
  70. val percentage = if (level >= 0 && scale > 0) (level * 100f / scale).toInt() else 0
  71. val status = batteryIntent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
  72. val charging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
  73. status == BatteryManager.BATTERY_STATUS_FULL
  74. val wifi = try {
  75. val connectivity = activity.getSystemService(ConnectivityManager::class.java)
  76. val network = connectivity?.activeNetwork
  77. val capabilities = network?.let { connectivity.getNetworkCapabilities(it) }
  78. capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
  79. } catch (_: Throwable) {
  80. false
  81. }
  82. return mapOf(
  83. "batteryLevel" to percentage,
  84. "charging" to charging,
  85. "wifi" to wifi,
  86. )
  87. }
  88. private fun audioManager(): AudioManager =
  89. activity.getSystemService(AudioManager::class.java)
  90. private fun readVolume(): Map<String, Any?> {
  91. val audio = audioManager()
  92. val snapshot =
  93. mapOf(
  94. "index" to streamVolume(audio, AudioManager.STREAM_MUSIC),
  95. "max" to audio.getStreamMaxVolume(AudioManager.STREAM_MUSIC),
  96. "fixed" to audio.isVolumeFixed,
  97. "streams" to
  98. mapOf(
  99. "music" to streamVolume(audio, AudioManager.STREAM_MUSIC),
  100. "voice" to streamVolume(audio, AudioManager.STREAM_VOICE_CALL),
  101. "ring" to streamVolume(audio, AudioManager.STREAM_RING),
  102. "alarm" to streamVolume(audio, AudioManager.STREAM_ALARM),
  103. "notif" to streamVolume(audio, AudioManager.STREAM_NOTIFICATION),
  104. "system" to streamVolume(audio, AudioManager.STREAM_SYSTEM),
  105. "dtmf" to streamVolume(audio, AudioManager.STREAM_DTMF),
  106. "access" to streamVolume(audio, AudioManager.STREAM_ACCESSIBILITY),
  107. ),
  108. )
  109. val log = snapshot.toString()
  110. if (log != lastVolumeLog) {
  111. lastVolumeLog = log
  112. Log.i(VOLUME_TAG, "read $log")
  113. }
  114. return snapshot
  115. }
  116. private fun streamVolume(audio: AudioManager, stream: Int): Int {
  117. return try {
  118. audio.getStreamVolume(stream)
  119. } catch (_: Throwable) {
  120. -1
  121. }
  122. }
  123. private fun isMusicPlaying(): Boolean {
  124. return try {
  125. audioManager().isMusicActive
  126. } catch (_: Throwable) {
  127. false
  128. }
  129. }
  130. private fun setKeepScreenOn(enabled: Boolean) {
  131. activity.runOnUiThread {
  132. try {
  133. if (enabled) {
  134. activity.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
  135. } else {
  136. activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
  137. }
  138. } catch (_: Throwable) {
  139. // Window flags are best-effort.
  140. }
  141. }
  142. }
  143. private fun sendMediaKey(action: String): Boolean {
  144. val keyCode = when (action) {
  145. "play" -> KeyEvent.KEYCODE_MEDIA_PLAY
  146. "pause" -> KeyEvent.KEYCODE_MEDIA_PAUSE
  147. "playPause" -> KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
  148. else -> return false
  149. }
  150. return try {
  151. val audio = audioManager()
  152. audio.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, keyCode))
  153. audio.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_UP, keyCode))
  154. true
  155. } catch (_: Throwable) {
  156. false
  157. }
  158. }
  159. private fun setVolume(index: Int): Boolean {
  160. return try {
  161. val audio = audioManager()
  162. // A fixed-volume policy means the car amplifier owns the level;
  163. // software volume changes are intentionally ignored by Android.
  164. if (audio.isVolumeFixed) {
  165. Log.i(VOLUME_TAG, "set $index rejected: fixed volume")
  166. return false
  167. }
  168. val max = audio.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
  169. val target = index.coerceIn(0, max)
  170. val before = audio.getStreamVolume(AudioManager.STREAM_MUSIC)
  171. audio.setStreamVolume(AudioManager.STREAM_MUSIC, target, 0)
  172. val after = audio.getStreamVolume(AudioManager.STREAM_MUSIC)
  173. Log.i(VOLUME_TAG, "set target=$target before=$before after=$after max=$max")
  174. if (after == target) return true
  175. // Some head units ignore absolute volume writes but accept the
  176. // same relative steps as the hardware volume keys.
  177. val result = if (after == before) adjustTo(audio, target, after) else false
  178. Log.i(VOLUME_TAG, "relative fallback result=$result now=${streamVolume(audio, AudioManager.STREAM_MUSIC)}")
  179. result
  180. } catch (error: Throwable) {
  181. Log.i(VOLUME_TAG, "set $index failed: $error")
  182. false
  183. }
  184. }
  185. private fun adjustTo(audio: AudioManager, target: Int, start: Int): Boolean {
  186. var current = start
  187. var remainingSteps = audio.getStreamMaxVolume(AudioManager.STREAM_MUSIC) * 2 + 2
  188. while (current != target && remainingSteps-- > 0) {
  189. audio.adjustStreamVolume(
  190. AudioManager.STREAM_MUSIC,
  191. if (target > current) AudioManager.ADJUST_RAISE else AudioManager.ADJUST_LOWER,
  192. 0,
  193. )
  194. val next = audio.getStreamVolume(AudioManager.STREAM_MUSIC)
  195. if (next == current) return false
  196. current = next
  197. }
  198. return current == target
  199. }
  200. }