From 9d8c1af13b163772002d6a3235921cc2c3ca5c42 Mon Sep 17 00:00:00 2001 From: adsamcik Date: Thu, 25 Jun 2026 12:37:43 +0200 Subject: [PATCH] feat(tracker): resilient user-session restart + previous-exit classification - TrackerService: return START_REDELIVER_INTENT (was START_STICKY) for user-initiated sessions so Android re-delivers the original start intent (carrying ARG_IS_USER_INITIATED) after a system kill, instead of a null intent that forces in-memory fallback recovery. - Application: classify the previous process exit via ApplicationExitInfo (API 30+) and log abnormal terminations (LOW_MEMORY/SIGNALED/CRASH/ANR) vs normal ones. Foundation for crash-informed recovery and for respecting user-requested force-stops. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../com/adsamcik/tracker/app/Application.kt | 48 +++++++++++++++++++ .../tracker/tracker/service/TrackerService.kt | 9 ++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/adsamcik/tracker/app/Application.kt b/app/src/main/java/com/adsamcik/tracker/app/Application.kt index ffaa21794..8a5febc06 100644 --- a/app/src/main/java/com/adsamcik/tracker/app/Application.kt +++ b/app/src/main/java/com/adsamcik/tracker/app/Application.kt @@ -1,5 +1,7 @@ package com.adsamcik.tracker.app +import android.app.ActivityManager +import android.app.ApplicationExitInfo import android.os.Build import androidx.annotation.MainThread import androidx.annotation.WorkerThread @@ -186,6 +188,7 @@ class Application : AndroidApplication(), Configuration.Provider { Reporter.initialize(this@Application) Logger.initialize(this@Application) CrashHandler(this@Application).initialize() + logPreviousExitReason() if (!isRobolectricUnitTest()) { initializeModules() } @@ -199,6 +202,51 @@ class Application : AndroidApplication(), Configuration.Provider { private fun isRobolectricUnitTest(): Boolean = Build.FINGERPRINT == "robolectric" + /** + * Logs the reason the previous process instance exited (Android 10+/API 30) so abnormal + * terminations — low-memory kills, OEM/SIGKILL, ANRs, native crashes — become observable + * in crash reports. This is the foundation for crash-informed recovery (e.g. draining the + * durable signal buffer after an abnormal kill) and for respecting a user-requested + * force-stop (REASON_USER_REQUESTED) instead of auto-restarting tracking. + */ + @WorkerThread + private fun logPreviousExitReason() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return + try { + val activityManager = getSystemService(ActivityManager::class.java) ?: return + val exitInfo = activityManager + .getHistoricalProcessExitReasons(packageName, 0, 1) + .firstOrNull() ?: return + + val reasonLabel = when (exitInfo.reason) { + ApplicationExitInfo.REASON_LOW_MEMORY -> "LOW_MEMORY" + ApplicationExitInfo.REASON_SIGNALED -> "SIGNALED" + ApplicationExitInfo.REASON_CRASH -> "CRASH" + ApplicationExitInfo.REASON_CRASH_NATIVE -> "CRASH_NATIVE" + ApplicationExitInfo.REASON_ANR -> "ANR" + ApplicationExitInfo.REASON_USER_REQUESTED -> "USER_REQUESTED" + ApplicationExitInfo.REASON_USER_STOPPED -> "USER_STOPPED" + ApplicationExitInfo.REASON_OTHER -> "OTHER" + else -> "reason=${exitInfo.reason}" + } + + val isAbnormal = when (exitInfo.reason) { + ApplicationExitInfo.REASON_LOW_MEMORY, + ApplicationExitInfo.REASON_SIGNALED, + ApplicationExitInfo.REASON_CRASH, + ApplicationExitInfo.REASON_CRASH_NATIVE, + ApplicationExitInfo.REASON_ANR -> true + else -> false + } + + val message = "Previous process exit: $reasonLabel (status=${exitInfo.status})" + if (isAbnormal) Reporter.w("App", message) else Reporter.log(message) + } catch (e: RuntimeException) { + // Defensive: getHistoricalProcessExitReasons can throw on some OEM builds. + Reporter.report(e) + } + } + fun startDeferredStartupIfNeeded() { if (!deferredStartupStarted.compareAndSet(false, true)) return diff --git a/tracker/engine/src/main/java/com/adsamcik/tracker/tracker/service/TrackerService.kt b/tracker/engine/src/main/java/com/adsamcik/tracker/tracker/service/TrackerService.kt index 7b6687997..e530fb895 100644 --- a/tracker/engine/src/main/java/com/adsamcik/tracker/tracker/service/TrackerService.kt +++ b/tracker/engine/src/main/java/com/adsamcik/tracker/tracker/service/TrackerService.kt @@ -237,9 +237,12 @@ internal class TrackerService : CoreService(), TrackerTimerReceiver { } } - // User-initiated sessions should restart after process death to preserve tracking. - // Auto-tracking sessions can be re-triggered by ActivityWatcherService. - return if (isUserInitiated) START_STICKY else START_NOT_STICKY + // User-initiated sessions use START_REDELIVER_INTENT so that, after a system kill, + // Android re-delivers the ORIGINAL start intent (carrying ARG_IS_USER_INITIATED) + // instead of a null intent — letting the session resume with the correct flags + // rather than relying on the in-memory recovery fallback below. Auto-tracking + // sessions stay START_NOT_STICKY and are re-triggered by ActivityWatcherService. + return if (isUserInitiated) START_REDELIVER_INTENT else START_NOT_STICKY } private fun ensureForegroundStarted() {