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

# Android SDK

> Integrate your native Android app via the Embedded SDK for Android.

The Android SDK has many high-level and low-level methods to interact with Plaud devices. We recommend using the high-level methods outlined on this page to handle:

1. Device Connection: Connecting to & disconnecting from Plaud devices via mobile app
2. File Management: Syncing files from Plaud device to mobile app
3. Firmware Updates: Updating Plaud device firmware

These methods should cover majority of Plaud Embedded use cases. For advanced usage, see the [advanced Android SDK usage](/plaud-embedded/advanced-android-sdk).

## Prerequisites

| Technology                 | Version          |
| -------------------------- | ---------------- |
| Android                    | 5.0+ (minSdk 21) |
| compileSdk                 | 34               |
| Java Development Kit (JDK) | 17               |

<Note>
  The SDK ships native libraries for `arm64-v8a` / `armeabi-v7a`. For testing, you must use a physical device, not an emulator.
</Note>

<Tip>
  **Try the Plaud Embedded Skill** to have your coding agent help you with your Android implementation.

  ```bash theme={"system"}
  npx skills add Plaud-AI/plaud-embedded-skills
  ```

  Visit our [GitHub repo](https://github.com/Plaud-AI/plaud-embedded-skills.git) for more details on the skill.
</Tip>

## Installation

<Steps>
  <Step title="Clone the Plaud SDK repo">
    ```bash theme={"system"}
    git clone https://github.com/Plaud-AI/plaud-sdk-public.git
    ```
  </Step>

  <Step title="Copy and import the Plaud Android SDK as a dependency">
    The Android SDK ships as a pre-built `.aar`. Copy `sdk/android/plaud-sdk.aar` into your app module's `libs/` directory and add it as a dependency:

    ```groovy build.gradle theme={"system"}
    dependencies {
        implementation files('libs/plaud-sdk.aar')
    }
    ```
  </Step>
</Steps>

***

## Getting Started

Import `PlaudDeviceAgent` from `sdk`. `PlaudDeviceAgent` is a singleton `object`, so call it directly — there is no instance to construct:

```kotlin Kotlin icon="android" theme={"system"}
import sdk.PlaudDeviceAgent

// Initialize once with your app Context, user token, and regional domain
PlaudDeviceAgent.initSDK(
    context = applicationContext,
    userAccessToken = "user-token",
    customDomain = "platform-us.plaud.ai"  // domain only, no https://
)

// Assign the global listener to receive device events
PlaudDeviceAgent.listener = object : PlaudDeviceAgentListener {
    override fun blePenState(state: Int, privacy: Int, keyState: Int, uDisk: Int) {
        // handshake complete — device is ready
    }
    // override only the callbacks you need
}
```

Use these methods and callbacks to drive device interactions between your mobile app and your users' Plaud devices.

***

## Methods

### Plaud Device SDK Initialization

The SDK is initialized with your app `Context`, a **User Token**, and your **regional** domain.

<Note>
  If you haven't onboarded to the Plaud Developer Platform, see our [quickstart onboarding steps](/plaud-embedded/quickstart#onboard-to-the-plaud-developer-platform).

  If you'd like more details on how to retrieve your User Token and your regional domain, see the [Authentication API reference](/plaud-embedded/auth-api-overview).
</Note>

```kotlin Kotlin icon="android" theme={"system"}
import sdk.PlaudDeviceAgent

PlaudDeviceAgent.initSDK(
    context = applicationContext,
    userAccessToken = "user-token",
    customDomain = "platform-us.plaud.ai"  // domain only, no https://
)
```

<ParamField path="context" type="Context" required>
  Your application `Context` (e.g. `applicationContext`).
</ParamField>

<ParamField path="userAccessToken" type="String" required>
  User Access Token (JWT), used for device authentication.
</ParamField>

<ParamField path="customDomain" type="String" required>
  Your regional Plaud server domain **without `https://` prefix**. All SDK network requests use this domain.

  For more information on how to find your regional server domain, see the [Authentication API docs](/plaud-embedded/auth-api-overview#find-your-region).
</ParamField>

### Permission Manager

BLE scanning requires runtime permissions on Android. The SDK's `sdk.permission.PermissionManager` requests the full set it needs and reports the result.

```kotlin Kotlin icon="android" theme={"system"}
import sdk.permission.PermissionManager

class ScanActivity : AppCompatActivity() {
    private val perms by lazy { PermissionManager(this) }
    private fun startScanning() {
        if (perms.hasAllPermissions()) {
            PlaudDeviceAgent.startScan()
        } else {
            perms.requestPermissions(this) { granted -> onPermissionResult(granted) }
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        perms.onRequestPermissionsResult(
            requestCode,
            arrayOf(*permissions),
            grantResults
        ) { granted -> onPermissionResult(granted) }
    }

    private fun onPermissionResult(granted: Boolean) {
        if (granted) PlaudDeviceAgent.startScan() else { /* ... */ }
    }
}
```

<ParamField path="activity" type="android.app.Activity" required>
  Android activity
</ParamField>

<ParamField path="onResult" type="(Boolean) -> Unit" required>
  Receives `true` only when every requested permission was granted.
</ParamField>

<Note>
  `PermissionManager` requests **`BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT`, `ACCESS_FINE_LOCATION` and `ACCESS_COARSE_LOCATION`** on API 31+, and `BLUETOOTH`, `BLUETOOTH_ADMIN`, `ACCESS_FINE_LOCATION`, `ACCESS_COARSE_LOCATION` on API ≤ 30. Note that it asks for location on Android 12+ as well.
</Note>

***

### Connecting (binding) to a Plaud Device

Binding a Plaud device generates a key-pair using the **user token** and creates an ownership lock on your users' Plaud device. This makes sure their files on device is always encrypted and can only be decrypted with a valid user token by your application.

Binding a device requires an API call to Plaud's cloud services, so you can track device statuses remotely. And a local bind triggered by the Embedded SDK to verify and generate keys on Plaud device.

<Steps>
  <Step title="Scan for the device">
    `.startScan()` delivers results on the `bleScanResult` callback defined in the [**PlaudDeviceAgentListener**](#plauddeviceagentlistener).

    ```kotlin Kotlin icon="android" theme={"system"}
    PlaudDeviceAgent.listener = object : PlaudDeviceAgentListener {
        override fun bleScanResult(devices: List<BleDevice>) {
            val match = devices.firstOrNull { it.serialNumber == lastSN } ?: return
            connect(match)   // see the next step
        }
        override fun bleConnectState(state: Int) { /* 1=connected, 0=disconnected, 2=failed */ }

        override fun bleBind(sn: String?, status: Int, protVersion: Int, timezone: Int) { /* status == 0 → bound */ }

        override fun blePenState(state: Int, privacy: Int, keyState: Int, uDisk: Int) { /* handshake complete */ }

        // ... recording / file-list / battery / storage callbacks
    }

    PlaudDeviceAgent.startScan()
    ```
  </Step>

  <Step title="Bind from the cloud">
    Registers the device/owner association in the Plaud registry. Re-binding a device to the same owner is idempotent, so multiple calls will not have side effects.

    ```kotlin Kotlin icon="android" theme={"system"}
    // POST https://platform-us.plaud.ai/developer/api/open/partner/sdk/bind
    val body = JSONObject()
        .put("type", snType)   // e.g. "notepro" / "notepins"
        .put("sn", sn)

    val request = Request.Builder()
        .url("https://platform-us.plaud.ai/developer/api/open/partner/sdk/bind")
        .header("Authorization", "Bearer $userAccessToken")
        .post(body.toString().toRequestBody("application/json".toMediaType()))
        .build()
    ```

    <ParamField path="type" type="String" required>
      Device type string derived from the SN prefix (`881` → `notepro`, `882` → `notepins`).
    </ParamField>

    <ParamField path="sn" type="String" required>
      Device serial number.
    </ParamField>

    <Note>
      A `403` response means the device is already bound to **another** account.
    </Note>
  </Step>

  <Step title="Bind the device over BLE">
    Connects and generates the key-pair on the device itself. The bind result is delivered on `bleBind(sn, status, protVersion, timezone)`.

    <Warning>
      **Two things must be in place before `connectBleDevice(...)`, or the secure handshake fails:**

      1. The partner RSA key pair must have arrived. `initSDK(...)` fetches it over HTTP.
      2. The device serial number must be signed and stored, with `NiceBuildSdk.signAndStoreDeviceSn(deviceType, sn)`. The BLE layer reads that signature during the pre-handshake.
    </Warning>

    ```kotlin Kotlin icon="android" theme={"system"}
    import sdk.NiceBuildSdk

    private fun connect(bleDevice: BleDevice) = lifecycleScope.launch(Dispatchers.IO) {
        val sn = bleDevice.serialNumber ?: return@launch
        val deviceType = if (sn.startsWith("881")) "notepro" else "notepins"

        // 1. Wait for the RSA key pair fetched by initSDK.
        val deadline = System.currentTimeMillis() + 10_000L
        while (!NiceBuildSdk.isPartnerDataReady() && System.currentTimeMillis() < deadline) {
            delay(200)
        }

        // 2. Sign the SN — the handshake reads the stored signature.
        if (!NiceBuildSdk.signAndStoreDeviceSn(deviceType, sn)) {
            // network unreachable or token rejected — the handshake will fail
        }

        PlaudDeviceAgent.connectBleDevice(bleDevice)
    }
    ```

    <ParamField path="bleDevice" type="BleDevice" required>
      A scanned device, as delivered by `bleScanResult`.
    </ParamField>

    <ParamField path="deviceToken" type="String">
      Optional second argument on the `connectBleDevice(bleDevice, deviceToken)` overload — a unique identifier for that device. Omit it to connect with an empty token, which is what most integrations do.

      The serial number is read from the `BleDevice` — you do not pass it here.
    </ParamField>
  </Step>
</Steps>

#### PlaudDeviceAgentListener Callbacks

| Callback                                                             | Description                                                          |
| -------------------------------------------------------------------- | -------------------------------------------------------------------- |
| `bleScanResult(devices: List<BleDevice>)`                            | Scan results updated                                                 |
| `bleConnectState(state: Int)`                                        | Connection state — `1` = connected, `0` = disconnected, `2` = failed |
| `bleBind(sn: String?, status: Int, protVersion: Int, timezone: Int)` | Device bound successfully                                            |
| `blePenState(state: Int, privacy: Int, keyState: Int, uDisk: Int)`   | Secure handshake complete                                            |

<Warning>
  Plaud devices can only be bound to one mobile application. If a user is **uninstalling your mobile app, make sure to unbind your Plaud device.**
</Warning>

### Depair (unbind) a device

Plaud devices **can only be bound to one application at a time**. This is done to properly secure and encrypt files stored on a Plaud device. When a user wants to unbind a Plaud device (whether to use with another Plaud Embedded App or the core Plaud app), unbind over both cloud and BLE.

Unbinding via the cloud allows you to track Connected Device statuses remotely and via API.

<Steps>
  <Step title="Unbind from the cloud">
    Removes the device/owner association in the Plaud registry (visible on the [Plaud developer portal](https://portal.plaud.ai/)). This does not require a BLE connection. Send an authenticated `POST` to the partner unbind endpoint on your regional domain. Unbinding an already-unbound device is an idempotent no-op, so this is safe to call best-effort.

    ```kotlin Kotlin icon="android" theme={"system"}
    // POST https://<your-domain>/developer/api/open/partner/sdk/unbind
    val body = JSONObject()
        .put("type", snType)   // e.g. "notepro" / "notepins"
        .put("sn", sn)

    val request = Request.Builder()
        .url("https://$customDomain/developer/api/open/partner/sdk/unbind")
        .header("Authorization", "Bearer $userAccessToken")
        .post(body.toString().toRequestBody("application/json".toMediaType()))
        .build()
    ```

    <ParamField path="type" type="String" required>
      Device type string derived from the SN prefix (`881` → `notepro`, `882` → `notepins`).
    </ParamField>

    <ParamField path="sn" type="String" required>
      Device serial number.
    </ParamField>
  </Step>

  <Step title="Depair the device over BLE">
    Clears the pairing/handshake on the device itself. Requires the device to be connected. On success the SDK disconnects and clears the session. The result is delivered on the `bleDepair(status: Int)` listener callback.

    ```kotlin Kotlin icon="android" theme={"system"}
    PlaudDeviceAgent.listener = object : PlaudDeviceAgentListener {
        override fun bleDepair(status: Int) {
            if (status == 0) {
                // device unpaired and disconnected
            }
        }
        // ...
    }

    PlaudDeviceAgent.depair(clear = false)
    ```

    <ParamField path="clear" type="Boolean">
      Whether to also clear the device's own stored bond list. **Pass `false` for the normal unbind flow** — this is what the [Android Starter App](/plaud-embedded/android-starter-app) does. Omit the argument to call the no-arg `depair()` overload.

      Reserve `clear = true` for recovering a device whose on-device pairing state is stale.
    </ParamField>

    <Note>
      `bleDepair` is best-effort — a device that is out of range or unresponsive never answers. Pair the callback with a timeout (a few seconds) and call `PlaudDeviceAgent.disconnect()` either way, so the BLE link is always released.
    </Note>
  </Step>
</Steps>

***

### File Synchronization

The `exportAudio` method exports audio files from the Plaud device to your users' phone, reporting progress through the [**AudioExporter.ExportCallback**](#audioexporterexportcallback).

<Note>
  Plaud devices will record **up to 5 hours**. Recordings longer should be broken up.
</Note>

The `.getFileList` accesses files on your users' Plaud device.

```kotlin Kotlin icon="android" theme={"system"}
// Get file list from device
PlaudDeviceAgent.getFileList()

PlaudDeviceAgent.exportAudio(
    sessionId = sessionId,
    outputDir = outputDir,
    format = AudioExportFormat.WAV,
    channels = 1,
    callback = object : AudioExporter.ExportCallback {
        override fun onProgress(progress: Int, message: String) { }
        override fun onComplete(outputFile: File) {
            // decoded file is ready at outputFile
        }
    }
)
```

<ParamField path="sessionId" type="Long" required>
  Session ID
</ParamField>

<ParamField path="outputDir" type="File" required>
  Output directory
</ParamField>

<ParamField path="format" type="AudioExportFormat" required>
  `PCM` | `WAV` | `OPUS` | `MP3`. We recommend `MP3` — it plays everywhere and is accepted directly by the transcription upload API.
</ParamField>

<ParamField path="channels" type="Int">
  Number of audio channels (`1` = mono). Optional — an overload without this argument exists.
</ParamField>

<ParamField path="callback" type="AudioExporter.ExportCallback" required>
  `fun onProgress(progress: Int, message: String)`

  `fun onComplete(outputFile: File)`
</ParamField>

<Note>
  For large audio files, we recommend UX considerations:

  * Progress indicators and setting expectations for long transfers (i.e. "This file is large. May take \~X minutes")
  * Supporting **background/resumable** transfer so closing the app doesn't kill the session.
  * Using WiFi Fast Transfer (see below)
</Note>

***

### WiFi Fast Transfer

An alternative to a BLE (Bluetooth Low Energy) file transfer, WiFi Fast Transfer is \~10x faster than BLE transfers.

A transfer spans two objects: `PlaudDeviceAgent` opens the hotspot, starts and ends the session, and decodes each recording; `IWifiTransferAgent` (reached with `getWifiAgent()`) lists and deletes files during the session.

<Note>
  `getWifiAgent()` returns `IWifiTransferAgent?` — it is `null` until the SDK has a WiFi agent to hand out. Always reach it with a safe call (`getWifiAgent()?.…`).
</Note>

<Steps>
  <Step title="Ask the device to open its hotspot">
    The device opens a WiFi hotspot on request over BLE. The credentials arrive on the `bleWiFiOpen` listener callback.

    ```kotlin Kotlin icon="android" theme={"system"}
    PlaudDeviceAgent.setDeviceWiFi(open = true)
    ```

    <ParamField path="open" type="Boolean" required>
      `true` opens the device hotspot, `false` closes it.
    </ParamField>
  </Step>

  <Step title="Start the transfer session">
    Start the session from the `bleWiFiOpen` callback. The SDK handles joining the hotspot and the secure handshake for you.

    ```kotlin Kotlin icon="android" theme={"system"}
    PlaudDeviceAgent.listener = object : PlaudDeviceAgentListener {
        override fun bleWiFiOpen(status: Int, ssid: String, password: String, url: String) {
            PlaudDeviceAgent.startWifiTransfer(userId, wifiCallback)
        }
        // ...
    }
    ```

    <ParamField path="userId" type="String" required>
      Identifier for the transfer session.
    </ParamField>

    <ParamField path="callback" type="IWifiTransferAgent.WifiTransferCallback" required>
      Drives the rest of the flow. See [**WifiTransferCallback**](#wifitransfercallback) below.
    </ParamField>

    Returns `false` if the session could not be opened.
  </Step>

  <Step title="Wait for READY, then list files">
    Connection state advances `NONE` → `CONNECTING` → `CONNECTED` → `HANDSHAKING` → `READY`. **No file command works before `READY`.**

    ```kotlin Kotlin icon="android" theme={"system"}
    val wifiCallback = object : IWifiTransferAgent.WifiTransferCallback {
        override fun onConnectionStateChanged(state: WifiConnectionState) {
            if (state == WifiConnectionState.READY) {
                PlaudDeviceAgent.getWifiAgent()?.getFileList()
            }
        }
        override fun onFileListReceived(files: List<WifiFileInfo>) {
            // export each session (next step)
        }
        override fun onError(code: Int, message: String) { }
        // ...
    }
    ```
  </Step>

  <Step title="Export each recording">
    `exportAudioViaWiFi` takes the same arguments as [`exportAudio`](#file-synchronization) and reports on the same `AudioExporter.ExportCallback`, so your existing export handling works unchanged over WiFi.

    ```kotlin Kotlin icon="android" theme={"system"}
    PlaudDeviceAgent.exportAudioViaWiFi(
        sessionId = file.sessionId,
        outputDir = outputDir,
        format = AudioExportFormat.MP3,
        channels = 1,
        callback = object : AudioExporter.ExportCallback {
            override fun onProgress(progress: Int, message: String) { }
            override fun onComplete(outputFile: File) {
                // decoded file is ready at outputFile
            }
        }
    )
    ```

    <Note>
      Use `exportAudioViaWiFi(...)` rather than the raw `downloadFile()` / `downloadAllFiles()` path on `IWifiTransferAgent`. Those write undecrypted `.opus` bytes straight to disk; `exportAudioViaWiFi` runs the same decode pipeline as BLE.
    </Note>
  </Step>

  <Step title="End the session">
    ```kotlin Kotlin icon="android" theme={"system"}
    PlaudDeviceAgent.endWiFiTransfer()
    ```

    <Warning>
      End the session with `endWiFiTransfer()`, not `getWifiAgent().stopWifiTransfer()`. Only `endWiFiTransfer()` also tells the device to close its hotspot over BLE — otherwise it stays open and drains the device battery.
    </Warning>
  </Step>
</Steps>

Use `PlaudDeviceAgent.isWifiTransferActive()` to check whether a session is currently open.

#### Deleting files over WiFi

`IWifiTransferAgent.deleteFiles(...)` removes several recordings in one call — the only batch delete in the SDK. The result arrives on `onFileDeleteCompleted`.

```kotlin Kotlin icon="android" theme={"system"}
PlaudDeviceAgent.getWifiAgent()?.deleteFiles(listOf(sessionId1, sessionId2))
```

<ParamField path="sessionIds" type="List<Long>" required>
  Session IDs of the recordings to delete from the device.
</ParamField>

#### WifiTransferCallback

The WiFi Fast Transfer flow is driven by the [IWifiTransferAgent.WifiTransferCallback](/plaud-embedded/android-sdk#iwifitransferagent-wifitransfercallback). All members are required — there are no default implementations.

| Callback                                                                                  | Description                                                    |
| ----------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| `onConnectionStateChanged(state: WifiConnectionState)`                                    | WiFi transfer connection state changed                         |
| `onHandshakeCompleted(info: String)`                                                      | Secure handshake completed; transfer is ready                  |
| `onFileListReceived(files: List<WifiFileInfo>)`                                           | Result of the file-list request                                |
| `onTransferProgress(sessionId: Long, progress: Int, speed: Double)`                       | Per-file transfer progress (`progress` 0–100, `speed` in KB/s) |
| `onFileTransferCompleted(sessionId: Long, path: String)`                                  | A single file finished downloading to `path`                   |
| `onBatchDownloadStarted(total: Int)`                                                      | Batch download started                                         |
| `onBatchDownloadProgress(current: Int, total: Int, filename: String)`                     | Progress across a batch download                               |
| `onBatchDownloadCompleted(success: Int, failed: Int, results: List<BatchDownloadResult>)` | Batch download finished                                        |
| `onFileDeleteCompleted(success: Boolean, deletedCount: Int, error: String?)`              | File delete finished                                           |
| `onWifiTransferStopped()`                                                                 | WiFi transfer session stopped                                  |
| `onDeviceBatteryUpdate(level: Int, charging: Boolean)`                                    | Device battery / charging status update                        |
| `onError(code: Int, message: String)`                                                     | An error occurred during the transfer                          |

<Note>
  While faster than BLE transfers, we still recommend the following UX considerations for WiFi Fast Transfer syncs:

  * Progress indicators and setting expectations for long transfers (i.e. "This file is large. May take \~X minutes")
  * Supporting **background/resumable** transfer so closing the app doesn't kill the session.
</Note>

***

### Firmware Update (OTA)

The Embedded SDK handles the entire OTA flow across three calls: version query → download → MD5 verify → CRC → BLE packet push → device restart → reconnect. Each phase reports through the [**FirmwareUpdateCallback**](#firmwareupdatecallback).

#### Check for Firmware Updates

```kotlin Kotlin icon="android" theme={"system"}
// Check for update
PlaudDeviceAgent.checkFirmwareUpdate(object : SimpleFirmwareUpdateCallback() {
    override fun onUpdateCheckResult(result: Result<FirmwareUpdateInfo>) {
        val info = result.getOrNull() ?: return
        if (!info.hasUpdate) return
        // proceed to download (below)
    }
})
```

<ParamField path="callback" type="FirmwareUpdateCallback" required>
  Callback that reports the check result as a `FirmwareUpdateInfo`.

  <Expandable title="FirmwareUpdateInfo">
    <ParamField path="hasUpdate" type="Boolean">
      Whether a firmware update is available
    </ParamField>

    <ParamField path="currentVersion" type="String">
      The device's current firmware version
    </ParamField>

    <ParamField path="isForceUpdate" type="Boolean">
      Whether the update is mandatory
    </ParamField>

    <ParamField path="versionResponse" type="DeviceVersionResponse">
      The full version-check response (latest version, download URL, MD5, release notes)
    </ParamField>
  </Expandable>
</ParamField>

#### Download Firmware Update

```kotlin Kotlin icon="android" theme={"system"}
// Download + MD5 verify
PlaudDeviceAgent.downloadFirmware(updateInfo, object : SimpleFirmwareUpdateCallback() {
    override fun onDownloadProgress(progress: UpdateProgress) {
        // progress.progress: 0 ~ 100
    }
    override fun onDownloadComplete(result: FirmwareDownloadResult) {
        if (result.success) {
            result.file?.let { /* install (below) */ }
        }
    }
})
```

<ParamField path="info" type="FirmwareUpdateInfo" required>
  The `FirmwareUpdateInfo` returned by `checkFirmwareUpdate`.
</ParamField>

<ParamField path="callback" type="FirmwareUpdateCallback" required>
  Reports download progress and completion.

  <Expandable title="UpdateProgress">
    <ParamField path="progress" type="Int">
      Progress from 0 to 100
    </ParamField>

    <ParamField path="message" type="String">
      Human-readable status message
    </ParamField>

    <ParamField path="detail" type="String">
      Additional detail for the current step
    </ParamField>

    <ParamField path="transferPhase" type="FirmwareTransferPhase">
      `TRANSFERRING` / `TRANSFER_COMPLETE_WAITING` / `DEVICE_RESTARTING` / `UPGRADE_COMPLETE` / `TRANSFER_FAILED` / `UPGRADE_FAILED`
    </ParamField>
  </Expandable>

  <Expandable title="FirmwareDownloadResult">
    <ParamField path="success" type="Boolean">
      Whether the download succeeded
    </ParamField>

    <ParamField path="file" type="File">
      The downloaded firmware file
    </ParamField>

    <ParamField path="md5Valid" type="Boolean">
      Whether the MD5 checksum verified
    </ParamField>

    <ParamField path="error" type="String">
      Error description if the download failed
    </ParamField>
  </Expandable>
</ParamField>

#### Install Firmware Update

```kotlin Kotlin icon="android" theme={"system"}
PlaudDeviceAgent.installFirmware(file, updateInfo, object : SimpleFirmwareUpdateCallback() {
    override fun onInstallProgress(progress: UpdateProgress) { }
    override fun onInstallComplete(result: FirmwareInstallResult) {
        if (result.success) {
            // device restarts and reconnects
        }
    }
})
```

<ParamField path="file" type="File" required>
  The firmware file from `FirmwareDownloadResult.file`
</ParamField>

<ParamField path="info" type="FirmwareUpdateInfo" required>
  The `FirmwareUpdateInfo` returned by `checkFirmwareUpdate`
</ParamField>

<ParamField path="callback" type="FirmwareUpdateCallback" required>
  Reports install progress and completion.

  <Expandable title="FirmwareInstallResult">
    <ParamField path="success" type="Boolean">
      Whether the install succeeded
    </ParamField>

    <ParamField path="error" type="String">
      Error description if the install failed
    </ParamField>
  </Expandable>
</ParamField>

***

## Interfaces and Callbacks

The Embedded SDK is callback-driven. Device events flow through a single global listener you assign to `PlaudDeviceAgent.listener`, while per-operation flows (audio export, WiFi transfer, firmware) take their own callback interface. These four cover most use cases.

<Note>
  Callbacks are delivered on the SDK's internal threads — **not** the main thread. Marshal to the main thread (e.g. `runOnUiThread { }` / a `Handler`) before touching UI or view state.
</Note>

### PlaudDeviceAgentListener

The primary listener for `PlaudDeviceAgent`. Assign it once to `PlaudDeviceAgent.listener` and it drives the entire BLE lifecycle — scan, connect, bind, device state, recording, and file sync. A single global listener receives every device event; all methods return `Unit`.

```kotlin Kotlin icon="android" theme={"system"}
PlaudDeviceAgent.listener = object : PlaudDeviceAgentListener {
    override fun blePenState(state: Int, privacy: Int, keyState: Int, uDisk: Int) {
        // handshake complete — device is ready
    }
    // override only the callbacks you need
}
```

| Group           | Callback                                                                                              | Description                                                     |
| --------------- | ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| Connection      | `bleScanResult(devices: List<BleDevice>)`                                                             | Scan results updated                                            |
| Connection      | `bleScanOverTime()`                                                                                   | Scan window elapsed                                             |
| Connection      | `bleConnectState(state: Int)`                                                                         | `1` = connected, `0` = disconnected, `2` = failed               |
| Connection      | `bleBind(sn: String?, status: Int, protVersion: Int, timezone: Int)`                                  | Binding result — `status == 0` → bound OK                       |
| Connection      | `bleDepair(status: Int)`                                                                              | Unpair / de-bind result                                         |
| Connection      | `blePenState(state: Int, privacy: Int, keyState: Int, uDisk: Int)`                                    | Handshake / device state reported after the handshake completes |
| Connection      | `bleDeviceName(name: String)`                                                                         | Device name read / changed                                      |
| Power & storage | `blePowerChange(power: Int, oldPower: Int)`                                                           | Battery % changed                                               |
| Power & storage | `bleChargingState(isCharging: Boolean, level: Int)`                                                   | Charging state + battery level                                  |
| Power & storage | `bleStorage(total: Long, free: Long, duration: Long)`                                                 | Bytes total/free + recordable seconds                           |
| Power & storage | `bleMicGain(gain: Int)`                                                                               | Mic gain read                                                   |
| Recording       | `bleRecordStart(sessionId: Long, start: Long, status: Int, scene: Int, startTime: Long, reason: Int)` | Recording started                                               |
| Recording       | `bleRecordStop(sessionId: Long, reason: Int, fileExist: Boolean, fileSize: Long)`                     | Recording stopped                                               |
| Recording       | `bleRecordPause(sessionId: Long, reason: Int, fileExist: Boolean, fileSize: Long)`                    | Recording paused                                                |
| Recording       | `bleRecordResume(sessionId: Long, start: Long, status: Int, scene: Int, startTime: Long)`             | Recording resumed                                               |
| File sync       | `bleFileList(files: List<BleFile>)`                                                                   | Result of `getFileList(...)`                                    |
| File sync       | `bleSyncFileHead(sessionId: Long, status: Int)`                                                       | Sync started for a file                                         |
| File sync       | `bleSyncFileTail(sessionId: Long, status: Int)`                                                       | Sync finished for a file                                        |
| File sync       | `bleData(sessionId: Long, timestamp: Long, data: ByteArray)`                                          | A chunk of streaming audio/PCM bytes                            |
| File sync       | `bleDataComplete()`                                                                                   | Data stream complete                                            |
| File sync       | `bleSyncFileStop()`                                                                                   | Sync stopped                                                    |
| File sync       | `bleDeleteFile(sessionId: Long, status: Int)`                                                         | Result of `deleteFile(...)`                                     |
| WiFi            | `bleWiFiOpen(status: Int, ssid: String, password: String, url: String)`                               | Device opened its hotspot                                       |
| OTA             | `bleFotaResult(sessionId: Long, status: Int, message: String)`                                        | Firmware push result                                            |

<Note>
  The listener also delivers the WiFi auto-sync configuration results (`onWifiSyncEnabled`, `onWifiSyncListReceived`, `onWifiSyncConfigReceived`, `onWifiSyncConfigSet`, `onWifiSyncDeleteResult`, `onWifiSyncTestStarted`, `onWifiSyncTestResult`). These configure the device's own scheduled background upload and are distinct from WiFi Fast Transfer.
</Note>

### AudioExporter.ExportCallback

Reports progress, completion, and errors for `exportAudio(...)` (BLE) and `exportAudioViaWiFi(...)` (WiFi).

```kotlin Kotlin icon="android" theme={"system"}
val callback = object : AudioExporter.ExportCallback {
    override fun onProgress(progress: Int, message: String) {
        // progress: 0–100 (download bytes)
    }
    override fun onComplete(outputFile: File) {
        // decoded file is ready at outputFile
    }
    override fun onError(error: String) {
        // export failed
    }
    override fun onStageChanged(stage: ExportStage) {
        // optional — DOWNLOADING → TRANSCODING
    }
}
```

<ParamField path="onProgress(progress: Int, message: String)" type="fun" required>
  Export progress, `0`–`100` (download bytes), with a human-readable status message.
</ParamField>

<ParamField path="onComplete(outputFile: File)" type="fun" required>
  Called when decoding finishes; `outputFile` is the written file.
</ParamField>

<ParamField path="onError(error: String)" type="fun" required>
  Called if export fails, with a description of the error.
</ParamField>

<ParamField path="onStageChanged(stage: ExportStage)" type="fun">
  Optional (has a default implementation). Language-neutral phase signal: `DOWNLOADING` (bytes still transferring off the device) → `TRANSCODING` (only local decode/encode remains).
</ParamField>

### IWifiTransferAgent.WifiTransferCallback

WiFi Fast Transfer is split across both high-level interfaces, so you will use `IWifiTransferAgent` directly for any fast transfer. `PlaudDeviceAgent` owns the session lifecycle and the audio pipeline; `IWifiTransferAgent` owns the file operations and the callback you implement to drive them.

```kotlin Kotlin icon="android" theme={"system"}
val wifi: IWifiTransferAgent? = PlaudDeviceAgent.getWifiAgent()
```

#### WiFi Fast Transfer

| Functionality                   | Method                                                             |
| ------------------------------- | ------------------------------------------------------------------ |
| Open the device hotspot         | `PlaudDeviceAgent.setDeviceWiFi(open = true)`                      |
| Start the session               | `PlaudDeviceAgent.startWifiTransfer(userId, callback)`             |
| List files on the device        | `IWifiTransferAgent.getFileList()`                                 |
| Download + decode one recording | `PlaudDeviceAgent.exportAudioViaWiFi(...)`                         |
| Delete recordings in a batch    | `IWifiTransferAgent.deleteFiles(sessionIds)`                       |
| Check session state             | `IWifiTransferAgent.getConnectionState()` / `checkPrerequisites()` |
| End the session                 | `PlaudDeviceAgent.endWiFiTransfer()`                               |

The typical flow: open the hotspot over BLE, start the session, wait for `READY`, list files, then export each one.

#### WifiTransferCallback

**12 required members** for Wifi Transfers, generally driven by `PlaudDeviceAgent.exportAudioViaWiFi`.

| Callback                                              | Description                                          |
| ----------------------------------------------------- | ---------------------------------------------------- |
| `onConnectionStateChanged(state)`                     | `READY` = handshake complete, safe to issue commands |
| `onHandshakeCompleted(info)`                          | Handshake detail string                              |
| `onFileListReceived(files)`                           | Result of `getFileList()`, as `WifiFileInfo`         |
| `onTransferProgress(sessionId, progress, speed)`      | `progress` 0–100, `speed` in KB/s                    |
| `onFileTransferCompleted(sessionId, path)`            | A single file finished                               |
| `onBatchDownloadStarted(total)`                       | `downloadAllFiles()` began                           |
| `onBatchDownloadProgress(current, total, filename)`   | Batch position                                       |
| `onBatchDownloadCompleted(success, failed, results)`  | Batch finished, with per-file `BatchDownloadResult`  |
| `onFileDeleteCompleted(success, deletedCount, error)` | Result of `deleteFiles(...)`                         |
| `onDeviceBatteryUpdate(level, charging)`              | Device battery over the WiFi session                 |
| `onWifiTransferStopped()`                             | Session ended                                        |
| `onError(code, message)`                              | A command failed                                     |

`WifiFileInfo` carries `sessionId`, `fileName`, `fileSize`, `duration` (ms), `timestamp` (epoch seconds), and `scene` — more per-file metadata than `BleFile` exposes over BLE.

### FirmwareUpdateCallback

Reports each phase of the OTA flow across `checkFirmwareUpdate(...)`, `downloadFirmware(...)`, and `installFirmware(...)`. Extend `SimpleFirmwareUpdateCallback()` to override only the methods you need — each has an empty default implementation.

```kotlin Kotlin icon="android" theme={"system"}
val callback = object : SimpleFirmwareUpdateCallback() {
    override fun onUpdateCheckResult(result: Result<FirmwareUpdateInfo>) {
        val info = result.getOrNull() ?: return
        if (info.hasUpdate) { /* proceed to download */ }
    }
    override fun onDownloadProgress(progress: UpdateProgress) { /* progress.progress: 0–100 */ }
    override fun onDownloadComplete(result: FirmwareDownloadResult) {
        if (result.success) { result.file?.let { /* install */ } }
    }
    override fun onInstallProgress(progress: UpdateProgress) { }
    override fun onInstallComplete(result: FirmwareInstallResult) {
        if (result.success) { /* device restarts and reconnects */ }
    }
}
```

| Callback                                                  | Description                                                                                                                       |
| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `onUpdateCheckResult(result: Result<FirmwareUpdateInfo>)` | Version-check result. `FirmwareUpdateInfo` carries `hasUpdate`, `currentVersion`, `isForceUpdate`, and the full `versionResponse` |
| `onDownloadProgress(progress: UpdateProgress)`            | Download progress (`progress` 0–100, plus `message`, `detail`, `transferPhase`)                                                   |
| `onDownloadComplete(result: FirmwareDownloadResult)`      | Download + MD5 verify finished (`success`, `file`, `md5Valid`, `error`)                                                           |
| `onInstallProgress(progress: UpdateProgress)`             | Install progress (BLE packet push + device restart)                                                                               |
| `onInstallComplete(result: FirmwareInstallResult)`        | Install finished (`success`, `error`)                                                                                             |
