- BT (RFCOMM SPP 00001105) вместо WiFi: BtGlassesTransport/BtServerTransport, реконнект 3с - PlaybackService (foreground, WAKE_LOCK) против морозов HONOR - Ассистент: AgentTools (TimeTool ISO 8601), TOOL_CALL-протокол, сессии SQLite (UUID) - Чат-UI: создание/сброс/переключение сессий, автообновление ленты 2с - LlmClient на прямом OkHttp (Koog давал пустой content на длинных историях) - Голосовой цикл: тройной тап на очках → STT → активная сессия → ShowText - Тесты: AgentToolsTest (3), GlassesServerTest переписан (6) — зелёные
7.1 KiB
Задача: телефон не должен засыпать во время просмотра (foreground service)
Симптом (пользователь, 21.08)
«Если я телефон отправлю спать, то звук на телефоне прекращается. А когда на телефоне прекращается звук, очки тоже перестают играть.»
Корень (диагноз подтверждён)
Архитектура: звук играет ТОЛЬКО на телефоне (у очков нет динамиков), видео —
на очках, синк по позиции (очки — мастер, 5с-цикл). На телефоне НЕТ
foreground service и НЕТ wake/wifi lock — приложение живёт только в Activity
(app-phone/src/main/AndroidManifest.xml: только MainActivity, пермишенов
WAKE_LOCK нет). При выключении экрана HONOR (Android 16, агрессивная фоновая
политика) замораживает/убивает процесс:
- WS-сервер (
GlassesServer, :8080) умирает → очки теряют связь (ping 5с) → автоматика обрыва на очках («связь потеряна — пауза») останавливает ВИДЕО. AudioSyncPlayer(ExoPlayer) заморожен → ЗВУК встаёт.
Решение
Добавить в app-phone foreground service типа mediaPlayback + WAKE_LOCK +
WifiLock, чтобы процесс (WS-сервер + аудио-плеер) жил при выключенном экране
телефона. Очки тогда не теряют связь, видео не паузится, звук играет.
1. Манифест (app-phone/src/main/AndroidManifest.xml)
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
Внутри <application> — сервис:
<service
android:name=".PlaybackService"
android:exported="false"
android:foregroundServiceType="mediaPlayback" />
2. Сервис app-phone/src/main/kotlin/pw/binom/viewmate/phone/PlaybackService.kt
class PlaybackService : Service() {
companion object {
const val CHANNEL_ID = "viewmate_playback"
const val NOTIFICATION_ID = 1
// аккуратный запуск: ContextCompat.startForegroundService(context, intent)
fun start(context: Context) {
ContextCompat.startForegroundService(
context, Intent(context, PlaybackService::class.java)
)
}
fun stop(context: Context) {
context.stopService(Intent(context, PlaybackService::class.java))
}
}
private var wakeLock: PowerManager.WakeLock? = null
private var wifiLock: WifiManager.WifiLock? = null
override fun onCreate() {
super.onCreate()
// канал уведомлений (Android 8+)
val channel = NotificationChannel(
CHANNEL_ID, "ViewMate playback",
NotificationManager.IMPORTANCE_LOW,
).apply { setShowBadge(false) }
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_media_play)
.setContentTitle("ViewMate")
.setContentText("Звук на телефоне при просмотре на очках")
.setOngoing(true)
.build()
startForeground(NOTIFICATION_ID, notification)
// держим CPU и WiFi живыми при выключенном экране
wakeLock = (getSystemService(POWER_SERVICE) as PowerManager)
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "viewmate:audio")
?.apply { acquire() }
wifiLock = (getApplicationContext().getSystemService(WIFI_SERVICE) as WifiManager)
.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "viewmate:wifi")
?.apply { acquire() }
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int =
START_NOT_STICKY
override fun onDestroy() {
wakeLock?.takeIf { it.isHeld }?.release()
wifiLock?.takeIf { it.isHeld }?.release()
super.onDestroy()
}
override fun onBind(intent: Intent?): IBinder? = null
}
Импорты: androidx.core.app.NotificationCompat, androidx.core.content.ContextCompat.
Check: в gradle уже есть androidx.core (проверить libs.versions.toml — ktor/compose
тянут core-ktx, но для чистоты добавить androidx.core:core-ktx если нет).
3. Запуск/остановка сервиса в PhoneApp.kt
- Старт — при каждом старте аудио (3 места):
autostartAudio()(строка ~173, передaudioSync.play(source, positionMs))DetailsScreen.onWatch— там где вызываетсяapp.audioSync.play(source, positionMs)(строка ~127) и второй play (строка ~215)- ОБЩИЙ подход: раз звук — сервис. Проще всего: стартовать в
PhoneApp.onCreate()сразу (сервис лёгкий, уведомление висит пока приложение живо) — НО только если пользователь не против постоянного уведомления. РЕШЕНИЕ: старт в onCreate, стоп в onTerminate — сервис живёт пока жив телефон-компаньон (WS-сервер тоже живёт всё время), уведомление — постоянный индикатор «ViewMate работает». Это проще и покрывает и watch, и autostartAudio, и STT-сессии.
- Стоп: в
onTerminate()—PlaybackService.stop(this).
Решение принято: старт в onCreate, стоп в onTerminate (сервис = спутник WS-сервера, а не аудио-сессии; уведомление постоянно, но это норма для приложения-компаньона — так работает, например, в паре с очками).
4. Проверка
- Сборка:
./gradlew :app-phone:assembleDebug— успех. - Юнит-тесты:
./gradlew :app-phone:testDebugUnitTest— зелёные. - На железе (через 142): установить APK, запустить, начать просмотр,
adb -s <phone> shell input keyevent 26(экран телефона в сон) → проверить: звук играет, видео на очках играет (позиция растёт в логах), сервис жив:adb shell dumpsys activity services | grep -A3 PlaybackService.