Android Simplified SDK Integration - scopetech/Mobile-Public GitHub Wiki
Android Simplified SDK Integration — ScopeSDK & AppScopeHost
For teams integrating SmartDrive trip recording on Android. Companion pages: Android Consolidated SDK documentation, SmartDriveDemo Android — App Overview, Android Diagnostics and Health Snapshot. Last updated: 12 Oct 2026 for Consolidated SDK 1.1.9 / SmartDrive 1.6.9.
The simplified integration is built from two files you write. The working reference is the SmartDriveDemo repository.
| File | Role | Portability |
|---|---|---|
AppScopeHost.kt |
The one question the SDK asks the app: is a user logged in. | App-specific, and usually one line. |
AppScopeConfiguration.kt |
The values the SDK cannot guess, and whichever engine settings your app changes from the defaults. | App-specific. |
A second branch of the demo, individual_sdks, is the same app on the five SDKs as separate dependencies, integrating at TripWrapper instead of ScopeSDK; see Android TripWrapper SDK. Nothing is copied from the demo verbatim. On iOS the ScopeSDK facade is a source file each app carries; on Android it ships inside the artifact, so there is no third file and nothing to keep byte-identical between apps.
What the design retires: the app-side helper that wrote engine preferences in a particular order, seeded the vehicle serial number, started the recording service, registered broadcast receivers and drove trip sending. The demo's helper went from 492 lines to 150, and what is left turns the SDK's callbacks into LiveData for its screens. The direct manager API remains fully supported; this page describes the recommended style going forward.
1. The principles
- The host answers, the app never pushes. The SDK asks
isAuthorized()whenever something may have changed. Your app keeps no SDK state in sync; it answers from what it already knows. - Values, not mechanics. Your configuration says what you want: mode, names, which features are on, which thresholds. Setter order, preference keys, the recording service and the transport are the SDK's business. A configuration that cannot work is refused at
startwith an exception naming the field. - The SDK decides when to record. Recording arms once three things are true: a session exists, a vehicle is known, and every required permission is granted.
initialize,start,authenticatedandpermissionsChangedeach re-check, so the order in which your app reaches them does not matter.initializealso restores the configuration the laststartaccepted, so recording resumes as soon as the process starts, before your app has fetched its settings again. - One list of permissions.
ScopeSDK.requiredPermissions(configuration)is both what you request and what the SDK gates on. There is no second list to keep in step.
2. Integration, step by step
Step 1 — add the dependency as in the hub page: one line, implementation 'com.scopetechnology:sdk:1.1.9', with the GitHub Packages repository configured and compileSdk 36 or later, which the SDKs require since 1.1.6.
Step 2 — write AppScopeHost.kt:
class AppScopeHost : ScopeHost {
override fun isAuthorized(): Boolean = BasePortalApi.isLoggedIn()
}
Two more questions have defaults you leave alone unless you have a reason:
| Question | Default | Override when |
|---|---|---|
deviceID() |
null, meaning the vehicle PortalAPI announced | Your app picks the vehicle by a rule PortalAPI does not know. Then call ScopeSDK.vehicleChanged() when your answer changes. |
onBeaconTripStart() |
no-op | You want to snapshot the beacon in range when a trip starts. |
An app without PortalAPI leaves deviceID() alone and passes the vehicle it selects, with its unit type, to ScopeSDK.vehicleSelected(unitSerialNumber, unitType); the SDK remembers both across restarts, and the unit type decides how the vehicle records, which is how MHub units recorded by the phone are told apart. The unit type is not a host question: it is known at the moment the vehicle is selected.
Step 3 — write AppScopeConfiguration.kt:
object AppScopeConfiguration {
fun build(context: Context) = ScopeConfiguration(
mode = ScopeConfiguration.Mode.SMART, // or BEACON: record only while the vehicle's beacon is in range
appName = context.getString(R.string.app_name),// on the recording notification
appNameAbbreviation = BuildConfig.APP_NAME, // short prefix for logs and file names
notificationActivity = MainActivity::class.java,
smartDrive = SmartDriveConfiguration(
resendFailedTrips = true, // retry a failed upload; off by default
distractedDrivingEnabled = true,
distractedDrivingPhoneCallEnabled = false, // phone use only; call detection would need READ_PHONE_STATE
distractionMinSpeedKmh = 5,
),
)
}
Four values the SDK cannot guess, then only the engine settings you change. Every field is documented on ScopeConfiguration and SmartDriveConfiguration; turning a feature on is one named argument. The defaults that matter most: trips are sent when they end, the trip-end timer is 5 minutes, the minimum trip is 102 metres, finished trips stay on the phone for 30 days, and distraction, mode-of-transport checks and accident detection are off. Real-time sending to MProfiler is realtimeSending = true plus the MProfiler settings Scope support gives you. Java callers use ScopeConfiguration.Builder and SmartDriveConfiguration.Builder.
Step 4 — wire the five calls, as the demo's MyApp.kt and LoginActivity.kt do:
// Application.onCreate, after PortalAPI is initialised, before login and before any permission dialog:
Timber.plant(FileLoggingTree(this)) // the SDK logs through Timber
val host = AppScopeHost()
ScopeSDK.initialize(applicationContext, host) // in every process: restores the last accepted configuration
ScopeSDK.start(applicationContext, AppScopeConfiguration.build(this), host) // whenever the settings are known or change
// After a successful login (safe to repeat):
ScopeSDK.authenticated()
// Once every permission in ScopeSDK.requiredPermissions(configuration) is granted:
ScopeSDK.permissionsChanged()
// On logout, the only place:
ScopeSDK.logout()
Step 5 — request the permissions the SDK names. PermissionsUtil.kt filters a fixed request order through ScopeSDK.requiredPermissions(configuration) and returns the groups still to ask for: location first, background location in a dialog of its own, then activity recognition, Bluetooth, phone state and notifications. The main screen requests the next missing group on resume and calls permissionsChanged() when nothing is missing.
Step 6 — receive trips. The SDK reports on the main thread through four callbacks. The demo's SmartDriveHelper.kt sets them once and turns them into LiveData its screens observe:
ScopeSDK.onTripStarted = { trip -> } // TripRecord
ScopeSDK.onTripUpdated = { distanceMeters, durationSeconds, speedKmh -> }
ScopeSDK.onTripEnded = { trip -> }
ScopeSDK.onTripSent = { trip, error -> } // error is null when delivered
Java hosts implement TripListener instead. Compose screens collect ScopeSDK.tripState and ScopeSDK.tripCounts; the counts follow the SDK's own trip deletions and logout too, so no app-side re-read is needed. A trip already recording when the app restarts arrives through onTripStarted.
start applies the settings your app has and remembers them; a call with settings equal to the ones in effect changes nothing, so calling it on every launch and on every settings change is the intended use. initialize brings the remembered settings back at the next process start, which is what lets the SDK record after a reboot or a service restart before your app has run its own startup.
3. What the facade guarantees for you
- Ordering is encoded once. Configuration is applied to the engine before the recording service starts, and the event source is attached after it. No app re-derives this.
- Recording waits for what it needs. No session, no vehicle, or a missing permission each leave recording disarmed, logged by name, until the next check finds them satisfied.
- The vehicle arrives on its own. PortalAPI announces the vehicle it selects, at login and on every change; the SDK hears it and remembers it across restarts. An app on PortalAPI writes no vehicle code.
- Permissions cannot desync. The request list and the recording gate read the same function.
- Recording survives a process restart.
initializerestores the configurationstartlast accepted and arms at once; a sticky service restart or a reboot does not wait for your settings fetch.logoutforgets the configuration, so a logged-out device records nothing. - Impossible configurations fail at launch, not at trip end: real-time sending without MProfiler settings, MHub tracking without real-time sending, a manual-sending delay combined with real-time sending, or a field out of range, each refused by name.
- The processing count agrees with the Portal. An uploaded trip counts as delivered in
tripCountsuntil the SDK sees it among the Portal's trips.ScopeSDK.confirmProcessingTrips { fetchTrips() }runs that check before your app shows a fetched list, and the SDK runs it on its own when the app returns to the foreground while such trips exist (SmartDrive 1.6.8). - Health at a glance.
ScopeSDK.isHealthy()wrapsgetDiagnostics(), and the SDK writes aTRIPWRAPPER HEALTH SNAPSHOTblock to the log at start, trip start and trip end, so an integration mistake shows in the first screen of a support log. See Android Diagnostics and Health Snapshot.
4. Pitfalls
- No Timber tree. Without one the SDK is silent and you will not see why recording waits.
getMissingProtocols()reports it. - Forgetting
permissionsChanged(). Granting a permission does not wake the SDK by itself. Call it from the permission-result callback, or from the place where you know everything is granted. - Skipping
initialize.startalone works, but after a reboot or a sticky service restart nothing records until your app has fetched its settings and calledstartagain. CallinitializefromApplication.onCreatein every process. - Starting with a setting a later screen changes.
startapplies the settings as they are at that moment, andrequiredPermissions(configuration)answers for whatever configuration you pass it. If a setting is only known later, such as a recording mode chosen on a first-run screen, callstartagain when it is chosen, as the demo'sAppTypeActivitydoes. Otherwise the SDK gates recording on the permissions of the mode it was started with while your app requests the permissions of the mode it now builds, and recording never arms. - Overriding
deviceID()when PortalAPI already answers. Two sources of truth for the vehicle. Override it only when your app picks the vehicle by a rule PortalAPI does not know; an app without PortalAPI usesvehicleSelectedinstead. - Turning on phone-call detection without the permission.
distractedDrivingPhoneCallEnabledaddsREAD_PHONE_STATEto the required list, and your manifest must declare it. Phone-use detection needs neither. - Leaving retry off in a field-test app.
resendFailedTripsis off by default, and a trip that fails to upload is then never retried automatically. Turn it on, or offer a button that callsScopeSDK.retryFailedTrips(), which sends failed trips regardless of that setting since SmartDrive 1.6.9. - Expecting a desk-bound test trip to upload. Trips shorter than 102 metres are discarded at trip end.
- Configuring the manager directly for something the configuration covers. The facade re-applies its configuration, so a direct write can be overwritten. Anything without a typed field goes in the
configureSmartDriveclosure insideSmartDriveConfiguration, which runs after every field. - Using
stop()wherelogout()is meant.stop()pauses recording and keeps local trips;logout()also clears the vehicle and the trips.
5. Differences from iOS
Teams integrating both platforms will notice the Android host asks less. Each difference removes work from the client.
| iOS | Android | Why |
|---|---|---|
| Host implements a logging protocol | Plant a Timber tree | Every Android app already has one. |
| Host implements a notification delegate | The SDK posts its own notifications, behind typed switches | The recording notification is a platform requirement on Android. |
| Host uploads trip batches through the app's API layer | The SDK owns its transport and retry rules | The app cannot get them wrong. |
Host answers username() |
Removed | The Android engine never read it. |
| App relays the selected vehicle | The SDK hears PortalAPI's announcement | PortalAPI already publishes it on the shared bus. |
stopSmartDrive closure on the configuration |
Removed | The facade's own stop() disarms the engine. |
configureSmartDrive on the top-level configuration |
Inside SmartDriveConfiguration |
Every engine setting in one object. |
| MHub as a recording mode | A tenant switch plus the vehicle's unit type | That is how the data arrives on Android. |
Two engine defaults also differ: phone-call distraction detection is on by default on Android and off on iOS, and the distraction minimum speed defaults to zero on Android and five on iOS. The demo names both explicitly so the two demos behave the same.