diff --git a/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/service/BootReceiver.kt b/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/service/BootReceiver.kt index a51144e..2ea25ba 100644 --- a/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/service/BootReceiver.kt +++ b/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/service/BootReceiver.kt @@ -9,11 +9,13 @@ import androidx.core.content.ContextCompat /** * Resumes recording on device boot. * - * Android 14+ note: starting a `foregroundServiceType=microphone` service from a - * BOOT_COMPLETED broadcast is allowed because RECORD_AUDIO is a "while-in-use" permission - * that survives across reboots once granted. If the start fails (e.g. user has revoked - * the runtime permission), Android will throw [ForegroundServiceStartNotAllowedException] - * and we just no-op; the user opens the app and taps "开始录音" to resume. + * Android 12+ note: starting a `foregroundServiceType=microphone` service from a + * BOOT_COMPLETED broadcast is NOT allowed — `ContextCompat.startForegroundService()` + * below queues the start successfully (no exception thrown here), but the actual + * `ForegroundServiceStartNotAllowedException` only surfaces later, inside + * `RecorderService.onCreate()`'s call to `startForeground()`, on the service's own + * call stack. The try/catch here can't catch that — see `RecorderService.onCreate()` + * for the real handling (posts a "tap to resume" notification instead of crashing). */ class BootReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { diff --git a/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/service/RecorderService.kt b/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/service/RecorderService.kt index a7ee99b..54aff41 100644 --- a/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/service/RecorderService.kt +++ b/lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/service/RecorderService.kt @@ -89,7 +89,21 @@ class RecorderService : Service() { override fun onCreate() { super.onCreate() - startForegroundCompat() + try { + startForegroundCompat() + } catch (e: Exception) { + // Android 12+ disallows starting a microphone-type FGS from a background + // trigger like BOOT_COMPLETED (ForegroundServiceStartNotAllowedException, + // API 31+ — caught as plain Exception so this also verifies on minSdk 30). + // The exception surfaces here, not in BootReceiver, because the FGS start + // is queued by startForegroundService() and only actually attempted once + // this service's onCreate() runs. + Log.w(TAG, "startForeground rejected (likely background-start restriction) — notifying user to resume manually", e) + val nm = getSystemService(android.app.NotificationManager::class.java) + nm?.notify(NOTIFICATION_ID, buildNotification("开机自动录音被系统拦截,点击打开 App 恢复录音", ongoing = false)) + stopSelf() + return + } acquireWakeLock() isRunning = true RecorderState.setRunning(true) @@ -171,6 +185,14 @@ class RecorderService : Service() { } override fun onDestroy() { + if (!isRunning) { + // onCreate() bailed out early (startForegroundCompat() rejected, e.g. by + // ForegroundServiceStartNotAllowedException) — none of the pipeline fields + // below were initialized, so the cleanup calls would throw + // UninitializedPropertyAccessException. + super.onDestroy() + return + } pipelineJob?.cancel() capture.stop() // 关掉当前块(落 manifest 留在磁盘,下次启动 listPending 续处理)。 @@ -363,7 +385,7 @@ class RecorderService : Service() { } } - private fun buildNotification(text: String = "录音中"): Notification { + private fun buildNotification(text: String = "录音中", ongoing: Boolean = true): Notification { val pi = PendingIntent.getActivity( this, 0, @@ -375,7 +397,7 @@ class RecorderService : Service() { .setContentText(text) .setSmallIcon(android.R.drawable.presence_audio_online) .setContentIntent(pi) - .setOngoing(true) + .setOngoing(ongoing) .setCategory(NotificationCompat.CATEGORY_SERVICE) .build() }