Android Session and App Lifecycle Management - scopetech/Mobile-Public GitHub Wiki

Android Session and App Lifecycle Management

Companion to the Android Consolidated SDK documentation and Android Simplified SDK Integration. Last updated: 9 Oct 2026 for Consolidated SDK 1.1.7 / SmartDrive 1.6.7.

This page explains how to keep a user logged in across screen-off, backgrounding and process death, and how to keep trip recording armed without forcing the user to log in again. If your users are being sent back to the login screen after locking the phone or reopening the app, this page is for you.

Important

The SDK already persists the session for you. PortalAPI Base stores the auth token after login and loads it again at BasePortalApi.init, so BasePortalApi.isLoggedIn() is true on the next launch without logging in again. You do not need to store tokens yourself, and you must not clear them on launch. A forced re-login after a screen-off is almost always caused by the app doing something explicit; Android alone does not require it.

Table of contents

1. The one principle

Three rules cover almost every case:

  1. At launch, check the session, don't re-create it. If isLoggedIn() is true, skip the login screen and go straight into the app.
  2. Initialize and start the SDK from Application.onCreate. It is the one place that runs on every process start, including when the system restarts the recording service with no screen showing. ScopeSDK.initialize restores the configuration the last start accepted and re-arms recording after a real termination before your app has fetched its settings; start then applies the settings you have, and equal settings change nothing. Both guard themselves on the host's isAuthorized(), so they are safe no-ops before the first login.
  3. Call logout() in exactly one place: the method your Logout action and PortalAPI's logout broadcast both reach. Never at launch, on resume, or when an Activity stops.

If those three hold, screen-off, backgrounding and process death all just work.

2. Two owners: session vs recording

Concern Owner Key calls
Session, who is logged in PortalAPI Base init(...), isLoggedIn(), standardAuth(...), logout(), the LOGOUT_BROADCAST it sends when a session expires
Recording, arming and stopping ScopeSDK initialize(...), start(...), authenticated(), permissionsChanged(), logout(), stop()

Session persistence is PortalAPI's job; arming is the facade's. The vehicle needs no owner in your app: PortalAPI announces the vehicle it selects and the SDK remembers it; an app without PortalAPI passes it to ScopeSDK.vehicleSelected once, on selection, and the SDK remembers that too. The rest of this page ties the two together in the right order.

3. Screen-off is not process death

Read this first; it is the single most common misunderstanding.

  • Screen off, or another app in front: your Activities pause and stop. The process, the session and the foreground recording service all stay. Nothing session-related should happen, and nothing should call stop().
  • Swiped away from recents: the Activities finish. The foreground recording service keeps running, because that is what a foreground service is for.
  • Process killed for memory: the recording service is sticky, so the system restarts it. That brings the process back, Application.onCreate runs, and ScopeSDK.initialize re-arms from the remembered configuration, the persisted session and the remembered vehicle, before your app has fetched anything. No screen, no login.
  • Reboot: the SDK's boot receiver runs in your process, so Application.onCreate runs and ScopeSDK.initialize brings recording back; the receiver itself only reschedules background work and leaves the recording service to your app class.
  • Force stop from Settings: ends everything until the user opens the app again. Nothing an app can do about it, on any platform.

Warning

If the app shows a login screen after a mere screen-off, the app is doing something explicit, almost certainly one of the mistakes in section 6. Android does not require a re-login on resume.

4. Lifecycle contract

Event Where Do Do not
Every process start, including a service restart Application.onCreate Plant the Timber tree, BasePortalApi.init, ScopeSDK.initialize, then ScopeSDK.start with the settings you have Re-create, reset or clear the session
Deciding login vs skip Launch screen Check isLoggedIn() first; refresh the user and go to the main screen if it is true Always show the login form
After a successful login Login success callback ScopeSDK.authenticated(), then the main screen Arm anything by hand
Permissions granted Permission-result callback ScopeSDK.permissionsChanged() once nothing is missing Assume a grant wakes the SDK by itself
Return to foreground onResume Re-check permissions, refresh the screen Log out or re-login
Background, screen off onPause, onStop Nothing session-related stop() or logout()
A permission revoked in Settings Next launch Nothing extra: the facade finds it missing and waits, your permission flow re-requests it —
User logs out, or the session expires One logout method BasePortalApi.logout(), ScopeSDK.logout(), then the login screen Call logout() anywhere else

5. Step-by-step implementation

Step 1. App launch

Application.onCreate is the only method guaranteed to run on every process start, including when the system restarts the recording service in the background. Plant the log tree, initialise PortalAPI, initialize the SDK, then start it with the settings you have.

// MyApp.kt
override fun onCreate() {
    super.onCreate()
    Timber.plant(FileLoggingTree(applicationContext, getDeviceDescription()))
    LocalBroadcastManager.getInstance(applicationContext)
        .registerReceiver(logoutReceiver, IntentFilter(Constants.LOGOUT_BROADCAST))

    BasePortalApi.init(applicationContext, PortalApiConfig(PrefUtil.serverURL, APP_NAME, VERSION_NAME), listOf())

    // Restores the configuration the last start accepted and arms recording from the PERSISTED
    // session: after a reboot or a service restart, recording resumes before anything else runs.
    SmartDriveHelper.initialize(applicationContext)   // ScopeSDK.initialize(context, host)
    // Applies the app's settings. Safe before the first login: the facade asks the host and
    // waits until isAuthorized() is true. Equal settings change nothing.
    SmartDriveHelper.start(applicationContext)        // ScopeSDK.start(context, configuration, host)

    if (PermissionsUtil.getNotGrantedPermissions().isEmpty())
        onAllPermissionsGranted()                 // a returning user: Wellness and BLE right away
}

Tip

After the process is killed and the system restarts the service, this method runs again; initialize sees the remembered configuration, the stored session and the remembered vehicle, and re-arms recording with no login screen and no settings fetch. If the app instead put these calls in an Activity, a restart with no screen would arm nothing.

Step 2. Decide login vs skip: do not force login

The launch screen asks "is there already a valid session?" and, if so, goes straight through.

// SplashActivity.kt
if (BasePortalApi.isLoggedIn()) {
    BasePortalApi.getUser().let { response ->          // refresh the user; PortalAPI announces the vehicle
        if (response.isSuccessful) startActivity(Intent(this, MainActivity::class.java))
        else showError(response)
    }
} else {
    startActivity(Intent(this, if (PrefUtil.isFirstRun) AppTypeActivity::class.java else LoginActivity::class.java))
}

Warning

This is the step most likely to be missing. If the app always shows the login form, or always calls logout or login on launch, the user is forced to re-authenticate even though a valid session exists.

Step 3. After a successful login

Order: login, then authenticated(), then the main screen. PortalAPI loads the user and selects a vehicle as part of the login; the SDK hears the selection on its own.

// LoginActivity.kt
val response = BasePortalApi.standardAuth(username, password)
if (response.isSuccessful) {
    SmartDriveHelper.authenticated()               // ScopeSDK.authenticated(): the session exists now
    startActivity(Intent(this, MainActivity::class.java))
    finish()
}

Step 4. Permissions

The SDK does not notice a grant by itself. Request the groups it names and tell it when nothing is missing.

// MainActivity.kt
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    if (PermissionsUtil.getNotGrantedPermissions().isEmpty())
        MyApp.instance.onAllPermissionsGranted()     // → ScopeSDK.permissionsChanged()
}

Step 5. Returning to the foreground

onResume refreshes. It re-checks permissions, because the user may have changed them in Settings, and redraws the status line. It never touches the session.

// MainActivity.kt
override fun onResume() {
    super.onResume()
    showSdkStatus()                                    // from ScopeSDK.getDiagnostics()
    if (checkPermissions()) fetchTrips()               // requests the next missing group, if any
}

Step 6. Going to background or screen off

Nothing. The Activities stop; the process, the session and the recording service continue. Do not call stop() or logout() from onStop.

Step 7. After the process is actually gone

When the system kills the process for memory, or restarts the sticky recording service, the next start runs Step 1 again. Because the configuration, the session and the vehicle were persisted, initialize re-arms and the login screen is skipped. There is nothing extra to implement, which is exactly why Step 1 must live in Application.onCreate.

Step 8. The only place to log out

// MyApp.kt
fun logout(reason: String?) {
    Timber.i("Logout! $reason")
    BasePortalApi.logout()
    SmartDriveHelper.logout()                          // ScopeSDK.logout(): stops recording, clears vehicle and local trips
    WellnessHelper.logout(applicationContext)
    if (PrefUtil.appType == AppType.BLE) BleHelper.logout()
    startActivity(Intent(applicationContext, LoginActivity::class.java)
        .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK))
}

private val logoutReceiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        logout(intent.getStringExtra(Constants.LOGOUT_REASON))   // PortalAPI: session expired or server forced it
    }
}

One method. PortalAPI's logout broadcast reaches it when the session expires; a Logout button in your app calls the same method. It is never called on launch, on resume, or when an Activity stops.

6. Common mistakes that force a re-login or lose recording

  • ❌ Calling BasePortalApi.logout() or ScopeSDK.logout() on launch or in onResume. Wipes the stored session and the local trips.
  • ❌ Always showing the login screen instead of checking isLoggedIn() first.
  • ❌ Calling ScopeSDK.initialize and start from an Activity. A service restart with no screen never re-arms.
  • ❌ Calling start but never initialize. It works until a reboot or a service restart; recording then waits for your settings fetch and your start.
  • ❌ Calling ScopeSDK.stop() from onStop. Screen-off then ends recording.
  • ❌ Granting a permission and never calling permissionsChanged(). Recording waits until the next check.
  • ❌ Arming only from the login screen, so a returning user with a persisted session records nothing until they log in again.
  • ✅ Correct: persist nothing yourself, check isLoggedIn() at launch, initialize and start in Application.onCreate, authenticated() after login, permissionsChanged() after the permission flow, logout() from one method only.

7. How to verify

  1. Log in normally and confirm a trip records.
  2. Turn the screen off, wait, turn it back on. The app resumes without a login screen.
  3. Swipe the app away from recents, reopen it. The login screen is skipped; the status line reads "ready to record".
  4. Drive with the app killed. A trip is still recorded: the sticky service brought the process back and Step 1 re-armed it.
  5. Reboot the phone while logged in and drive without opening the app. A trip is still recorded: the boot receiver started the process and initialize re-armed recording from the remembered configuration.
  6. Revoke location in Settings, reopen the app. The status line reads "waiting for" the permission; grant it, and recording resumes without a login.
  7. Log out. The next launch shows the login screen.

If step 2 or 3 shows a login screen, review Step 2 and the section 6 list: the app is clearing or ignoring the persisted session.

8. Reference implementation

The SmartDriveDemo repository implements this pattern: MyApp.kt for Steps 1 and 8, SplashActivity.kt for Step 2, LoginActivity.kt for Step 3, and MainActivity.kt for Steps 4 and 5.

⚠️ **GitHub.com Fallback** ⚠️