# Task: Integrate the truemetrics SDK into this courier app
You are integrating the truemetrics telemetry SDK into this courier/driver app. truemetrics records device sensor data during delivery tours and uses it to detect stops, parking places and building entrances, and to measure stop times. Data collection, buffering and upload are handled by the SDK itself. The app only needs to: initialize the SDK, start/stop recording, and log metadata.
Work in this order:
1. Analyze the codebase: language and framework, login/logout, route start/end, navigation start, delivery confirmation (success/fail), app startup and session restore, and the data models for routes, stops, deliveries, parcels and addresses.
2. For EACH integration point below, propose the exact file, function and line, explain why, and ASK ME TO CONFIRM before writing code. Ask all questions in one batch where possible. You may add the SDK repository and dependency before asking, to read the SDK's manifest and API; change nothing else before I confirm, and include this change in your questions.
3. Implement only what I confirmed. Keep changes minimal and isolated (ideally one wrapper class, e.g. `TruemetricsTracker`).
4. Use the SDK API reference at the end of this prompt. If a signature differs in the installed version, the installed version wins. For metadata event names, keys and triggers, follow this prompt where other truemetrics documentation pages differ.
## API key
The API key is issued by truemetrics. If I have not given you one, stop and tell me to request it from jan@truemetrics.io. Store it in the app's existing secrets/config mechanism (e.g. Gradle property, .env, build config), never hard-coded in source or committed to git.
## 0. Choose the SDK variant
Detect the app's language and framework, then pick the matching SDK and tell me which one you chose:
- Android native (Kotlin/Java): `io.truemetrics:truemetricssdk` from the truemetrics Maven repo, version 1.6.3 or newer.
- Flutter: package `truemetrics_flutter_sdk`, version 0.0.13 or newer (`^0.0.13`). Older versions start recording on init by themselves; if dependency resolution fails, tell me instead of using one. Leave `delayAutoStartRecording` out of the config, even where the plugin's README or example sets it.
- Any other framework (e.g. React Native, Expo): this prompt does not cover it. Stop and tell me to contact truemetrics.
Both variants: the native SDK is built with Kotlin 2.2, so the app's Kotlin Gradle plugin (`org.jetbrains.kotlin.android`) must be 2.1 or newer; otherwise the build fails with "Module was compiled with an incompatible version of Kotlin". If it is older, tell me before upgrading it.
Flutter 0.0.13 wraps native SDK 1.6.0, which has no `FOREGROUND_SERVICE_START_REFUSED`: its `initialize` can throw when the app is in the background. Catch any exception (not only `PlatformException`), call `initialize` again on the next `AppLifecycleState.resumed`, and never initialize from a background isolate (FCM background handler, WorkManager). In 0.0.13, `deInitialize()` blocks the Android main thread, so logout can freeze the UI for a few seconds; this cannot be avoided from Dart, so await it as usual. Tell me about both when you report the chosen variant.
Reference implementations (public GitHub, org TRUE-Metrics-io): `truemetrics_android_demo_app_p` (Kotlin), `truemetrics_flutter_p` (Flutter plugin + example).
iOS: truemetrics does NOT record on iPhone/iOS devices. In Flutter apps, guard every truemetrics call with `Platform.isAndroid` so it only runs on Android and is a safe no-op on iOS. Do not add iOS permissions or background modes for truemetrics.
Confirm with me: the detected framework and the SDK variant.
## 1. Permissions and configuration
The single source of truth for permissions is the SDK's own `AndroidManifest.xml`. Do not rely on a hard-coded list: permissions change between SDK versions.
Read it for the installed SDK version: the `AndroidManifest.xml` inside the `truemetricssdk` AAR (Maven repo or Gradle cache), or the manifest merger report after a build (native: `app/build/outputs/logs/manifest-merger-*-report.txt`; Flutter: `build/app/outputs/logs/manifest-merger-*-report.txt`). List every `uses-permission` and `service` it declares. They merge into the app automatically: make sure the app's config removes none of them (`tools:node="remove"`). Re-check this whenever the SDK version is updated.
Steps:
- Check the app's config files: `app/src/main/AndroidManifest.xml` (native), `android/app/src/main/AndroidManifest.xml` (Flutter), plus Gradle files (minSdk, repositories, dependency).
- Add the SDK repository and dependency if missing (minSdk 24). Flutter: add only the Maven repository, to `allprojects { repositories }` in `android/build.gradle(.kts)`, or to `dependencyResolutionManagement { repositories }` in `android/settings.gradle(.kts)` if the app declares repositories there; the plugin pins the native SDK version, so do not add the native dependency.
- Request the runtime permissions BEFORE recording starts, ideally right after login, and on app start for users who are already logged in: every runtime (dangerous) permission found in the SDK manifest, location first and background location after it. Before the first location permission request, show an in-app dialog stating that the app collects location data in the background, also when the app is closed or not in use, and what it is used for (Google Play requires this prominent disclosure). Reuse the app's existing permission flow if it has one.
- Handle the SDK status `AskForPermissions(permissions)` (Flutter: `onPermissionsRequired`) by requesting the listed permissions.
- Exclude the app from battery optimization if the app's existing flow supports it.
Show me the permission and config diff before applying it.
## 2. Initialization and deinitialization
- Initialize the SDK right after a successful login. It needs the API key; on native Android a foreground notification factory is optional (to customize the notification).
- Also initialize it on every app start while a user is already logged in (native: `Application.onCreate()`, in the main process only; Flutter: at app startup, once the stored session is restored). Native: do not add a foreground check: `onCreate()` can run in the background, and on 1.6.3+ the SDK may then report `FOREGROUND_SERVICE_START_REFUSED` and finishes starting once the app's UI is shown. Flutter: see section 0. A recording that was started and not stopped survives a crash, an OS kill, an app update or a reboot, and resumes on the next init. Without that init, recording stays off and metadata is lost until the next login.
- `init()` starts the SDK's foreground service. Its notification stays visible until `deinitialize()`, also while no recording is running. Tell me about this.
- Use explicit start: recording must only begin when `startRecording()` is called (this is the default since SDK 1.6.0; do not enable auto-start via `delayAutoStartRecording`).
- Call init from one place in the wrapper: once per process, and again after `deinitialize()` has returned (Flutter: after awaiting `deInitialize()`). Track this with the wrapper's own flag, set only when init did not throw. The SDK status and Flutter's `isInitialized()` are not reliable for this check.
- Deinitialize on logout only, not at route end: uploading of the remaining data stops at `deinitialize()` and continues only after the next init. Native: call `deinitialize()` off the main thread, it can block for a few seconds.
- Observe the SDK status and log errors. `FOREGROUND_SERVICE_START_REFUSED` is not fatal (see above).
- Do not add a client-side init timeout or assert: offline and without a cached configuration (first init after install or after `deinitialize()`), the SDK stays `Initializing` and keeps retrying.
Confirm with me: where login completes, where an existing session is restored on app start, and where logout happens.
## 3. Start recording
`startRecording()` starts the sensor recording. It should run when the courier leaves the depot. Preferred triggers, in this order:
1. Driver starts the route (preferred).
2. Navigation is started for the first time after login.
3. After the first delivery is completed (acceptable if missing the first stop is OK).
4. During login (fallback only: records extra data, e.g. while the vehicle is being loaded).
Check `isRecordingInProgress()` before starting to avoid duplicate starts. Do not call `startRecording()` on a plain app start: a started recording resumes on init by itself (section 2). Flutter (native SDK 1.6.0): hold `startRecording()` and `stopRecording()` until the SDK is ready (section 5), then call them; never call them straight after `initialize`. After a login, if this user's route is already started and not finished (e.g. after a forced re-login mid-route), call `startRecording()` again: the logout's `deinitialize()` stopped it.
IMPORTANT, no high-frequency start/stop: the SDK is not designed to be started and stopped often. Start recording once at the beginning of the route and stop it once when the route is finished (logout stops it through `deinitialize()`). In between, leave it running: do NOT stop and restart per stop, per delivery, per screen, or when the app goes to the background. The only accepted exception is a courier break that must not be recorded (e.g. a lunch break the app knows about): stop at break start and start again at break end.
Confirm with me: which trigger this app supports and where it lives in code.
## 4. Stop recording
Call `stopRecording()` when:
- The driver ends the route. If the app has no explicit route end, propose one (e.g. the last stop reaching a final state).
- The driver ends the shift through an explicit action in the app, if there is one. On logout, `deinitialize()` (section 2) stops the recording; no separate stop is needed.
- On app start (after init) or when the app returns to the foreground, if the app's data shows the route is already finished or belongs to a previous day (once per finished route, not on every foreground).
- Recommended: a daily time cutoff, e.g. stop automatically at 22:00 local time (a scheduled job, plus the same check on app start; Flutter: a timer while the app runs plus the checks on app start and resume, no background isolate). The foreground service keeps the app process alive, so a route that is never ended would otherwise keep recording overnight. After the cutoff, start again that day only on an explicit route start.
Call `stopRecording()` unconditionally, without an `isRecordingInProgress()` check: right after init that check is still false. Native: a stop issued after `init()` but before the SDK is ready is queued; a stop issued before `init()` is discarded. Flutter: hold it until the SDK is ready, then call it (section 3); do not drop it.
Do NOT stop recording just because a crash handler or a lifecycle callback fired (`onDestroy`, `onTaskRemoved`, `onTerminate`, Flutter `AppLifecycleState` changes or `dispose()`, app swiped away from recents, app moved to the background). They fire on screen rotation, on swipe-away and when the system destroys the app's screens in the background. The only stops tied to the app lifecycle are the finished-route and cutoff checks above. A recording that was started and not stopped resumes on the next init (section 2).
Confirm with me: all stop points, and the cutoff time (or that I decline the cutoff).
## 5. Log metadata
Required: for EACH delivery, send exactly ONE metadata object via `logMetadata` (Flutter: `logStandardMetadata`), independent of the outcome. Successful, failed, rejected, delivered to a neighbor or parcel locker: every delivery attempt produces one event, with the outcome in `delivery_state`.
When to send it:
- Trigger on the moment the driver finalizes the delivery in the app: the final scan, the confirm button, the signature or photo capture completing, or the failure / reject reason being selected.
- Find the single place in code where a delivery reaches its final state (e.g. the handler that marks the stop or task as done/failed). Hook in there, not in UI buttons that can be pressed several times.
- One event per delivery attempt: do not send on intermediate steps (opening the stop, starting a scan) and do not send twice if the driver edits the result. If this app's data cannot tell an edit from a new attempt, ask me. If a stop holds several parcels for the same address delivered together, send one event for the stop and list the parcel IDs.
- A later second attempt at the same address (e.g. after a failed first try) is a new delivery attempt and gets its own event.
- Pickups follow the same rule with `eventType` = `pickup`.
IMPORTANT, neighbor deliveries: every delivery handed to a neighbor (or any other address than the recipient's, e.g. a nearby shop) MUST be identifiable in the metadata. Set `extra.delivered_to_neighbor` = `true` (otherwise `false`) and, if the app knows it, `extra.neighbor_address` (the street address, not the neighbor's name) / `extra.neighbor_address_id`. Reason: the courier stopped at a different building than the recipient's address, so truemetrics must be able to filter these events out. Otherwise a wrong parking place or entrance gets linked to the recipient's address_id. If the app only has a status code for this, still send the explicit flag and keep the raw code in `delivery_state`.
Send it right away, also when offline. Do not check connectivity. The SDK buffers everything on the device and uploads when a good connection is available. Log at the moment of the driver's action, from the code that first saves it on the device, never from a server-sync callback: for some accounts the SDK uploads only the sensor data around the time `logMetadata` is called. If logging can only happen later, put the actual event timestamp in `eventTime`.
The SDK drops metadata that is logged before it is ready, e.g. right after init or on an app start before init ran. The SDK is ready from the first `Initialized`, `RecordingInProgress`, `RecordingStopped`, `DelayedStart`, `TrafficLimitReached`, `ReadingsDatabaseFull` or `AskForPermissions` status after each init (Flutter: also any `onPermissionsRequired` call, even with an empty list) until `deinitialize()`, also through later errors. In the wrapper, build each event, `eventTime` included, when the driver acts. While the SDK is not ready or the queue is not empty, append the event to a queue kept in local storage (the wrapper's own file or preferences, so no database migration is needed). Once the SDK is ready, send the queued events in order, then log directly. Keep only the newest 100 events in the queue: it is sent in one burst, and metadata logged together is uploaded as one piece (section 6). When trimming, never drop a `planned_sequence` event; drop the oldest other events instead. Keep the queue across logout; it is sent after the next init.
Confirm with me: the exact function(s) where a delivery reaches its final state, and how multi-parcel stops are modeled.
Use the standard metadata fields of `StandardMetadata` (wire keys in brackets). All values are strings.
| Field (wire key) | Content |
| --- | --- |
| eventTime (`t_event`) | ISO 8601 timestamp of the driver interaction. |
| eventType (`type_event`) | `delivery` or `pickup`. |
| deliveryId (`id_delivery_external`) | Delivery ID. |
| tourId (`id_tour`) | Route / tour ID. |
| waypointLatitude, waypointLongitude (`lat_waypoint_external`, `lon_waypoint_external`) | Waypoint: the street coordinate the courier is actually routed to, i.e. the point your routing system and the courier's navigation use. Typically a street projection from Google Maps or from an internal system. |
| referenceLatitude, referenceLongitude (`lat_reference_external`, `lon_reference_external`) | Reference: where you believe the delivery is actually done, i.e. the destination building. Typically a rooftop coordinate from your address system or geocoder. |
| address (`address_raw`) | Normalized if available, raw is fine, as one string. |
Put everything else into `extra`:
| Extra key | Content |
| --- | --- |
| address_id | HIGHEST PRIORITY. Stable primary identifier for the address. truemetrics returns parking places and entrances keyed by this ID. |
| parcel_id | Parcel ID(s), comma-separated for multi-parcel stops. |
| delivery_state | success, failed, or special (e.g. delivered to neighbor, customer not home, parcel locker), plus the raw internal code. |
| delivered_to_neighbor | Required. `true` if handed to a neighbor or any other address than the recipient's, else `false`. Plus `neighbor_address` / `neighbor_address_id` if known. |
| service_type | Raw internal code as-is (e.g. signature required, age check). |
| eta_first, eta_latest | Required. First planned ETA and the most recent updated ETA, as ISO 8601 timestamps. If the app has only one ETA, send it in both. If it has none, tell me; do not compute one. |
| depot_id | ID of the depot or hub the route starts from, as this app's systems name it. Send it on every event, also on `planned_sequence` (section 6). If the app has no depot ID, tell me; do not invent one. |
| street, house_number, postal_code, city | Address parts, if the app has them separately. |
| vehicle_type, driver_id, driver_experience, ... | Any other fields commonly used in this app's systems. No recipient names, phone numbers or free-text notes. |
Confirm with me: the field mapping from this app's data models to the tables above.
## 6. Log the planned sequence at route start
Required. Exactly once per route, at the moment the route is actually started, make one dedicated `logMetadata` call for the planned sequence. It is its own event, never combined with a delivery event. Set `eventType` = `planned_sequence`, `eventTime` and `tourId` set, and in `extra` under the key `planned_sequence` the ordered list of planned stops serialized as a JSON array string, e.g. `[{"seq":1,"task_id":"T321","eta":"2026-10-15T08:12:00Z"}, ...]`. Also set `extra.stop_count` and `extra.depot_id`. Leave the other standard fields empty.
Traceability: each planned stop needs exactly one identifier, e.g. task_id, stop_id or parcel_id, whichever the app has. Use the same key name and value in the delivery events from section 5 (add it to their `extra` if missing), so truemetrics can match each planned stop to its actual delivery event. No further IDs are needed in the sequence. Required: each planned stop must also carry its planned ETA in `eta` (ISO 8601).
Size: metadata logged close together is uploaded as one piece, the upload limit is set per account and can be as low as 200 KB, and a piece above the limit is never uploaded. Keep the `planned_sequence` event under 50 KB: per stop only the identifier, `seq` and `eta`. A typical route of 100 to 200 stops is far below that. If it would still be larger, tell me; do not split it into several events.
Do not send it again on re-plans, app restarts or re-logins for the same route: persist a sent flag per tourId, set in the same step that logs or queues the event (section 5).
## 7. Device ID for debugging
After each init, take `deviceId` from the first `Initialized` or `RecordingInProgress` status (Flutter: from that `onStateChange` event), or call `getDeviceId()` once the SDK is ready (section 5), and store it in this app's own backend or logs next to the driver/session. It identifies the phone and installation, changes periodically, and is only used for debugging with truemetrics.
## 8. Final check: test recording
Finish with this test, together with me: I run it on the test device, you tell me each step and run the check. Tell me at the start that it takes about 50 to 75 minutes, most of it waiting.
1. Recording (30 min): on a real Android test device that has not recorded earlier that UTC day, once the SDK reports `Initialized` (metadata logged before that is lost), start recording and keep it running for 30 minutes, ideally while moving (walking or driving), so location, gyroscope and accelerometer produce data. Finish before 00:00 UTC: the check covers one whole UTC day.
2. Metadata: at about minutes 1, 15 and 29 of the recording, log 3 test metadata objects through the wrapper (section 5) with `eventType` = `integration_test` and, in `extra`, the same `test_run_id` (a new unique ID, e.g. a UUID) and `test_index` `1`, `2`, `3`. A temporary debug action is enough; do not ship it in the release build.
3. Stop: stop the recording with the app's stop trigger or the debug action, not by logging out, and keep the app logged in and the device online until the check passes: the SDK uploads the end of the recording after the stop, and after a logout it uploads nothing until the next login.
4. Check (up to 45 min): ask me for the test device's ID (section 7). Save the script below outside the repository (never commit it) and start it right after the recording stops, as `python3 check_integration.py <device ID>` (`py` instead of `python3` on Windows), with `TRUEMETRICS_API_KEY` set from where you stored the API key ("API key" above), never written into a file or a command line. It waits 20 minutes, since data arrives with the SDK's uploads, typically 10 to 20 minutes after recording (tell me this wait is expected, not an error), then checks every 5 minutes up to 45 minutes after the stop. Run it in the background and read its output when it ends; if you cannot run background commands, give me the script and the command, and ask me for its output. It needs Python 3.8 or newer and nothing else.
"""Checks a test recording against GET /testresult. Start it right after the recording stops:
python3 check_integration.py <device ID> [YYYY-MM-DD], with the key in TRUEMETRICS_API_KEY
Waits 20 minutes, then checks every 5 minutes until 45 minutes after the stop.
Exit code: 0 passed, 1 failed, 2 the check could not run, 3 wrong key or request.
Needs Python 3.8 or newer, nothing else.
"""
import datetime
import json
import os
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
URL = "https://geodata.truemetrics.cloud/V1/testresult"
def utc_now():
return datetime.datetime.now(datetime.timezone.utc)
def test_objects(result):
"""Metadata entries that carry both test fields."""
return sum(
1
for entry in result.get("customer_metadata") or []
if {"test_run_id", "test_index"} <= set(entry.get("fields") or [])
)
def summary(result, names):
"""The given fields of a result, plus its number of test objects."""
shown = {name: result.get(name) for name in names}
shown["test_objects"] = test_objects(result)
return json.dumps(shown, indent=2)
def fetch(url, key):
"""One request: the HTTP status (0 for no answer) and the body."""
request = urllib.request.Request(url, headers={"x-api-key": key})
try:
with urllib.request.urlopen(request, timeout=60) as response:
return response.status, json.load(response)
except urllib.error.HTTPError as error:
return error.code, error.read().decode(errors="replace")
except (urllib.error.URLError, OSError, ValueError) as error:
return 0, str(getattr(error, "reason", error))
def main():
key = os.environ.get("TRUEMETRICS_API_KEY")
if len(sys.argv) not in (2, 3) or not key:
print("usage:", __doc__.splitlines()[1].strip(), file=sys.stderr)
return 3
device_id = sys.argv[1]
# The recording's UTC day, fixed before the wait.
day = sys.argv[2] if len(sys.argv) == 3 else utc_now().date().isoformat()
query = urllib.parse.urlencode({"id_device": device_id, "date": day})
print(f"Checking device {device_id} for {day} (UTC).", flush=True)
time.sleep(int(os.environ.get("WAIT_SECONDS", "1200")))
last = None
for check in range(1, 7):
if check > 1:
time.sleep(300)
status, body = fetch(f"{URL}?{query}", key)
answer = f"HTTP {status}" if status else f"no answer ({body})"
print(f"{utc_now():%H:%M} UTC, check {check} of 6: {answer}", flush=True)
if status == 200:
ok = body.get("status") == "ok" and body.get("config") == "device"
if ok and test_objects(body) >= 3:
print("PASSED", summary(body, ("counts", "recording_minutes")), sep="\n")
return 0
last = body
elif status in (400, 403):
print(body)
if status == 403:
print("Wrong API key, or the key is not enabled for this check: ask truemetrics.")
return 3
if last is not None:
print("FAILED", summary(last, ("status", "reasons", "config", "counts")), sep="\n")
return 1
print("The check could not run: this is not a test result. Try again later.")
return 2
if __name__ == "__main__":
sys.exit(main())
Exit code 0: the test passed. 1: it failed (see below); `test_objects` below 3 means the test metadata did not arrive. 2: the check could not run (rate limit or server error): that is not a test result, run it again later with `WAIT_SECONDS=0` and, as the second argument, the UTC day it printed at the start (it would otherwise check the current day). 3: the key or the request was rejected; on a `403`, tell me to ask truemetrics to enable the key for this check. A result with `config` `unavailable` (rates not checked) does not pass: report it to me.
If it failed, go through every entry in `reasons` with me and suggest fixes. Few or no readings: recording did not start, a sensor permission is missing (section 1), or the app was logged out or offline after the recording. Rates below the configured frequency: the app is restricted in the background, e.g. by battery optimization. `no customer metadata`: metadata was logged before the SDK was ready, or never sent (section 5). `unknown device`: the ID is mistyped, the key is not the one the app uses, or the SDK never initialized on that device. To repeat the test on the same day, clear the app's data on the test device first, or use another device: the device then gets a new ID, so the earlier run does not count.
## SDK API reference
### Android native (Kotlin/Java)
Gradle:
repositories { maven { url = uri("https://github.com/TRUE-Metrics-io/truemetrics_android_SDK_p_maven/raw/main") } }
dependencies { implementation("io.truemetrics:truemetricssdk:<1.6.3 or newer, latest in io/truemetrics/truemetricssdk/maven-metadata.xml of that repo>") }
| Purpose | Call |
| --- | --- |
| Init | `TruemetricsSdk.init(context, SdkConfiguration.Builder(apiKey).build())`, optionally `.foregroundNotificationFactory(factory)` on the builder |
| Instance | `TruemetricsSdk.getInstance()` |
| Status | `sdkStatus` (StateFlow) or `observeSdkStatus()`: `Uninitialized`, `Initializing`, `Initialized(deviceId)`, `RecordingInProgress(deviceId)`, `RecordingStopped`, `DelayedStart`, `TrafficLimitReached`, `ReadingsDatabaseFull`, `AskForPermissions(permissions)`, `Error(errorCode, message)`. Needs `kotlinx-coroutines` in the app; from Java, add `androidx.lifecycle:lifecycle-livedata-ktx` and use `FlowLiveDataConversions.asLiveData(sdk.getSdkStatus())` |
| Is ready | no direct call: see the ready statuses in section 5 |
| Start / stop | `startRecording()`, `stopRecording()` |
| Recording state | `isRecordingInProgress()`, `isRecordingStopped()`, `getRecordingStartTime()` |
| Metadata | `logMetadata(StandardMetadata(eventTime, eventType, deliveryId, tourId, waypointLatitude, waypointLongitude, referenceLatitude, referenceLongitude, address, extra))` (the `Map` overload is deprecated) |
| Device ID | `getDeviceId()` (see section 7) |
| Teardown | `deinitialize()`, off the main thread |
### Flutter (`truemetrics_flutter_sdk`, class `TruemetricsFlutterSdk`)
`initialize(TruemetricsConfig(config: {'apiKey': ...}))` (in try/catch, see section 0), `startRecording()`, `stopRecording()`, `isRecordingInProgress()`, `logStandardMetadata(StandardMetadata)` (all nine standard fields are required in Dart: pass `''` for unknown ones; `extra` is `Map<String, String>`), `getDeviceId()`, `deInitialize()`. All return Futures. Status, errors and permission requests: `setStatusListener(onStateChange: (event) {...}, onError: (errorCode, message) {...}, onPermissionsRequired: (permissions) {...})`. `event.state` is a `TruemetricsState` (same names as native, lower camel case) and `event.deviceId` the device ID; errors arrive only in `onError`. Each call replaces the previous listener, so call it once, in the wrapper.
## Done when
- Correct SDK variant added, iOS calls guarded as no-ops.
- All required permissions present in the merged manifest and requested at runtime before recording.
- Init on login and on every app start with a logged-in user, API key, explicit start, deinit on logout, status observed and logged.
- Start and stop recording at the confirmed triggers: start guarded by `isRecordingInProgress()`, stop unconditional and after init. No stop from crash handlers or lifecycle callbacks.
- Exactly one metadata event per delivery attempt, successful or failed, with `delivered_to_neighbor`, `eta_first`, `eta_latest`, `depot_id` and the stop identifier shared with the planned sequence; one planned sequence event with an `eta` per stop at route start. Metadata queued in local storage until the SDK is ready.
- Device ID stored in our systems.
- The final check (section 8) passed: `/testresult` returned `ok` and `config` `device`, with the 3 test metadata objects.
- Remind me of the Google Play Console declarations this needs: background location access, and foreground service types Location and Data sync.
- Give me a short summary of every file changed and a test plan: developer test on a real Android device (including killing the app mid-route and reopening it, an app start while offline, and logout and login mid-route), then a system test with about 3 hours of recording under field-like conditions.