> ## Documentation Index
> Fetch the complete documentation index at: https://docu.truemetrics.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# Android Native Integration

> Complete guide for integrating Truemetrics SDK 1.5.4 into Android applications

The Truemetrics SDK provides advanced sensor data collection and analytics capabilities for Android
applications. This guide covers all aspects of integrating and using the SDK effectively.

## Table of Contents

* [Installation](#installation)
* [Quick Start](#quick-start)
* [Initialization Lifecycle](#initialization-lifecycle)
* [Configuration](#configuration)
* [Core Features](#core-features)
* [SDK Status](#sdk-status)
* [Statistics API](#statistics-api)
* [Best Practices](#best-practices)
* [API Reference](#api-reference)

***

## Installation

Add the following to your root-level `build.gradle` file (or `settings.gradle`):

```gradle theme={null}
repositories {
    maven {
        url "https://github.com/TRUE-Metrics-io/truemetrics_android_SDK_p_maven/raw/"
    }
}
```

Then add the dependency to your app-level `build.gradle`:

```gradle theme={null}
dependencies {
    implementation 'io.truemetrics:truemetricssdk:1.5.4'
}
```

## Quick Start

### 1. Initialize the SDK in your Application class

```kotlin theme={null}
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        val config = SdkConfiguration.Builder("YOUR_API_KEY").build()
        TruemetricsSdk.init(this, config)
    }
}
```

### 2. Start and stop recordings

```kotlin theme={null}
val sdk = TruemetricsSdk.getInstance()

// Start recording sensor data
sdk.startRecording()

// Stop recording
sdk.stopRecording()

// Check recording status
if (sdk.isRecordingInProgress()) {
    // Recording is active
}
```

***

## Initialization Lifecycle

`TruemetricsSdk.init()` returns immediately but the SDK is **not ready for use until it reaches `Status.Initialized`**. Initialization runs asynchronously in the background: it binds a foreground service, then fetches the configuration from the backend.

### State Transitions

```
Uninitialized ──► Initializing ──► Initialized
                       │
                       ├──► Error(AUTHENTICATION_ERROR)   (invalid/expired/revoked API key)
                       │
                       └──► stays in Initializing, retrying every 30s
                            (network unreachable, 5xx from backend, timeouts)
```

### How Long To Wait

| Condition              | Typical time to `Initialized`                                            |
| ---------------------- | ------------------------------------------------------------------------ |
| Good network           | 1–3 seconds                                                              |
| Slow/lossy network     | up to tens of seconds                                                    |
| Backend 5xx or offline | indefinite — retries every 30 seconds until it succeeds                  |
| Invalid API key        | fails fast (under \~2 seconds) with `Status.Error(AUTHENTICATION_ERROR)` |

<Note>
  There is no hard timeout on initialization. Under poor connectivity the SDK stays in `Status.Initializing` and keeps retrying. Do not impose an arbitrary client-side timeout — either wait for `Status.Initialized`/`Status.Error`, or gate UI on `getDeviceId()` becoming non-null.
</Note>

### Checking Initialization Status

**Recommended — observe the status flow and wait for `Status.Initialized`:**

```kotlin theme={null}
import io.truemetrics.truemetricssdk.engine.state.Status
import kotlinx.coroutines.flow.first

suspend fun awaitSdkReady(): String {
    val sdk = TruemetricsSdk.getInstance()
    val ready = sdk.observeSdkStatus().first { status ->
        status is Status.Initialized ||
        status is Status.RecordingInProgress ||
        status is Status.DelayedStart ||
        status is Status.Error
    }
    if (ready is Status.Error) {
        throw IllegalStateException("SDK init failed: ${ready.errorCode} ${ready.message}")
    }
    return sdk.getDeviceId()!!
}
```

**Non-blocking — react to state changes as they arrive:**

```kotlin theme={null}
sdk.observeSdkStatus().collect { status ->
    when (status) {
        is Status.Initializing -> showSpinner()
        is Status.Initialized -> enableStartButton(status.deviceId)
        is Status.Error -> showError(status.errorCode, status.message)
        else -> {}
    }
}
```

**Quick synchronous check** — `getDeviceId()` returns `null` until initialization completes:

```kotlin theme={null}
if (sdk.getDeviceId() != null) {
    // SDK is initialized
}
```

### Calling Other APIs Before Initialization Completes

* `startRecording()` — safe to call immediately after `init()`. If the service is not yet bound, the request is queued and executed once binding completes. However, if called after the service binds but **before** the config fetch succeeds, it will transition the SDK to `Status.Error(CONFIG_ERROR)`. To avoid this, either await `Status.Initialized` first, or rely on the auto-start configuration (see [Configuration](#configuration)).
* `logMetadata()` — ignored with a log warning if called before initialization. Wait for `Status.Initialized` before logging.
* `getDeviceId()`, `getActiveConfig()`, `getUploadStatistics()`, `getSensorStatistics()` — return `null` until initialization completes.

### Terminal vs. Transient Failures

| Status                                          | Meaning                                                             | Your action                                                                |
| ----------------------------------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `Status.Initializing` (stuck)                   | Backend unreachable or returning 5xx; SDK auto-retries every 30s    | Wait, or surface a "connecting" indicator. No need to call `init()` again. |
| `Status.Error(AUTHENTICATION_ERROR)`            | API key invalid, expired, or revoked. SDK has deinitialized itself. | Do **not** retry with the same key. Fix the key, then call `init()` again. |
| `Status.Error(CONFIG_ERROR)`                    | `startRecording()` was called before config was loaded              | Await `Status.Initialized` before calling `startRecording()`.              |
| `Status.Error(MISSING_NOTIFICATION_PERMISSION)` | Android 13+ notification permission not granted                     | Request `POST_NOTIFICATIONS`, then re-init.                                |

***

## Configuration

The SDK is configured using the `SdkConfiguration.Builder` class.

### Basic Configuration

```kotlin theme={null}
val config = SdkConfiguration.Builder("YOUR_API_KEY").build()
```

### Custom Foreground Notification

Customize the notification shown when the SDK is running as a foreground service using the SDK's notification channel:

```kotlin theme={null}
class CustomNotificationFactory : ForegroundNotificationFactory {
    override fun createNotification(context: Context): Notification {
        // Use SDK's notification channel ID
        return NotificationCompat.Builder(context, "FOREGROUND_SERVICE")
            .setContentTitle("Truemetrics SDK Running")
            .setContentText("Collecting sensor data...")
            .setSmallIcon(R.drawable.ic_sensors)
            .setOngoing(true)
            .build()
    }
}

val config = SdkConfiguration.Builder("YOUR_API_KEY")
    .foregroundNotificationFactory(CustomNotificationFactory())
    .build()
```

***

## Core Features

### Recording Management

```kotlin theme={null}
val sdk = TruemetricsSdk.getInstance()

// Start recording
sdk.startRecording()

// Stop recording
sdk.stopRecording()

// Check recording status
val isRecording = sdk.isRecordingInProgress()
val isStopped = sdk.isRecordingStopped()

// Get recording start time
val startTime = sdk.getRecordingStartTime() // Returns timestamp in milliseconds
```

### Device ID

Get the unique device identifier (available after initialization):

```kotlin theme={null}
val deviceId = sdk.getDeviceId()

// Or get from Status
sdk.observeSdkStatus().collect { status ->
    when (status) {
        is Status.Initialized -> println("Device ID: ${status.deviceId}")
        is Status.RecordingInProgress -> println("Device ID: ${status.deviceId}")
        else -> {}
    }
}
```

<Note>
  The device ID may rotate automatically based on a server-configured TTL. When expired, a new ID is generated transparently on the next initialization.
</Note>

### Metadata Logging

Log standardized delivery/pickup event metadata:

```kotlin theme={null}
import io.truemetrics.truemetricssdk.metadata.StandardMetadata

sdk.logMetadata(StandardMetadata(
    eventTime = "2025-01-15T14:30:00",
    eventType = "delivery_successful",
    deliveryId = "parcel_123",
    tourId = "tour_456",
    waypointLatitude = "52.5200",
    waypointLongitude = "13.4050",
    referenceLatitude = "52.5201",
    referenceLongitude = "13.4051",
    address = "Pflanzstrasse 5, 10762 Berlin",
    extra = mapOf("type_vehicle" to "car")
))
```

The map-based `logMetadata(Map<String, String>)` overload is deprecated. Use `logMetadata(StandardMetadata)` instead.

For advanced metadata with templates and tags, see [Metadata Guide](/metadata).

### Sensor Management

```kotlin theme={null}
// Enable/disable all sensors
sdk.setAllSensorsEnabled(true)

// Check if sensors are enabled
val enabled = sdk.getAllSensorsEnabled()

// Observe available sensors
sdk.sensorInfo.collect { sensors ->
    sensors.forEach { sensor ->
        println("Sensor: ${sensor.sensorName}, Status: ${sensor.sensorStatus}")
        println("Frequency: ${sensor.frequency} Hz")
        if (sensor.missingPermissions.isNotEmpty()) {
            println("Missing permissions: ${sensor.missingPermissions}")
        }
    }
}
```

### Complete Cleanup

```kotlin theme={null}
sdk.deinitialize()
```

**Warning:** After calling `deinitialize()`, the SDK instance becomes unusable. You must call
`init()` again to use the SDK.

***

## SDK Status

Monitor the SDK state using `observeSdkStatus()`:

```kotlin theme={null}
sdk.observeSdkStatus().collect { status ->
    when (status) {
        is Status.Uninitialized -> {
            // SDK not yet initialized
        }
        is Status.Initializing -> {
            // SDK is fetching config from server
            // Retries automatically on network errors or server 5xx
        }
        is Status.Initialized -> {
            // SDK initialized, deviceId available
            val deviceId = status.deviceId
        }
        is Status.RecordingInProgress -> {
            // Recording is active
            val deviceId = status.deviceId
        }
        is Status.RecordingStopped -> {
            // Recording has stopped
        }
        is Status.DelayedStart -> {
            // Waiting for auto-start delay
            val deviceId = status.deviceId
            val delayMs = status.delayMs
        }
        is Status.TrafficLimitReached -> {
            // Traffic limit reached
        }
        is Status.ReadingsDatabaseFull -> {
            // Device storage full
        }
        is Status.Error -> {
            // SDK encountered an error
            val errorCode = status.errorCode
            val message = status.message
        }
        is Status.AskForPermissions -> {
            // SDK requesting permissions
            val permissions = status.permissions
        }
    }
}
```

### Error Codes

| Error Code                        | Description                                                          |
| --------------------------------- | -------------------------------------------------------------------- |
| AUTHENTICATION\_ERROR             | API key is not valid, expired or revoked                             |
| UPLOAD\_ERROR                     | Recordings couldn't be uploaded after exhausting all attempts        |
| STORAGE\_FULL                     | Device storage is full which prevents saving sensor readings         |
| MISSING\_NOTIFICATION\_PERMISSION | Notification permission not granted, foreground service cannot start |
| CONFIG\_ERROR                     | Configuration error                                                  |
| TRAFFIC\_USED\_UP                 | All allotted traffic has been used                                   |
| SENSORS\_NOT\_WORKING             | Some sensors are not working. Check if permissions are missing       |

***

## Statistics API

### Upload Statistics

Monitor upload health:

```kotlin theme={null}
val uploadStats = sdk.getUploadStatistics()
if (uploadStats != null) {
    println("Successful uploads: ${uploadStats.successfulUploadsCount}")
    println("Last upload: ${uploadStats.lastSuccessfulUploadTimestamp}")
}
```

### Sensor Statistics

Get detailed sensor data quality:

```kotlin theme={null}
val sensorStats = sdk.getSensorStatistics()
sensorStats?.forEach { stat ->
    println("Sensor: ${stat.sensorName}")
    println("Configured: ${stat.configuredFrequencyHz} Hz")
    println("Actual: ${stat.actualFrequencyHz} Hz")
    println("Quality: ${stat.quality}")
}
```

Quality levels:

* **EXCELLENT**: 95-100% of configured frequency
* **GOOD**: 80-95%
* **POOR**: 50-80%
* **BAD**: less than 50%
* **UNKNOWN**: no data or not recording

***

## Best Practices

### 1. Initialization

* Always initialize in your `Application` class
* Use the application context, not activity context
* Initialize once and reuse the singleton instance

### 2. Error Handling

```kotlin theme={null}
sdk.observeSdkStatus().collect { status ->
    when (status) {
        is Status.Error -> {
            Log.e("SDK", "Error: ${status.errorCode} - ${status.message}")
        }
        is Status.ReadingsDatabaseFull -> {
            Log.w("SDK", "Storage full")
        }
        else -> {}
    }
}
```

### 3. Monitoring SDK Health

```kotlin theme={null}
fun checkSdkHealth() {
    val uploadStats = sdk.getUploadStatistics()
    if (uploadStats != null && uploadStats.successfulUploadsCount == 0) {
        Log.w("SDK", "No successful uploads yet")
    }

    val sensorStats = sdk.getSensorStatistics()
    sensorStats?.filter { it.quality == SensorDataQuality.BAD }?.forEach {
        Log.w("SDK", "Bad quality for: ${it.sensorName}")
    }
}
```

***

## API Reference

### TruemetricsSdk

| Method                                | Return Type               | Description                      |
| ------------------------------------- | ------------------------- | -------------------------------- |
| `init(context, config)`               | `TruemetricsSdk`          | Initialize SDK (static)          |
| `getInstance()`                       | `TruemetricsSdk`          | Get singleton instance (static)  |
| `startRecording()`                    | `Unit`                    | Start sensor recording           |
| `stopRecording()`                     | `Unit`                    | Stop sensor recording            |
| `isRecordingInProgress()`             | `Boolean`                 | Check if recording active        |
| `isRecordingStopped()`                | `Boolean`                 | Check if recording stopped       |
| `getRecordingStartTime()`             | `Long`                    | Get recording start timestamp    |
| `getDeviceId()`                       | `String?`                 | Get unique device identifier     |
| `logMetadata(standardMetadata)`       | `Unit`                    | Log standardized event metadata  |
| `logMetadata(payload)` *(deprecated)* | `Unit`                    | Log custom metadata map          |
| `setAllSensorsEnabled(enabled)`       | `Unit`                    | Enable/disable all sensors       |
| `getAllSensorsEnabled()`              | `Boolean`                 | Get sensor enable status         |
| `getActiveConfig()`                   | `Configuration?`          | Get active backend configuration |
| `getUploadStatistics()`               | `UploadStatistics?`       | Get upload stats                 |
| `getSensorStatistics()`               | `List<SensorStatistics>?` | Get sensor stats                 |
| `deinitialize()`                      | `Unit`                    | Shutdown SDK completely          |

### Observable Flows

| Property/Method         | Type                              | Description                    |
| ----------------------- | --------------------------------- | ------------------------------ |
| `sensorInfo`            | `StateFlow<Iterable<SensorInfo>>` | Available sensors info         |
| `sdkStatus`             | `StateFlow<Status>`               | SDK operational status         |
| `observeSdkStatus()`    | `Flow<Status>`                    | Observe SDK status changes     |
| `getActiveConfigFlow()` | `Flow<Configuration>?`            | Observe backend config changes |

### Data Classes

#### UploadStatistics

| Property                        | Type    | Description              |
| ------------------------------- | ------- | ------------------------ |
| `successfulUploadsCount`        | `Int`   | Total successful uploads |
| `lastSuccessfulUploadTimestamp` | `Long?` | Last upload timestamp    |

#### SensorStatistics

| Property                | Type                | Description               |
| ----------------------- | ------------------- | ------------------------- |
| `sensorName`            | `SensorName`        | Sensor identifier         |
| `configuredFrequencyHz` | `Float`             | Configured frequency      |
| `actualFrequencyHz`     | `Float`             | Actual measured frequency |
| `quality`               | `SensorDataQuality` | Quality assessment        |

#### SensorInfo

| Property             | Type           | Description                      |
| -------------------- | -------------- | -------------------------------- |
| `sensorName`         | `SensorName`   | Sensor identifier                |
| `sensorStatus`       | `SensorStatus` | Status: ON, OFF, or NA           |
| `frequency`          | `Float`        | Polling frequency in Hz          |
| `missingPermissions` | `List<String>` | Required permissions not granted |
