Files
subochev bbf7a020d4 Ассистент Порфирий: тула времени, сессии, чат-UI, голосовой цикл; BT-транспорт очки↔телефон
- 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) — зелёные
2026-08-22 16:31:04 +03:00

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, агрессивная фоновая политика) замораживает/убивает процесс:

  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)

<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. Проверка

  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.