Android Session Tracking — A Senior Engineer’s Perspective: When the OS Promises You Nothing
Source: Dev.to
Introduction
Android makes no promise about telling you when a user leaves your app.
This article is not about tricks; it is about thinking correctly about session tracking on Android—at production and SDK level.
Most Android developers have believed at least one of these at some point:
onStop()means the user leftonDestroy()means the app ended- Swiping from Recents ends a session
All of these are fundamentally wrong. Android does not run apps for you; your app is a guest. Processes can be killed without callbacks (SIGKILL / LMK) and offer zero cleanup guarantees. Lifecycle events only exist while the process is alive.
Implications
If you are waiting for a callback to detect session end, services such as Firebase, AdMob, Adjust, AppsFlyer do not “detect kills”. They:
- Track process foreground/background
- Apply timeout heuristics
- Infer session end on the next launch
No events, no guarantees—only probabilistic modeling. This is senior thinking: accept uncertainty and design around it.
Session Lifecycle
A real session has states:
- Created
- Active
- Background
- Expired
- Restored (inferred)
A session does not end when the app dies; we decide it is no longer valid based on:
- Background duration
- Next launch timing
- Persisted metadata
ProcessLifecycleOwner provides:
ON_START: process enters foregroundON_STOP: process goes to background
It does not give you:
- App kill
- Swipe from Recents
- Native crashes
And it shouldn’t. Senior engineers do not demand APIs do the impossible.
Timeout as a Business Rule
Timeout is not a hack; it is a business rule:
“If the user stays away longer than X ms, the session is considered ended.”
The exact value (30 seconds, 1 minute, 5 minutes…) depends on product requirements—there is no universally correct value.
When the app is killed:
- No
onSessionEndcallback - No cleanup, no flush
What remains:
- Last background timestamp
- Session ID
- Inferred reason
On the next launch, the SDK must:
- Load persisted state
- Infer the previous session ended
- Emit a logical session end
This two‑phase design is familiar to senior engineers.
Modeling Exit Reasons
Exit reasons should be modeled as:
USER_BACKGROUND_TIMEOUTPROCESS_KILLED_INFERREDAPP_UPGRADECRASH_DETECTED
The keyword is inferred. Good SDKs do not claim “the user did X”.
If session tracking:
- Depends on Activities
- Relies on system callbacks
- Depends on real time
→ it is not testable, and therefore not trustworthy.
A Proper SessionTracker
A proper SessionTracker:
- Is pure logic
- Injects its clock
- Accepts fake lifecycle signals
Testable means understandable.
SDK Design Goals
- Avoid Activity dependencies
- Never crash on process kill
- Never block the main thread
The SDK must be initialized in Application.onCreate().
// src/main/kotlin/com/example/App.kt
class App : Application() {
private lateinit var sessionObserver: AndroidSessionObserver
override fun onCreate() {
super.onCreate()
sessionObserver = AndroidSessionObserver(
context = this,
timeoutMs = 30_000L,
callback = object : SessionTracker.Callback {
override fun onSessionStart(session: Session) {
// analytics / ads init
}
override fun onSessionEnd(
session: Session,
reason: ExitReason
) {
// flush analytics / revenue sync
}
}
)
ProcessLifecycleOwner.get()
.lifecycle
.addObserver(sessionObserver)
}
}
Key points
- No Activity lifecycle usage
- No UI references
- Callbacks may never fire if the app is killed (the SDK is designed with this assumption)
Instead, the SDK:
- Persists session state on background (timestamp + session ID)
- On the next launch, restores previous state, infers the session ended, and emits
ExitReason.PROCESS_KILLED_INFERRED
Exit Reason Examples
USER_BACKGROUND_TIMEOUT→ time‑based evidencePROCESS_KILLED_INFERRED→ missing resume inference
Analytics and backend systems must treat these as inferred data. The SDK does not hide this.
Testable Logic
All session logic:
- Is framework‑independent
- Injects time
- Is fully testable
// src/test/kotlin/com/example/SessionTrackerTest.kt
@Test
fun `session ends after timeout`() {
val fakeClock = FakeClock()
val tracker = SessionTracker(fakeClock, timeoutMs = 30_000)
tracker.onForeground()
fakeClock.advance(31_000)
tracker.onForeground()
assertTrue(tracker.lastExitReason is ExitReason.Timeout)
}
Testable logic produces deterministic behavior.
Repository
The complete SDK—including pure SessionTracker logic, AndroidSessionObserver, ExitReason model, and unit tests—is available on GitHub:
🔗 https://github.com/vinhvox/ViO---Android-Session-Tracker
The repository is designed to:
- Read like documentation
- Be copy‑paste friendly
- Not pretend Android is controllable
Final Words for Senior Engineers
Session tracking on Android is never perfect. Good SDKs:
- Do not hide their limits
- State their constraints clearly
- Model uncertainty explicitly
- Help the business make correct—not just pretty—decisions
Android does not give you absolute truth, but it provides enough signals to infer—if you design correctly.