Android Session Tracking — A Senior Engineer’s Perspective: When the OS Promises You Nothing

Published: (December 30, 2025 at 10:09 AM EST)
3 min read
Source: Dev.to

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 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; 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:

  1. Track process foreground/background
  2. Apply timeout heuristics
  3. 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 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.

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 onSessionEnd callback
  • No cleanup, no flush

What remains:

  • Last background timestamp
  • Session ID
  • Inferred reason

On the next launch, the SDK must:

  1. Load persisted state
  2. Infer the previous session ended
  3. 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_TIMEOUT
  • PROCESS_KILLED_INFERRED
  • APP_UPGRADE
  • CRASH_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:

  1. Persists session state on background (timestamp + session ID)
  2. 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 evidence
  • PROCESS_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.

Back to Blog

Related posts

Read more »

How to Kill a Running Process in Linux

Have you ever felt like your Linux system suddenly stopped listening to you? You click, you type, you wait… and nothing happens. Often this is because a process...