bbf7a020d4
- 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) — зелёные
133 lines
7.1 KiB
Markdown
133 lines
7.1 KiB
Markdown
# Задача: телефон не должен засыпать во время просмотра (foreground service)
|
|
|
|
## Симптом (пользователь, 21.08)
|
|
|
|
«Если я телефон отправлю спать, то звук на телефоне прекращается. А когда на
|
|
телефоне прекращается звук, очки тоже перестают играть.»
|
|
|
|
## Корень (диагноз подтверждён)
|
|
|
|
Архитектура: звук играет ТОЛЬКО на телефоне (у очков нет динамиков), видео —
|
|
на очках, синк по позиции (очки — мастер, 5с-цикл). На телефоне НЕТ
|
|
foreground service и НЕТ wake/wifi lock — приложение живёт только в Activity
|
|
(`app-phone/src/main/AndroidManifest.xml`: только MainActivity, пермишенов
|
|
WAKE_LOCK нет). При выключении экрана HONOR (Android 16, агрессивная фоновая
|
|
политика) замораживает/убивает процесс:
|
|
|
|
1. WS-сервер (`GlassesServer`, :8080) умирает → очки теряют связь (ping 5с) →
|
|
автоматика обрыва на очках («связь потеряна — пауза») останавливает ВИДЕО.
|
|
2. `AudioSyncPlayer` (ExoPlayer) заморожен → ЗВУК встаёт.
|
|
|
|
## Решение
|
|
|
|
Добавить в app-phone **foreground service** типа `mediaPlayback` + WAKE_LOCK +
|
|
WifiLock, чтобы процесс (WS-сервер + аудио-плеер) жил при выключенном экране
|
|
телефона. Очки тогда не теряют связь, видео не паузится, звук играет.
|
|
|
|
### 1. Манифест (`app-phone/src/main/AndroidManifest.xml`)
|
|
|
|
```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>` — сервис:
|
|
```xml
|
|
<service
|
|
android:name=".PlaybackService"
|
|
android:exported="false"
|
|
android:foregroundServiceType="mediaPlayback" />
|
|
```
|
|
|
|
### 2. Сервис `app-phone/src/main/kotlin/pw/binom/viewmate/phone/PlaybackService.kt`
|
|
|
|
```kotlin
|
|
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. Проверка
|
|
|
|
1. Сборка: `./gradlew :app-phone:assembleDebug` — успех.
|
|
2. Юнит-тесты: `./gradlew :app-phone:testDebugUnitTest` — зелёные.
|
|
3. На железе (через 142): установить APK, запустить, начать просмотр,
|
|
`adb -s <phone> shell input keyevent 26` (экран телефона в сон) →
|
|
проверить: звук играет, видео на очках играет (позиция растёт в логах),
|
|
сервис жив: `adb shell dumpsys activity services | grep -A3 PlaybackService`.
|