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 | 9 | /** |
| 10 | 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 | 20 | class BootReceiver : BroadcastReceiver() { |
| 19 | 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 | 89 | |
| 90 | 90 | override fun onCreate() { |
| 91 | 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 | 107 | acquireWakeLock() |
| 94 | 108 | isRunning = true |
| 95 | 109 | RecorderState.setRunning(true) |
| ... | ... | @@ -171,6 +185,14 @@ class RecorderService : Service() { |
| 171 | 185 | } |
| 172 | 186 | |
| 173 | 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 | 196 | pipelineJob?.cancel() |
| 175 | 197 | capture.stop() |
| 176 | 198 | // 关掉当前块(落 manifest 留在磁盘,下次启动 listPending 续处理)。 |
| ... | ... | @@ -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 | 389 | val pi = PendingIntent.getActivity( |
| 368 | 390 | this, |
| 369 | 391 | 0, |
| ... | ... | @@ -375,7 +397,7 @@ class RecorderService : Service() { |
| 375 | 397 | .setContentText(text) |
| 376 | 398 | .setSmallIcon(android.R.drawable.presence_audio_online) |
| 377 | 399 | .setContentIntent(pi) |
| 378 | - .setOngoing(true) | |
| 400 | + .setOngoing(ongoing) | |
| 379 | 401 | .setCategory(NotificationCompat.CATEGORY_SERVICE) |
| 380 | 402 | .build() |
| 381 | 403 | } | ... | ... |