экран очков: единый тумблер на «Пульте» + очки шлют реальное состояние экрана
- shared: новое сообщение ScreenState(on) от очков телефону - glasses: слушаем ACTION_SCREEN_ON/OFF (PowerManager.isInteractive), шлём состояние при подключении телефона и на каждое изменение; после ScreenControl — актуализация - phone: VmPhoneController.screenOn (StateFlow<Boolean?>), оптимистичное обновление в setScreen - phone: на экране «Пульт» одна кнопка-тумблер «Погасить/Разбудить экран очков», подпись отражает реальное состояние, disabled без Канала 1
This commit is contained in:
@@ -4,14 +4,17 @@ import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.media.AudioManager
|
||||
import android.net.nsd.NsdManager
|
||||
import android.net.nsd.NsdServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.lifecycle.LifecycleService
|
||||
@@ -46,6 +49,7 @@ import pw.binom.rayneovm.shared.CursorClick
|
||||
import pw.binom.rayneovm.shared.CursorMove
|
||||
import pw.binom.rayneovm.shared.RequestShizukuStatus
|
||||
import pw.binom.rayneovm.shared.ScreenControl
|
||||
import pw.binom.rayneovm.shared.ScreenState
|
||||
import pw.binom.rayneovm.shared.ShizukuStatus
|
||||
import pw.binom.rayneovm.shared.audio.AudioConfig
|
||||
import pw.binom.rayneovm.shared.audio.AudioDatagram
|
||||
@@ -118,6 +122,7 @@ class VmService : LifecycleService() {
|
||||
startAudioServer()
|
||||
startAudioPump()
|
||||
registerMdns()
|
||||
registerScreenReceiver()
|
||||
// Сообщаем телефону о Shizuku, как только меняется его состояние (сервер поднялся
|
||||
// или нам выдали permission) — чтобы телефон не опрашивал вручную.
|
||||
lifecycleScope.launch {
|
||||
@@ -169,6 +174,9 @@ class VmService : LifecycleService() {
|
||||
*/
|
||||
private val currentConn = java.util.concurrent.atomic.AtomicReference<String?>(null)
|
||||
|
||||
/** Ресивер `ACTION_SCREEN_ON/OFF`: сообщаем телефону реальное состояние экрана очков. */
|
||||
private var screenReceiver: BroadcastReceiver? = null
|
||||
|
||||
private fun startServer() {
|
||||
server = Transports.wifiServer(port = WS_PORT, path = WS_PATH)
|
||||
lifecycleScope.launch(kotlinx.coroutines.Dispatchers.IO) {
|
||||
@@ -184,6 +192,8 @@ class VmService : LifecycleService() {
|
||||
controller.onPhoneConnected()
|
||||
// И сразу отдать телефoну состояние Shizuku (для ADB-бутстрапа).
|
||||
lifecycleScope.launch { sendShizukuStatus(connId) }
|
||||
// ...и реальное состояние экрана (для кнопки-тумблера на телефоне).
|
||||
lifecycleScope.launch { sendScreenState(connId) }
|
||||
},
|
||||
onDisconnected = { connId ->
|
||||
Log.i(TAG, "Phone disconnected: $connId")
|
||||
@@ -238,6 +248,11 @@ class VmService : LifecycleService() {
|
||||
is ScreenControl -> {
|
||||
Log.i(TAG, "ScreenControl: sleep=${msg.sleep}")
|
||||
controller.setScreen(msg.sleep)
|
||||
// Экран переключается чуть позже инжекта — сообщаем телефону актуальное состояние.
|
||||
lifecycleScope.launch {
|
||||
kotlinx.coroutines.delay(600)
|
||||
pushScreenState()
|
||||
}
|
||||
}
|
||||
is SetText -> {
|
||||
Log.i(TAG, "SetText: displayId=${msg.displayId} len=${msg.text.length}")
|
||||
@@ -461,6 +476,55 @@ class VmService : LifecycleService() {
|
||||
sendShizukuStatus(connId)
|
||||
}
|
||||
|
||||
/** Текущее состояние экрана очков (true — экран включён). */
|
||||
private fun currentScreenOn(): Boolean =
|
||||
(getSystemService(POWER_SERVICE) as? PowerManager)?.isInteractive ?: true
|
||||
|
||||
/** Отправляет состояние экрана конкретной сессии (ответ на подключение / изменение). */
|
||||
private suspend fun sendScreenState(connId: String) {
|
||||
val on = currentScreenOn()
|
||||
val json = VmProtocol.encode(ScreenState(on = on))
|
||||
try {
|
||||
server?.send(connId, json)
|
||||
} catch (t: Throwable) {
|
||||
Log.w(TAG, "Не удалось отправить ScreenState: ${t.message}")
|
||||
}
|
||||
Log.i(TAG, "ScreenState: on=$on")
|
||||
}
|
||||
|
||||
/** Отправляет состояние экрана текущей активной сессии (если есть). */
|
||||
private suspend fun pushScreenState() {
|
||||
val connId = currentConn.get() ?: return
|
||||
sendScreenState(connId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Слушаем системные `ACTION_SCREEN_ON/OFF`, чтобы держать телефон в курсе реального
|
||||
* состояния экрана — это питает кнопку-тумблер «экран вкл/выкл» на экране «Пульт».
|
||||
*/
|
||||
private fun registerScreenReceiver() {
|
||||
if (screenReceiver != null) return
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
val on = intent?.action == Intent.ACTION_SCREEN_ON
|
||||
Log.i(TAG, "Экран ${if (on) "включён" else "погашен"}")
|
||||
lifecycleScope.launch { pushScreenState() }
|
||||
}
|
||||
}
|
||||
val filter = IntentFilter().apply {
|
||||
addAction(Intent.ACTION_SCREEN_ON)
|
||||
addAction(Intent.ACTION_SCREEN_OFF)
|
||||
}
|
||||
registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED)
|
||||
screenReceiver = receiver
|
||||
}
|
||||
|
||||
private fun unregisterScreenReceiver() {
|
||||
val r = screenReceiver ?: return
|
||||
screenReceiver = null
|
||||
runCatching { unregisterReceiver(r) }
|
||||
}
|
||||
|
||||
private fun createNotificationChannel(): String {
|
||||
val channelId = "vm_service_channel"
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
@@ -499,6 +563,7 @@ class VmService : LifecycleService() {
|
||||
audioServer = null
|
||||
audioSessions.clear()
|
||||
unregisterMdns()
|
||||
unregisterScreenReceiver()
|
||||
server?.close()
|
||||
server = null
|
||||
instance = null
|
||||
|
||||
Reference in New Issue
Block a user