Repository navigation
Android TripWrapper SDK
The SmartDrive-level facade, com.scope.mhub.TripWrapper, for apps that integrate the SmartDrive SDK on its own. Apps on the consolidated SDK use the same calls and types through ScopeSDK; see Android Simplified SDK Integration. Last updated: 12 Oct 2026 for SmartDrive 1.6.9.
TripWrapper is the integration layer between your app and the SmartDrive recording engine. It owns the order of operations, decides when recording may run, applies your configuration to the engine, and delivers trip events, so your code supplies values and receives results.
- Arms and disarms the recording service when a session, a vehicle and the required permissions are all present.
- Applies a typed, validated configuration to the engine, in the order the engine needs.
- Remembers the vehicle PortalAPI announces, or the one a host without PortalAPI passes to
vehicleSelectedwith its unit type, or asks the host for one. - Remembers the configuration
startaccepted and restores it at the next process start, so recording resumes after a reboot or a service restart before the app has its settings again. - Delivers trip start, update, end and upload results on the main thread.
- Reports diagnostics, a health verdict, and a health snapshot in the log.
- Lists the runtime permissions your configuration needs.
Your app ─ host (one question) + configuration ─► TripWrapper ─► SCPSmartDriveManager / RecordTripService
◄─ callbacks, tripState, tripCounts, diagnostics ─┘
TripWrapper is the only class in the root package com.scope.mhub; the types a host handles live in com.scope.mhub.tripwrapper. It is additive: the direct manager API keeps working, and the configureSmartDrive closure lets you reach it for anything without a typed field.
Rule: once you integrate through TripWrapper, do not also start or stop the recording service, write engine preferences, or select the vehicle through
SCPSmartDriveManagerdirectly. The facade re-applies its configuration and would overwrite you. Anything without a typed field goes inconfigureSmartDrive, which runs after every field.
-
minSdk 24;compileSdk 36or later since 1.6.6, which the artifact's AAR metadata requires. Kotlin or Java; the host interface has default methods, so a Java host implements one method. - Runtime permissions, from
TripWrapper.requiredPermissions(configuration):
| Permission | When |
|---|---|
ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION
|
always; Android 12 requires both in one request |
ACCESS_BACKGROUND_LOCATION |
Android 10 and later |
ACTIVITY_RECOGNITION |
Android 10 and later, unless recording is beacon gated |
POST_NOTIFICATIONS |
Android 13 and later |
READ_PHONE_STATE |
distractedDrivingEnabled together with distractedDrivingPhoneCallEnabled, or speakerOffAfterCall
|
BLUETOOTH_SCAN, BLUETOOTH_CONNECT
|
Android 12 and later, when recording is beacon gated |
- A Timber tree. The SDK logs through Timber and is silent without one.
The SmartDriveDemo individual_sdks branch is this integration in working form: AppTripWrapperHost.kt, AppTripWrapperConfiguration.kt and SmartDriveHelper.kt, with the five SDKs as individual dependencies. Its master branch is the same app through the consolidated SDK.
Step 1 — write the host
class AppHost : TripWrapperHost {
override fun isAuthorized(): Boolean = /* a user session exists */
}Step 2 — initialize and start in Application.onCreate
val host = AppHost()
TripWrapper.initialize(applicationContext, host) // in every process: restores the last accepted configuration
val configuration = TripWrapperConfiguration(
mode = TripWrapperConfiguration.Mode.SMART, // or BEACON
appName = getString(R.string.app_name),
appNameAbbreviation = "APP",
notificationActivity = MainActivity::class.java,
smartDrive = SmartDriveConfiguration(resendFailedTrips = true),
)
TripWrapper.start(applicationContext, configuration, host) // whenever the settings are known or changeThen TripWrapper.authenticated() after login, TripWrapper.permissionsChanged() once every required permission is granted, and TripWrapper.logout() on logout. Any order. An app without PortalAPI also calls TripWrapper.vehicleSelected(unitSerialNumber, unitType) when the user picks a vehicle.
Step 3 — handle trip events
TripWrapper.onTripStarted = { trip -> }
TripWrapper.onTripUpdated = { distanceMeters, durationSeconds, speedKmh -> }
TripWrapper.onTripEnded = { trip -> }
TripWrapper.onTripSent = { trip, error -> } // error is null when deliveredTripWrapperHost is the one interface your app implements. The SDK asks it whenever something may have changed.
| Question | Required | Meaning |
|---|---|---|
isAuthorized(): Boolean |
yes | A user session exists. Recording is armed only while this is true. |
deviceID(): String? |
no, default null
|
Unit serial number of the vehicle trips are recorded for. Null means the vehicle PortalAPI announced when it selected one, or the one passed to vehicleSelected; the facade remembers it across restarts. A non-null answer wins over both. Call vehicleChanged() when your answer changes. |
onBeaconTripStart() |
no, default no-op | Called when a beacon starts a trip; hosts with BLE can snapshot the beacon in range. |
The selected vehicle's unit type is not a host question. A host without PortalAPI passes it with the vehicle to vehicleSelected(unitSerialNumber, unitType), and the facade remembers it next to the serial, across restarts, so the recording decision below survives a restart like the serial does. selectedVehicleUnitType() reads it back for the app's own screens. PortalAPI's announcement carries no type and leaves the decision to mode.
Who overrides deviceID(). Only an app on PortalAPI whose vehicle rule differs from PortalAPI's. PortalAPI announces only SmartDrive vehicles whose portal permissions are all granted. An app without PortalAPI leaves deviceID() alone and passes the vehicle it selects, with its unit type, to vehicleSelected, which stores both the way an announcement is stored; the call is ignored before initialize or start, one reason to call initialize from Application.onCreate. Leave deviceID() alone when the vehicle is announced or selected; override it when your app picks it by a rule of its own while PortalAPI announces another.
Recording decision, from the unit type given to vehicleSelected and smartDrive.trackMHubVehicles:
| Unit type | trackMHubVehicles |
Result |
|---|---|---|
MHUB |
true | Distraction-only trips, started from activity recognition |
MHUB |
false | No recording service |
VIRTUAL_BLE |
any | Normal recording, beacon gated |
VIRTUAL_SMART, UNKNOWN
|
any | Normal recording |
| none given | any | Normal recording; gated when mode is BEACON
|
An MHub records the trip itself and the phone only adds the distraction events, so its trips are not gated on the unit's beacon and need no Bluetooth contact with it. Beacon gating applies to VIRTUAL_BLE vehicles, and to every vehicle in BEACON mode when no unit type is given. A change of gating at runtime, a switch between a beacon-gated vehicle and a sensor-only one, reaches the running recording service without a restart.
Logging and notifications are not part of the host: the SDK logs through Timber and posts its own notifications behind typed switches.
| Type | Fields |
|---|---|
TripRecord |
tripId, portalId, startDate, endDate (null while recording), startLocation, endLocation, distanceMeters, durationSeconds, maxSpeedKmh, status, unitSerialNumber, distractionOnly, events (a list of TripRecord.Event) |
TripStatus |
RECORDING, READY_TO_SEND, PENDING, SENDING, DELIVERED, FAILED
|
TripCounts |
local, unsent, failed, delivered; unsent and failed count finished trips only |
TripState |
Idle; Recording(trip, distanceMeters, durationSeconds, speedKmh); Ended(trip)
|
VehicleUnitType |
MHUB, VIRTUAL_BLE, VIRTUAL_SMART, UNKNOWN
|
TripWrapperDiagnostics |
See Android Diagnostics and Health Snapshot |
Units are in the names: metres, seconds, km/h.
Every member of TripWrapper is static (@JvmStatic).
| Call | Effect |
|---|---|
initialize(context, host) |
Call from Application.onCreate, in every process. Restores the configuration the last start accepted, unless the user logged out since, and arms recording at once; with none remembered it waits for start. The restored configuration has no configureSmartDrive closure; the next start brings it back. |
start(context, configuration, host) |
Applies the configuration and remembers it for initialize. Call whenever the settings are known or change, before or after login; a configuration equal to the one in effect changes nothing. Refuses a configuration that cannot work with an IllegalArgumentException naming the field. Schedules the SDK's daily trip-retention sweep for smartDrive.tripRetentionDays. Works without initialize, then without the restore. |
authenticated() |
Re-checks the session and arms recording if everything else is present. Safe to repeat. |
permissionsChanged() |
Re-checks the required permissions. Call from the permission-result callback. |
vehicleChanged() |
For hosts that answer deviceID() themselves: re-reads it. Hosts that leave the vehicle to PortalAPI never need it. |
vehicleSelected(unitSerialNumber, unitType = null) |
For hosts without PortalAPI: the app selected this vehicle. Remembered with its unit type, recording re-evaluated. A host that answers deviceID() still wins for the serial. Ignored before initialize or start. |
selectedVehicleUnitType() |
The unit type remembered with the selected vehicle, or null when none was given. |
stop() |
Stops recording, keeps configuration and local trips. authenticated() arms again. |
logout() |
Stops recording and clears the vehicle selection, the remembered configuration and local trips. |
isInitialized |
True while a configuration is in effect: accepted by start, or restored by initialize. |
| Member | Delivery |
|---|---|
onTripStarted: (TripRecord) -> Unit |
Main thread, when a trip starts, including a trip already recording when the facade arms after an app restart. |
onTripUpdated: (distanceMeters, durationSeconds, speedKmh) -> Unit |
Main thread, on every position while recording. |
onTripEnded: (TripRecord) -> Unit |
Main thread, when a trip stops recording. |
onTripSent: (TripRecord, error: String?) -> Unit |
Main thread, when an upload finishes; error is the failure name or null. tripCounts is refreshed first. |
listener: TripListener? |
Java-friendly alternative to the four lambdas. |
tripState: StateFlow<TripState> |
For Compose, or Views through asLiveData(). |
tripCounts: StateFlow<TripCounts> |
For a Pending trips screen. Finished trips only; the trip being recorded is in no count. Refreshed at trip end, after every upload result, when the SDK deletes trips on its own (backend confirmation, retention, mode-of-transport reject) or changes a status outside an upload, and set to TripCounts.NONE on logout; before 1.6.7 only the first two applied. |
| Call | Returns |
|---|---|
getCurrentTrip() |
The trip being recorded, or null. |
getLocalTrips() |
Every trip stored on the phone. |
getTargetTrips() |
Trips not yet delivered, failed ones included. |
retryFailedTrips() |
Sends unsent trips now, honouring the mode-of-transport settings. A manual send ignores the automatic-retry settings: it also sends a trip quarantined after repeated backend rejects and, with resendFailedTrips off, the failed trips the SDK leaves for the user (since 1.6.9; until then the call ran the SDK's own eligibility check and sent nothing in those cases). The SDK also retries on its own when resendFailedTrips is on. |
confirmProcessingTrips(onDone = null) |
Runs the processing-confirmation sweep now and calls onDone on the main thread when it is over, tripCounts refreshed first: uploaded trips the Portal already lists leave the delivered count. Call it before showing trips fetched from the Portal, confirmProcessingTrips { fetchTrips() }. The SDK also sweeps ten minutes after each upload, every eight hours, and on every return to the foreground while such trips exist; a sweep within two minutes of the previous one is skipped. Since 1.6.8. |
| Call | Returns |
|---|---|
getDiagnostics() |
A TripWrapperDiagnostics picture of the integration. |
isHealthy() |
Its verdict. |
getMissingProtocols() |
Integration gaps that stay silent at runtime: no session after authenticated, no vehicle once there is a session, no Timber tree. |
logHealthSnapshot(trigger = MANUAL) |
Writes the snapshot block to the log, as the SDK does itself at start, trip start and trip end. |
requiredPermissions(configuration) |
The runtime permissions this configuration needs on this device. |
TripWrapperConfiguration(mode, appName, appNameAbbreviation, notificationActivity, realtimeSending = false, autoSend = true, realtime = null, smartDrive = SmartDriveConfiguration()). Java uses TripWrapperConfiguration.Builder. Configurations compare by value, so one built again with the same settings is no change for start; a configureSmartDrive closure compares by instance.
SmartDriveConfiguration holds every engine setting, with the SDK's own defaults, so an empty one changes nothing:
| Group | Fields |
|---|---|
| Trip end |
autostopThresholdSeconds (300), minTripDistanceMeters (102), locationAccuracy (320 m) |
| Sending |
resendFailedTrips (false), beaconTripAutoSend (null: follow autoSend), pendingTripsManualSendingHoursDelay (0) |
| Retention |
tripRetentionDays (30): days a finished trip stays on the phone before the daily sweep deletes it; unsent trips get twice that, never past the Portal's 30-day processing horizon. null keeps trips |
| Notifications |
showNotificationForValidTripOnly (false), showDistractedDriverNotification (false), showOldTripReminders (true) |
| Distraction |
distractedDrivingEnabled (false), distractedDrivingPhoneUseEnabled (true), distractedDrivingPhoneCallEnabled (true), distractionMinSpeedKmh (0), distractionSensor
|
| Mode of transport |
motEnabled (false), motSkipTypes, motElevationKey, motStaticMapsKey, motChecks
|
| Accident |
accidentDetectionEnabled (false), accidentValidation (required when enabled), positionAfterIncidentDurationSeconds (300), positionAfterIncidentIntervalSeconds (20), speakerOffAfterCall (false) |
| Driving behaviour |
drivingBehaviour (0.29 g, 0.31 g, 0.25 g, all detectors on, 1000 ms) |
| MHub |
trackMHubVehicles (false; requires realtimeSending) |
| Escape hatch |
configureSmartDrive: ((SCPSmartDriveManager) -> Unit)?, runs after every field |
Refused at start: realtimeSending without realtime, trackMHubVehicles without realtimeSending, pendingTripsManualSendingHoursDelay combined with realtimeSending, motEnabled without motStaticMapsKey, accidentDetectionEnabled without accidentValidation, and any field out of its range.
The facade owns the engine. Delete the code that started and stopped the recording service, wrote preferences and selected the vehicle, and express the same intent as configuration.
- Replace your configure routine with a
TripWrapperConfiguration; the mapping table below says where each setting went. - Write a host that answers
isAuthorized(). Delete the code that seeded the vehicle serial number: an app on PortalAPI gets the vehicle announced, an app without PortalAPI passes it tovehicleSelectedwith its unit type, and only an app that picks the vehicle by a rule PortalAPI does not know makes that ruledeviceID(). - Replace start and stop calls with
initializeinApplication.onCreate,startwhere your settings are known,authenticatedafter login andlogout, and addpermissionsChanged()where your permission flow finishes. - Replace broadcast receivers and helper LiveData with the four callbacks.
- Run once and read the health snapshot in the log; it names anything still missing.
The full walkthrough is the Android TripWrapper Migration Guide.
| Direct manager use | Through TripWrapper |
|---|---|
setAppName, setAppNameAbbreviation, setTripActivity, recordTripServiceHandledFromAppClass
|
appName, appNameAbbreviation, notificationActivity; the last is handled |
setTripRecordingOnlyWhenBeaconInRange(true) |
mode = BEACON |
lastSelectedUnitSerialNumber = ... |
PortalAPI's announcement; vehicleSelected(unitSerialNumber, unitType) for an app without PortalAPI; deviceID() on the host for a rule of the app's own |
setTripAutoSendingEnabled, setIBeaconTripAutoSending
|
autoSend, smartDrive.beaconTripAutoSend
|
Scheduling DeleteOldTripsWorker yourself |
smartDrive.tripRetentionDays; start schedules the sweep |
resendFailedTrips(true) |
smartDrive.resendFailedTrips |
AUTOSTOP_THRESHOLD_PREF (minutes) |
smartDrive.autostopThresholdSeconds |
setMinimumTripDistance, REGULAR_LOCATION_ACCURACY_THRESHOLD_PREF
|
minTripDistanceMeters, locationAccuracy
|
setMOTEnabled, setStaticMapsApiKey, setElevationApiKey, setSkipTripMOTs, setCustom*MOT*
|
motEnabled, motStaticMapsKey, motElevationKey, motSkipTypes, motChecks
|
setupProfilerConfiguration(...) |
realtimeSending = true, realtime = MProfilerConfiguration(...)
|
ACCIDENT_DETECTION_* preferences, setPositionAfterIncident*
|
accidentDetectionEnabled, accidentValidation, positionAfterIncidentDurationSeconds, positionAfterIncidentIntervalSeconds
|
DISTRACTION_DETECTION_* preferences |
distractedDrivingEnabled, distractedDrivingPhoneUseEnabled, distractedDrivingPhoneCallEnabled, distractionMinSpeedKmh, distractionSensor
|
showNotificationForValidTripOnly, showDistractedDriverNotification, checkForOldTrips
|
the three notification fields |
enableTripAutoStart() + runRecordTripService()
|
initialize(...) and start(...), then authenticated()
|
disableTripAutoStart() + stopRecordTripService()
|
stop() or logout()
|
allTrips, allUnsentTrips
|
getLocalTrips(), getTargetTrips()
|
sendAllTrips(), sendAllUnsentTrips(...)
|
retryFailedTrips() |
Trip start/end broadcast receivers, TripManagementHelper LiveData |
the four callbacks, tripState, tripCounts
|
sendFakeInVehicleActivityUpdate(context) |
unchanged; a test affordance on the manager, not part of the facade |
| Any setter without a field | smartDrive.configureSmartDrive = { manager -> ... } |
- Dependency added and resolving; a Timber tree planted.
- Host answers
isAuthorized();deviceID()overridden only if your app picks the vehicle by its own rule;vehicleSelectedcalled on vehicle selection if your app has no PortalAPI. -
initializeinApplication.onCreate; configuration built;startruns without an exception. -
authenticated()after login,permissionsChanged()after the permission flow,logout()on logout. - Permissions requested from
requiredPermissions(configuration), background location in its own dialog. - The four callbacks, or
TripListener, wired. - No remaining direct calls that start or stop the service, write engine preferences or select the vehicle.
-
isHealthy()true on a logged-in device with permissions; the health snapshot in the log shows no warning marks.
Consolidated SDK
SmartDrive
PortalApiBase
PortalApiClient
Wellness
SBeacon
- Overview
- Installation
- Initialization
- Beacon Scanning
- Beacon Pairing
- Trip Management
- Driving Behavior
- Calibration
- Event Handling
- Data Models
- API Reference
Consolidated SPM
SDApiManager
CCFrameworkOpen
SmartDrive
SBeacon