Android Session and App Lifecycle Management - scopetech/Mobile-Public GitHub Wiki
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.
- 1. The one principle
- 2. Two owners: session vs recording
- 3. Screen-off is not process death
- 4. Lifecycle contract
- 5. Step-by-step implementation
- 6. Common mistakes that force a re-login or lose recording
- 7. How to verify
- 8. Reference implementation
Three rules cover almost every case:
-
At launch, check the session, don't re-create it. If
isLoggedIn()istrue, skip the login screen and go straight into the app. -
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.initializerestores the configuration the laststartaccepted and re-arms recording after a real termination before your app has fetched its settings;startthen applies the settings you have, and equal settings change nothing. Both guard themselves on the host'sisAuthorized(), so they are safe no-ops before the first login. -
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.
| 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.
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.onCreateruns, andScopeSDK.initializere-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.onCreateruns andScopeSDK.initializebrings 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.
| 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 |
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.
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.
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()
}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()
}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
}Nothing. The Activities stop; the process, the session and the recording service continue. Do not call stop() or logout() from onStop.
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.
// 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.
- ❌ Calling
BasePortalApi.logout()orScopeSDK.logout()on launch or inonResume. Wipes the stored session and the local trips. - ❌ Always showing the login screen instead of checking
isLoggedIn()first. - ❌ Calling
ScopeSDK.initializeandstartfrom an Activity. A service restart with no screen never re-arms. - ❌ Calling
startbut neverinitialize. It works until a reboot or a service restart; recording then waits for your settings fetch and yourstart. - ❌ Calling
ScopeSDK.stop()fromonStop. 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,initializeandstartinApplication.onCreate,authenticated()after login,permissionsChanged()after the permission flow,logout()from one method only.
- Log in normally and confirm a trip records.
- Turn the screen off, wait, turn it back on. The app resumes without a login screen.
- Swipe the app away from recents, reopen it. The login screen is skipped; the status line reads "ready to record".
- Drive with the app killed. A trip is still recorded: the sticky service brought the process back and Step 1 re-armed it.
- Reboot the phone while logged in and drive without opening the app. A trip is still recorded: the boot receiver started the process and
initializere-armed recording from the remembered configuration. - Revoke location in Settings, reopen the app. The status line reads "waiting for" the permission; grant it, and recording resumes without a login.
- 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.
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.