Commit f400f99ac269ee5972e563ea3fee5488ba00b6c4
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>
Showing
2 changed files
with
32 additions
and
8 deletions
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 | } |