Android makes no promise about telling you when a user leaves your app.
Yet analytics, ads, and business logic are forced to assume such a thing exists.This article is not about tricks. It is about thinking correctly about session tracking on Android — at production and SDK level.
1. The real problem is not APIs — it’s illusion
Most Android developers have believed at least one of these at some point:
-
onStop()means the user left -
onDestroy()means the app ended - Swiping from Recents ends a session
All of these are fundamentally wrong.
Android does not run apps for you.
Android runs itself. Your app is a guest.
2. OS truths senior engineers must accept
- Processes can be killed without callbacks
- SIGKILL / LMK offer zero cleanup guarantees
- Lifecycle events only exist while the process is alive
In other words:
If you are waiting for a callback to detect session end,
you already lost — conceptually.
3. How real SDKs actually work
Firebase, AdMob, Adjust, AppsFlyer do not “detect kills”.
They do three things:
- 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.
4. A session is not an event — it’s a state machine
A real session has states:
- Created
- Active
- Background
- Expired
- Restored (inferred)
A session does not end when the app dies.
It ends when we decide it is no longer valid.
That decision is based on:
- Background duration
- Next launch timing
- Persisted metadata
5. ProcessLifecycleOwner: the right tool, not a magic one
ProcessLifecycleOwner gives you:
-
ON_START: process enters foreground -
ON_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.
6. Timeout is not a workaround — it’s a definition
Timeout is not a hack.
Timeout is a business rule:
“If the user stays away longer than X ms, the session is considered ended.”
30 seconds? 1 minute? 5 minutes?
There is no correct value — only product-appropriate ones.
7. App kill: no callback, only traces
When the app is killed:
- No
onSessionEnd - 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 is a two-phase design — familiar to senior engineers.
8. Exit reasons: be honest, not confident
Exit reasons should be modeled as:
- USER_BACKGROUND_TIMEOUT
- PROCESS_KILLED_INFERRED
- APP_UPGRADE
- CRASH_DETECTED
The key word is inferred.
Good SDKs do not say “the user did X”.
They say “based on evidence, we infer X”.
9. Why this module is testable — and why that matters
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:
- is pure logic
- injects its clock
- fakes lifecycle signals
Testable means understandable.
Understandable means reliable.
10. How to use the Session Tracking SDK
The theory matters. Now the practice.
This SDK is designed to:
- avoid Activity dependencies
- never crash on process kill
- never block the main thread
10.1 Initialize in Application
The SDK must be initialized in Application.onCreate().
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.
10.2 What happens when the app is killed?
No callback is invoked.
Instead, the SDK:
- persists session state on background
- stores timestamp and session ID
On the next launch, it:
- restores previous state
- infers the session ended
- emits
ExitReason.PROCESS_KILLED_INFERRED
This behavior is intentional.
10.3 Interpreting ExitReason correctly
Exit reasons are not absolute truth.
Examples:
-
USER_BACKGROUND_TIMEOUT→ time-based evidence -
PROCESS_KILLED_INFERRED→ missing resume inference
Analytics and backend systems must treat these as inferred data.
The SDK does not hide this.
10.4 Unit testing: why you should trust this SDK
All session logic:
- is framework-independent
- injects time
- is fully testable
Example:
@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.
11. Full source code
The complete SDK, including:
- Pure SessionTracker logic
- AndroidSessionObserver
- ExitReason model
- Unit tests
is available on GitHub:
🔗 https://github.com/vinhvox/ViO---Android-Session-Tracker
This repository is designed to:
- read like documentation
- be copy-paste friendly
- and, most importantly, not pretend Android is controllable
12. Final words for senior engineers
Session tracking on Android is never perfect.
Good SDKs do not hide that.
They:
- state their limits clearly
- model uncertainty explicitly
- help the business make correct, not just pretty, decisions
Android does not give you absolute truth.
But it gives enough signals to infer — if you design correctly.
Top comments (0)