Commit f400f99ac269ee5972e563ea3fee5488ba00b6c4

Authored by yibo
1 parent a6de2220

fix(lifelog-recorder): 修复开机自动恢复录音崩溃

Android 12+ 禁止从 BOOT_COMPLETED 直接启动 microphone 类型前台服务,
异常实际出现在 RecorderService.onCreate() 而非 BootReceiver(FGS 启动是
排队执行,BootReceiver 里的 try/catch 接不住)。原先没有捕获导致每次
重启手机后录音都无法自动恢复,必须手动开一次 App。

现在优雅捕获该异常,改为弹一条可划掉的"点击恢复录音"通知;同时给
onDestroy() 加了早退保护,避免提前 return 后摸到未初始化的 lateinit
字段导致连带崩溃。已用两轮真机重启验证。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/service/BootReceiver.kt
@@ -9,11 +9,13 @@ import androidx.core.content.ContextCompat @@ -9,11 +9,13 @@ import androidx.core.content.ContextCompat
9 /** 9 /**
10 * Resumes recording on device boot. 10 * Resumes recording on device boot.
11 * 11 *
12 - * Android 14+ note: starting a `foregroundServiceType=microphone` service from a  
13 - * BOOT_COMPLETED broadcast is allowed because RECORD_AUDIO is a "while-in-use" permission  
14 - * that survives across reboots once granted. If the start fails (e.g. user has revoked  
15 - * the runtime permission), Android will throw [ForegroundServiceStartNotAllowedException]  
16 - * and we just no-op; the user opens the app and taps "开始录音" to resume. 12 + * Android 12+ note: starting a `foregroundServiceType=microphone` service from a
  13 + * BOOT_COMPLETED broadcast is NOT allowed — `ContextCompat.startForegroundService()`
  14 + * below queues the start successfully (no exception thrown here), but the actual
  15 + * `ForegroundServiceStartNotAllowedException` only surfaces later, inside
  16 + * `RecorderService.onCreate()`'s call to `startForeground()`, on the service's own
  17 + * call stack. The try/catch here can't catch that — see `RecorderService.onCreate()`
  18 + * for the real handling (posts a "tap to resume" notification instead of crashing).
17 */ 19 */
18 class BootReceiver : BroadcastReceiver() { 20 class BootReceiver : BroadcastReceiver() {
19 override fun onReceive(context: Context, intent: Intent) { 21 override fun onReceive(context: Context, intent: Intent) {
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/service/RecorderService.kt
@@ -89,7 +89,21 @@ class RecorderService : Service() { @@ -89,7 +89,21 @@ class RecorderService : Service() {
89 89
90 override fun onCreate() { 90 override fun onCreate() {
91 super.onCreate() 91 super.onCreate()
92 - startForegroundCompat() 92 + try {
  93 + startForegroundCompat()
  94 + } catch (e: Exception) {
  95 + // Android 12+ disallows starting a microphone-type FGS from a background
  96 + // trigger like BOOT_COMPLETED (ForegroundServiceStartNotAllowedException,
  97 + // API 31+ — caught as plain Exception so this also verifies on minSdk 30).
  98 + // The exception surfaces here, not in BootReceiver, because the FGS start
  99 + // is queued by startForegroundService() and only actually attempted once
  100 + // this service's onCreate() runs.
  101 + Log.w(TAG, "startForeground rejected (likely background-start restriction) — notifying user to resume manually", e)
  102 + val nm = getSystemService(android.app.NotificationManager::class.java)
  103 + nm?.notify(NOTIFICATION_ID, buildNotification("开机自动录音被系统拦截,点击打开 App 恢复录音", ongoing = false))
  104 + stopSelf()
  105 + return
  106 + }
93 acquireWakeLock() 107 acquireWakeLock()
94 isRunning = true 108 isRunning = true
95 RecorderState.setRunning(true) 109 RecorderState.setRunning(true)
@@ -171,6 +185,14 @@ class RecorderService : Service() { @@ -171,6 +185,14 @@ class RecorderService : Service() {
171 } 185 }
172 186
173 override fun onDestroy() { 187 override fun onDestroy() {
  188 + if (!isRunning) {
  189 + // onCreate() bailed out early (startForegroundCompat() rejected, e.g. by
  190 + // ForegroundServiceStartNotAllowedException) — none of the pipeline fields
  191 + // below were initialized, so the cleanup calls would throw
  192 + // UninitializedPropertyAccessException.
  193 + super.onDestroy()
  194 + return
  195 + }
174 pipelineJob?.cancel() 196 pipelineJob?.cancel()
175 capture.stop() 197 capture.stop()
176 // 关掉当前块(落 manifest 留在磁盘,下次启动 listPending 续处理)。 198 // 关掉当前块(落 manifest 留在磁盘,下次启动 listPending 续处理)。
@@ -363,7 +385,7 @@ class RecorderService : Service() { @@ -363,7 +385,7 @@ class RecorderService : Service() {
363 } 385 }
364 } 386 }
365 387
366 - private fun buildNotification(text: String = "录音中"): Notification { 388 + private fun buildNotification(text: String = "录音中", ongoing: Boolean = true): Notification {
367 val pi = PendingIntent.getActivity( 389 val pi = PendingIntent.getActivity(
368 this, 390 this,
369 0, 391 0,
@@ -375,7 +397,7 @@ class RecorderService : Service() { @@ -375,7 +397,7 @@ class RecorderService : Service() {
375 .setContentText(text) 397 .setContentText(text)
376 .setSmallIcon(android.R.drawable.presence_audio_online) 398 .setSmallIcon(android.R.drawable.presence_audio_online)
377 .setContentIntent(pi) 399 .setContentIntent(pi)
378 - .setOngoing(true) 400 + .setOngoing(ongoing)
379 .setCategory(NotificationCompat.CATEGORY_SERVICE) 401 .setCategory(NotificationCompat.CATEGORY_SERVICE)
380 .build() 402 .build()
381 } 403 }