> ## 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.

# Low-Level Android SDK Methods

> Low-level Embedded SDK methods for advanced use cases

## High-level Facades

The Plaud Embedded Android SDK has two high-level interfaces:

1. **PlaudDeviceAgent**: a Kotlin `object` you call statically
2. **IWifiTransferAgent**: the WiFi Fast Transfer session and file operations

These high-level methods and callbacks should cover most use cases including:

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

See our [Android SDK reference](/plaud-embedded/android-sdk) for examples and documentation on these high-level facades.

```
  Your app
     │
     ├── PlaudDeviceAgent ─────┬───────────────┐   (High-level)
     ├── IWifiTransferAgent ───┤               │
     │                         ▼               ▼
     │                   NiceBuildSdk    FirmwareUpdate
     │                   (low-level)        Manager
     │                         │               │
     │                         ▼               │
     │                    IBleAgent ◄──────────┘
     │                    (low-level)
     └─────────────────────────┴───────────────────► device
```

Additionally, using PlaudDeviceAgent abstracts many details that low-level interfaces will not handle.

| Managed Behavior            | Purpose                                                                                                |
| --------------------------- | ------------------------------------------------------------------------------------------------------ |
| Response management         | One listener to handle different response objects (`StorageRsp`, `GetStateRsp`, …)                     |
| Callback plumbing           | Supplies the `OnRequest` / `OnResponse` pairs every `IBleAgent` call requires                          |
| Audio pipeline              | `exportAudio` handles download, caching, E2EE decrypt, and transcode to `mp3` / `wav` / `opus` / `pcm` |
| Firmware orchestration      | Version query, MD5 verify, CRC, BLE packet push, and reconnect after restart                           |
| Optional listener callbacks | `PlaudDeviceAgentListener` has **0** required members — override only what you need                    |

<Note>
  You can mix the high-level and low-level facades. For example if there's a behavior that `PlaudDeviceAgent` does not include, you can use `IBleAgent` to cover that functionality.
</Note>

## Low-level Facades

The Android SDK has a few lower-level interfaces that `PlaudDeviceAgent` wraps over:

1. **IBleAgent**: the BLE protocol library
2. **NiceBuildSdk**: helper SDK for crypto and cloud API calls
3. **PartnerApiManager**: manages the backend endpoints for device signing and RSA keypair generation

**We do not recommend using these low-level facades as main drivers. Use them as coverage for functionality not exposed by PlaudDeviceAgent**.

***

### IBleAgent Reference

`IBleAgent` is the raw BLE transport, reached through the protocol library's `TntAgent` singleton:

```kotlin Kotlin icon="android" theme={"system"}
import com.tinnotech.penblesdk.TntAgent
import com.tinnotech.penblesdk.core.IBleAgent

val ble: IBleAgent = TntAgent.getInstant().bleAgent
```

It exposes **165 methods** — roughly four times the facade's surface — so most device features the SDK supports but `PlaudDeviceAgent` doesn't expose are reachable here.

#### The callback convention

Every `IBleAgent` command takes an `OnRequest` / `OnResponse` pair instead of reporting to a global listener.

```kotlin Kotlin icon="android" theme={"system"}
ble.getStorage(
    object : AgentCallback.OnRequest {
        override fun onRequestResult(success: Boolean, code: Int, msg: String?) {
            // the command reached the device (or didn't)
        }
    },
    object : AgentCallback.OnResponse<StorageRsp> {
        override fun onResponse(rsp: StorageRsp) {
            // the device replied — typed response object, not primitives
        }
    }
)
```

<Note>
  `OnRequest` fires when the command is written to the device; `OnResponse` fires when the device answers. A successful `OnRequest` with no `OnResponse` means the device accepted the command but never replied.
</Note>

#### Device Connection

| `PlaudDeviceAgent`                         | `IBleAgent`                                                     |
| ------------------------------------------ | --------------------------------------------------------------- |
| `startScan()`                              | `scanBle(true)`                                                 |
| `stopScan()`                               | `scanBle(false)`                                                |
| `isConnected()`                            | `isBtConnected()`                                               |
| `connectBleDevice(bleDevice, deviceToken)` | `connectionBLE(device, handshakeToken, deviceToken, "", false)` |
| `disconnect()`                             | `disconnectBle()`                                               |
| `depair(clear)`                            | `depair(clear, onRequest, onResponse)`                          |
| `getState()`                               | `getState(onRequest, onResponse)`                               |

```kotlin Kotlin icon="android" theme={"system"}
val ble = TntAgent.getInstant().bleAgent

TntAgent.getInstant().addBleAgentListeners(myBleAgentListener)
ble.scanBle(true)

// From scanBleDeviceReceiver(...) on your BleAgentListener:
ble.connectionBLE(
    device,                                          // the scanned BleDevice
    NiceBuildSdk.resolveHandshakeToken(userJwt),     // handshake token from the JWT `sub`
    deviceToken,                                     // your per-device token
    "",                                              // userName
    false                                            // isForceClear
)
```

<ParamField path="device" type="BleDevice" required>
  The device to connect to, as delivered by `scanBleDeviceReceiver(...)`.
</ParamField>

<ParamField path="handshakeToken" type="String" required>
  Parsed from the user JWT's `sub` claim. `NiceBuildSdk.resolveHandshakeToken(...)` does this for you — the facade calls it internally.
</ParamField>

<ParamField path="deviceToken" type="String" required>
  Per-device token. This is the value `PlaudDeviceAgent.connectBleDevice(bleDevice, deviceToken)` forwards; pass `""` for the no-token overload.
</ParamField>

<ParamField path="userName" type="String" required>
  Owner name written to the device. The facade passes `""` (iOS passes `"Plaud"`).
</ParamField>

<ParamField path="isForceClear" type="Boolean" required>
  Clear any existing pairing before connecting. The facade passes `false`.
</ParamField>

#### Recording & Device State

```kotlin Kotlin icon="android" theme={"system"}
// Recording — each takes OnRequest/OnResponse callbacks
ble.startRecord(scene, onRequest, onResponse)
ble.stopRecord(onRequest, onResponse)
ble.recordPause(sessionId, onRequest, onResponse)
ble.recordResume(sessionId, onRequest, onResponse)
ble.startImmediateRecord(...); ble.stopImmediateRecord(...)
ble.startSyncRecord(...)

// State reads
ble.getState(...); ble.getStorage(...); ble.getDeviceStatus(...)
ble.getBatteryLevel(...); ble.isCharging(); ble.getBattStatus(...)
ble.getSerialNumber(...); ble.getSsn(...); ble.getMacAddress()
ble.getCurrentConnectedDevice(); ble.getCurrentConnectedBleMac()
ble.isRecording(); ble.isScanningBle(); ble.isSupportWifi(); ble.isUsbState()
```

`IBleAgent` also exposes the full device-settings surface the facade only partially wraps. Each setting is a `get…` / `set…` pair.

<Accordion title="Device settings available only on IBleAgent">
  ```kotlin Kotlin icon="android" theme={"system"}
  // Backlight
  getBackLightTime()        / setBackLightTime(...)
  getBackLightBrightness()  / setBackLightBrightness(...)

  // Language, scene & mode
  getLanguage()             / setLanguage(...)
  getRecScene()             / setRecScene(...)
  getRecMode()              / setRecMode(...)
  openVAD(...)
  getVadSensitivity()       / setVadSensitivity(...)

  // Gains & audio
  getVpuGain()              / setVpuGain(...)
  getMICGain()              / setMICGain(...)
  getVpuCLK()               / setVpuCLK(...)
  getBatteryMode()          / setBatteryMode(...)
  getAudioChannel()
  isOggAudio(); isNoNsAgc()

  // Automation toggles
  getAutoRecord()             / setAutoRecord(...)
  getAutoStopRecord()         / setAutoStopRecord(...)
  getAutoPowerOff()           / setAutoPowerOff(...)
  getAutoSync()               / setAutoSync(...)
  getAutoDeleteRecordFile()   / setAutoDeleteRecordFile(...)
  getSaveRAWFile()            / setSaveRAWFile(...)
  getFindMyState()            / setFindMyState(...)
  getIBeaconWakeup()          / setIBeaconWakeup(...)
  getSwitchHandlerId()        / setSwitchHandlerId(...)

  // Naming, LED, alarm, privacy
  getBleDeviceName()  / setBleDeviceName(...)
  getLedStatus()      / setLedStatus(...)
  getAlarmRec()       / setAlarmRec(...)
  setPrivacy(...);  setActive(...);  restoreFactorySettings(...)

  // Generic channel for settings without a dedicated method
  getCommonParams(...) / setCommonParams(...) / commonSettings(...)

  // Device logs, markings, WiFi credentials, websocket, misc
  getDeviceLogFileList(...); startSyncDeviceLogFile(...); stopSyncDeviceLogFile(...)
  deleteDeviceLogFile(...)
  getRecMarkings(...); getRecTags(...)
  getWifiList(...); getWifiInfo(...); setSyncWifi(...); deleteWifiInfo(...)
  testWifiInfo(...); testWifiResult(...); getRouterSsid(...) / setRouterSsid(...)
  getWebsocket(...) / setWebsocket(...); setWebsocketUrl(...); testWebsocket(...)
  getSDFlashCID(...); resetPassword(...); resetFindMy(...)
  setSoundPlusToken(...); syncTime(...); bleRateTest(...)
  setMtu(...); setPriorityHigh(); setPriorityLow(); updateRssi(...)
  ```
</Accordion>

#### File Sync

```kotlin Kotlin icon="android" theme={"system"}
ble.getRecSessions(startSessionId, onRequest, onResponse)
ble.syncFileStart(sessionId, start, end, onRequest, onHeadRsp, onTailRsp, voiceDataCollector)
ble.syncFileStop(onRequest, onResponse)
ble.syncFileDel(sessionId, onRequest, onResponse)
ble.clearRecordFile(onRequest, onResponse)
ble.fileDataCheck(...)
```

<ParamField path="startSessionId" type="Long" required>
  The session ID to start listing from. `0` lists everything.
</ParamField>

<ParamField path="sessionId" type="Long" required>
  The recording to sync or delete.
</ParamField>

<ParamField path="start" type="Long" required>
  Start byte offset. Use `0` for the whole file, or resume from a prior offset.
</ParamField>

<ParamField path="end" type="Long" required>
  End byte offset. `0` transfers to the end of the file.
</ParamField>

<ParamField path="voiceDataCollector" type="ISyncVoiceDataKeepOut" required>
  Receives streamed audio bytes. Build one with `VoiceDataCreatorFactory.newOriginalData()` to write raw device bytes to a file. `PlaudDeviceAgent` handles this.
</ParamField>

There is no format conversion and no decryption at this layer — the collector receives exactly what the device sends. `PlaudDeviceAgent.exportAudio(...)` handles this conversion and decryption.

#### Firmware Push

```kotlin Kotlin icon="android" theme={"system"}
ble.appFotaPush(filePath, fromVersion, toVersion, thirdVersion, otaPushListener)
ble.isFotaPushing()
ble.interruptFotaPush()
ble.getSyncOtaFileInfo(onRequest, onResponse)
ble.appGetThirdVersion(...)
```

`FirmwareUpdateManager` composes the full flow on top of this from querying the version to MD5 verification, CRC, and the post-restart reconnect.

<Note>
  Android's firmware version check queries `GET /api/sdk/latest-version` with an `Authorization` header, while iOS uses the partner endpoint with `X-Device-Signature`.
</Note>

#### BleAgentListener

`IBleAgent`'s listener is registered on `TntAgent`:

```kotlin Kotlin icon="android" theme={"system"}
TntAgent.getInstant().addBleAgentListeners(listener)
TntAgent.getInstant().removeBleAgentListeners(listener)
```

| Interface                  | Members | Required |
| -------------------------- | ------- | -------- |
| `BleAgentListener`         | 29      | **27**   |
| `PlaudDeviceAgentListener` | 31      | **0**    |

<Warning>
  Conforming to `BleAgentListener` means implementing 27 members, and it hands you undocumented protocol response types instead of primitives. Unless you need an event the facade doesn't re-emit, keep your listener on `PlaudDeviceAgentListener`.
</Warning>

| `BleAgentListener`                                        | `PlaudDeviceAgentListener`                                           |
| --------------------------------------------------------- | -------------------------------------------------------------------- |
| `scanBleDeviceReceiver(BleDevice)` — one device at a time | `bleScanResult(List<BleDevice>)` — accumulated list                  |
| `deviceStatusRsp(String, GetStateRsp)`                    | `blePenState(state, privacy, keyState, uDisk)`                       |
| `deviceOpStorageRsp(String, StorageRsp)`                  | `bleStorage(total, free, duration)`                                  |
| `deviceOpRecordStart(String, RecordStartRsp)`             | `bleRecordStart(sessionId, start, status, scene, startTime, reason)` |
| `deviceFotaResult(String, AppFotaPushRsp)`                | `bleFotaResult(sessionId, status, message)`                          |
| `batteryLevelUpdate` **+** `chargingStatusChange`         | `bleChargingState(isCharging, level)` — merged                       |

Events available only on `BleAgentListener`:

| Group             | Callbacks                                                                                                                    |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| Connection detail | `btStatusChange`, `bleConnectFail`, `bleConnectStage` *(default)*, `scanFail`, `handshakeWaitSure`, `sendMoreFailDisconnect` |
| Link quality      | `rssiChange`, `mtuChange`                                                                                                    |
| Sensors           | `stickAngles`, `motorStatus`, `deviceNewFeature`, `deviceStatusDetailed`                                                     |
| Device logs       | `deviceLogSyncData`, `deviceLogSyncStop`, `deviceLogSyncEnd`                                                                 |
| WiFi / OTA detail | `deviceWifiSyncStartRsp`, `deviceSwitchWifiMode`, `deviceFotaThirdVersion`                                                   |
| Settings          | `commonSettingNotify` *(default)*                                                                                            |

***

### PartnerApiManager

`PartnerApiManager` wraps the partner authentication endpoints. Like `IBleAgent`, reach it via the lower facade:

```kotlin Kotlin icon="android" theme={"system"}
val api = NiceBuildSdk.getPartnerApiManager()
```

```kotlin Kotlin icon="android" theme={"system"}
class PartnerApiManager {                       // sdk.network.manager
    fun setUserAccessToken(token: String)
    fun getUserAccessToken(): String
    fun hasUserAccessToken(): Boolean
    fun updateBaseUrl(url: String)
    fun clearToken()

    // → POST …/open/partner/sdk/sn-sign
    suspend fun signDeviceSn(deviceType: String, sn: String): SnSignResponse?

    // → POST …/open/partner/sdk/gen-key
    suspend fun generateRsaKeyPair(): GenKeyResponse?

    // → POST …/open/partner/sdk/metadata
    suspend fun postDeviceMetadata(signature: String, request: MetadataRequest): Boolean
}
```

#### Device Security

Partner endpoints for device authentication:

| Endpoint        | Method | Auth header                          | Driven by                              |
| --------------- | ------ | ------------------------------------ | -------------------------------------- |
| `…/sdk/gen-key` | `POST` | `Authorization: Bearer <user token>` | `PartnerApiManager.generateRsaKeyPair` |
| `…/sdk/sn-sign` | `POST` | `Authorization: Bearer <user token>` | `PartnerApiManager.signDeviceSn`       |

<Steps>
  <Step title="Wait for the RSA key pair — gen-key">
    `initSDK(...)` fetches the key pair asynchronously. Poll `isPartnerDataReady()` before connecting.

    ```kotlin Kotlin icon="android" theme={"system"}
    val deadline = System.currentTimeMillis() + 10_000L
    while (!NiceBuildSdk.isPartnerDataReady() && System.currentTimeMillis() < deadline) {
        delay(200)
    }
    ```

    Or, without a coroutine, use the callback form:

    ```kotlin Kotlin icon="android" theme={"system"}
    NiceBuildSdk.ensurePartnerDataReady { ready -> /* … */ }
    ```
  </Step>

  <Step title="Sign the device SN — sn-sign">
    `signAndStoreDeviceSn(...)` signs the SN and writes the result where the BLE layer reads it during the pre-handshake.

    ```kotlin Kotlin icon="android" theme={"system"}
    val signed = NiceBuildSdk.signAndStoreDeviceSn(deviceType, sn)   // suspend
    // or: NiceBuildSdk.signDeviceSnAsync(deviceType, sn) { ok -> /* … */ }
    ```

    <ParamField path="deviceType" type="String" required>
      Device family, derived from the first three characters of the serial number.

      | SN prefix | `deviceType` |
      | --------- | ------------ |
      | `881`     | `notepro`    |
      | `882`     | `notepins`   |
    </ParamField>

    <ParamField path="sn" type="String" required>
      Device serial number, from `BleDevice.getSerialNumber()`.
    </ParamField>
  </Step>

  <Step title="Connect">
    ```kotlin Kotlin icon="android" theme={"system"}
    PlaudDeviceAgent.connectBleDevice(bleDevice)
    ```
  </Step>
</Steps>

<Warning>
  The SN signature is stored **in memory only** — there is no Keystore-backed cache and no persistence across process death. Every fresh launch needs a live `sn-sign` call before its first connect, so the device requires network reachability at that moment. Re-sign as a fallback whenever the stored signature is missing.
</Warning>

The RSA key pair itself *is* persisted, wrapped by an AES master key in the Android Keystore, and is what feeds audio E2EE decryption:

```kotlin Kotlin icon="android" theme={"system"}
val privateKeyPem: String = NiceBuildSdk.getSecurePrivateKey()
NiceBuildSdk.clearPartnerData()   // drop the stored partner data
```

<Note>
  `NiceBuildSdk.bindDevice(...)` / `unbindDevice(...)` are **not** the cloud bind documented in the [Android SDK reference](/plaud-embedded/android-sdk). They post to a different service (`/api/devices/bind`) with a different credential. For the documented flow, call `developer/api/open/partner/sdk/bind` directly with the user token.
</Note>
