feat: стабилизация гашения экрана очков (пауза/резюм звука)
Проблемы на железе (RayNeo X2 + HONOR): при гашении экрана позиция ExoPlayer «набегала» (мусор 42 мин), откат не срабатывал (markPosition сохранял мусор), доставка паузы телефону 5-10с (RayNeo вырубает WiFi-радио при screen-off, игнорируя WifiLock). Решение: - markPosition(positionMs, trusted): защита от мусорных скачков (>30с за цикл = игнор); seek через трекбар помечает позицию trusted (rollback откатывает к seek, не к началу) - rollback при resume: откат к последней достоверной позиции, если текущая убежала вперёд >5с - WifiLock (FULL_HIGH_PERF) при просмотре + ensureWifiLock в точках гашения (страховка; RayNeo игнорирует, но не мешает) - Watchdog на телефоне: очки молчат >5с (AUDIO_SILENCE_PAUSE_MS) → пауза звука (гарантированная верхняя граница) - sendPlaybackPositionNow с повторами (0/2/5с) при включении экрана — свежая позиция уходит при пробуждении радио - Диагностика: события пишутся в files/events.log (logcat очков забит EGL-спамом) - Тесты: +14 (rollback/скачки/trusted seek) - Железо подтверждено: позиция после гашения идеальная (продолжение с места паузы, не в начало), звук пауза ≤5-6с, синк diff < 1000
This commit is contained in:
@@ -1,6 +1,15 @@
|
||||
package pw.binom.viewmate.glasses
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.hardware.display.DisplayManager
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.Display
|
||||
import java.io.File
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.core.view.WindowCompat
|
||||
@@ -25,6 +34,32 @@ import pw.binom.viewmate.glasses.ui.GlassesMovieScreen
|
||||
class MainActivity : ComponentActivity() {
|
||||
private var resumeAfterScreenOn = false
|
||||
|
||||
private val screenReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
Intent.ACTION_SCREEN_OFF -> {
|
||||
log("ui", "SCREEN_OFF receiver")
|
||||
val was = GlassesApp.instance.movieController.pauseIfPlaying()
|
||||
resumeAfterScreenOn = resumeAfterScreenOn || was
|
||||
logEvent("SCREEN_OFF: pause wasPlaying=$was")
|
||||
GlassesApp.instance.movieController.ensureWifiLock()
|
||||
GlassesApp.instance.hostConnection.sendPlaybackPositionNow()
|
||||
if (was) log("ui", "экран погас (receiver) — пауза")
|
||||
}
|
||||
Intent.ACTION_SCREEN_ON -> {
|
||||
log("ui", "SCREEN_ON receiver")
|
||||
logEvent("SCREEN_ON: resume flag=$resumeAfterScreenOn")
|
||||
if (resumeAfterScreenOn) {
|
||||
resumeAfterScreenOn = false
|
||||
GlassesApp.instance.movieController.resume()
|
||||
sendPositionWithRetries()
|
||||
log("ui", "экран включён (receiver) — продолжаю")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
@@ -43,25 +78,94 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
hideSystemBars()
|
||||
log("ui", "иммерсивный режим включён")
|
||||
val filter = IntentFilter().apply {
|
||||
addAction(Intent.ACTION_SCREEN_OFF)
|
||||
addAction(Intent.ACTION_SCREEN_ON)
|
||||
}
|
||||
registerReceiver(screenReceiver, filter)
|
||||
|
||||
val dm = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
|
||||
dm.registerDisplayListener(object : DisplayManager.DisplayListener {
|
||||
override fun onDisplayAdded(displayId: Int) {}
|
||||
override fun onDisplayRemoved(displayId: Int) {}
|
||||
override fun onDisplayChanged(displayId: Int) {
|
||||
if (displayId != Display.DEFAULT_DISPLAY) return
|
||||
val state = dm.getDisplay(displayId).state
|
||||
logEvent("display state=$state")
|
||||
if (state == Display.STATE_OFF) {
|
||||
val was = GlassesApp.instance.movieController.pauseIfPlaying()
|
||||
resumeAfterScreenOn = resumeAfterScreenOn || was
|
||||
logEvent("display OFF: pause wasPlaying=$was")
|
||||
GlassesApp.instance.movieController.ensureWifiLock()
|
||||
GlassesApp.instance.hostConnection.sendPlaybackPositionNow()
|
||||
} else if (state == Display.STATE_ON) {
|
||||
logEvent("display ON: resume flag=$resumeAfterScreenOn")
|
||||
if (resumeAfterScreenOn) {
|
||||
resumeAfterScreenOn = false
|
||||
GlassesApp.instance.movieController.resume()
|
||||
sendPositionWithRetries()
|
||||
}
|
||||
}
|
||||
}
|
||||
}, null)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
log("ui", "onPause")
|
||||
val was = GlassesApp.instance.movieController.pauseIfPlaying()
|
||||
resumeAfterScreenOn = was
|
||||
resumeAfterScreenOn = resumeAfterScreenOn || was
|
||||
logEvent("onPause: pause wasPlaying=$was")
|
||||
GlassesApp.instance.movieController.ensureWifiLock()
|
||||
GlassesApp.instance.hostConnection.sendPlaybackPositionNow()
|
||||
if (was) log("ui", "экран погас — пауза")
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
log("ui", "onResume")
|
||||
hideSystemBars()
|
||||
logEvent("onResume: resume flag=$resumeAfterScreenOn")
|
||||
if (resumeAfterScreenOn) {
|
||||
resumeAfterScreenOn = false
|
||||
GlassesApp.instance.movieController.resume()
|
||||
sendPositionWithRetries()
|
||||
log("ui", "экран включён — продолжаю")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
logEvent("focus=$hasFocus")
|
||||
log("ui", "focus=$hasFocus")
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
unregisterReceiver(screenReceiver)
|
||||
}
|
||||
|
||||
/**
|
||||
* Немедленная отправка позиции + повторы (0с/2с/5с): радио RayNeo просыпается
|
||||
* ~5-7с после включения экрана, первый PlaybackPosition уходит только когда радио
|
||||
* ожило — свежая позиция (playing=true) доедет сразу после пробуждения.
|
||||
* Повторы безвредны: телефон идемпотентен.
|
||||
*/
|
||||
private fun sendPositionWithRetries() {
|
||||
val conn = GlassesApp.instance.hostConnection
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
conn.sendPlaybackPositionNow()
|
||||
handler.postDelayed({ conn.sendPlaybackPositionNow() }, 2_000)
|
||||
handler.postDelayed({ conn.sendPlaybackPositionNow() }, 5_000)
|
||||
}
|
||||
|
||||
private fun logEvent(msg: String) {
|
||||
runCatching {
|
||||
val f = File(filesDir, "events.log")
|
||||
f.appendText("${System.currentTimeMillis()} $msg\n")
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideSystemBars() {
|
||||
window.decorView.post {
|
||||
WindowInsetsControllerCompat(window, window.decorView).apply {
|
||||
|
||||
Reference in New Issue
Block a user