Skip to main content

Installation

Add the Plaud SDK to your Android project by including the following steps:
1

Add the ARR file

Add the provided plaud-sdk.aar file under {your_project}/app/libs/
your-project/
│── app/
│   │── libs/
│      │── plaud-sdk.aar   // Place SDK AAR here
│   │── src/
│   │── build.gradle
2

Add dependency and plugin

Add dependencies and plugins in your app-level build.gradle file:
build.gradle.kts
dependencies {
    // Plaud SDK(Android)
    implementation(name: "plaud-sdk", ext: "aar")

    // Required dependencies
    implementation "com.google.guava:guava:28.2-android"
    implementation "androidx.navigation:navigation-fragment-ktx:2.7.7"
    implementation "androidx.navigation:navigation-ui-ktx:2.7.7"
    implementation "com.squareup.retrofit2:retrofit:2.9.0"
    implementation "com.squareup.retrofit2:converter-gson:2.9.0"
    implementation "com.squareup.okhttp3:okhttp:4.10.0"
    implementation "com.squareup.okhttp3:logging-interceptor:4.10.0"
}

plugins {
    id 'com.android.application'
    id 'kotlin-android'
}

Requirements

  • Supported Versions: The SDK requires a minimum of Android 5.0 (API 21) and is built against Android 14 (API 34).
  • Internet: Required for making API calls to Plaud Cloud.
  • Bluetooth: Essential for device scanning, connection, and communication.
  • Network Security: Configuration must allow secure HTTPS traffic for all API communication.

Setup

Manifest Configuration

Add the following permissions to your AndroidManifest.xml:
<!-- Bluetooth permissions -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

<!-- For Android 12+ (API 31+) -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

<!-- Location permissions (required for BLE scanning) -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

<!-- Network permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />

<!-- Storage permissions -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

<!-- For Android 13+ (API 33+) media access -->
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />

<!-- Wake lock for background operations -->
<uses-permission android:name="android.permission.WAKE_LOCK" />

<!-- Foreground service (if needed) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

Runtime Permissions

For Android 12 (API level 31) and higher, Bluetooth permissions have changed, requiring runtime requests for BLUETOOTH_SCAN and BLUETOOTH_CONNECT. The following helper class demonstrates how to handle permissions for both newer and older Android versions:
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat

class PermissionManager(private val context: Context) {
    companion object {
        private val BLUETOOTH_PERMISSIONS = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            arrayOf(
                Manifest.permission.BLUETOOTH_SCAN,
                Manifest.permission.BLUETOOTH_CONNECT,
                Manifest.permission.ACCESS_FINE_LOCATION,
                Manifest.permission.ACCESS_COARSE_LOCATION,
            )
        } else {
            arrayOf(
                Manifest.permission.BLUETOOTH,
                Manifest.permission.BLUETOOTH_ADMIN,
                Manifest.permission.ACCESS_FINE_LOCATION,
                Manifest.permission.ACCESS_COARSE_LOCATION,
            )
        }
    }

    fun hasAllPermissions(): Boolean {
        return BLUETOOTH_PERMISSIONS.all { permission ->
            ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
        }
    }

    fun requestPermissions(activity: Activity) {
        ActivityCompat.requestPermissions(activity, BLUETOOTH_PERMISSIONS, 1001)
    }
}

Network Security Configuration

For apps targeting Android 9 (API level 28) or higher, ensure your network security configuration allows clear text traffic if needed:
<!-- In AndroidManifest.xml -->
<application
    android:networkSecurityConfig="@xml/network_security_config"
    ... >
<!-- res/xml/network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">your-api-domain.com</domain>
    </domain-config>
</network-security-config>

Start Development

Prerequisites

  • Android Studio: Arctic Fox or newer recommended
  • Kotlin: 1.6.21 or newer
  • Java: 8 or newer
  • Gradle: 7.3.1 or newer

Initialization

Initialize the Plaud SDK in your Application class or main activity.

Parameters

ParameterTypeRequiredDescription
contextContextApplication context
appKeyStringYour application key from developer platform
appSecretStringYour application secret from developer platform
bleAgentListenerBleAgentListenerCallback listener for BLE events
hostNameStringYour app identifier (e.g., “MyCompany Demo App”)
extraMapOptional extra parameters
customDomainStringCustom API domain URL
partnerTokenStringUser-level access token from Partner API
Device-specific authentication:
  • NotePro / NotePins: Use partnerToken for authentication. For appKey/appSecret, you can pass any non-empty placeholder value (e.g., “placeholder”)
  • Note / NotePin: Depends on firmware version. Newer firmware supports partnerToken, older firmware requires appKey + appSecret
About partnerToken: The partnerToken is a user-level access token obtained from the Partner API. Each token is associated with a specific end-user in your application.
import android.content.Context
import android.util.Log
import sdk.NiceBuildSdk
import sdk.penblesdk.TntAgent
import sdk.penblesdk.entity.BleDevice
import sdk.penblesdk.entity.BluetoothStatus
import sdk.penblesdk.impl.ble.BleAgentListener
import sdk.penblesdk.Constants
import sdk.penblesdk.entity.bean.ble.response.RecordStartRsp
import sdk.penblesdk.entity.bean.ble.response.RecordStopRsp

val bleAgentListener = object : BleAgentListener {
    override fun scanBleDeviceReceiver(device: BleDevice) {
        // Handle scanned device - add device to your UI list
        Log.d("BLE", "Found device: ${device.serialNumber}")
    }
    
    override fun btStatusChange(sn: String?, status: BluetoothStatus) {
        // Handle Bluetooth status changes - update connection UI
        when (status) {
            BluetoothStatus.CONNECTED -> Log.d("BLE", "Device connected")
            BluetoothStatus.DISCONNECTED -> Log.d("BLE", "Device disconnected")
            else -> Log.d("BLE", "Status changed: $status")
        }
    }
    
    override fun bleConnectFail(sn: String?, reason: Constants.ConnectBleFailed) {
        // Handle connection failures - show error message to user
        Log.e("BLE", "Connection failed: $reason")
    }
    
    override fun handshakeWaitSure(sn: String?, param: Long) {
        // Device handshake confirmation
        Log.d("BLE", "Handshake confirmation required")
    }
    
    override fun deviceOpRecordStart(sn: String?, response: RecordStartRsp) {
        // Device started recording
        Log.d("BLE", "Recording started: ${response.sessionId}")
    }
    
    override fun deviceOpRecordStop(sn: String?, response: RecordStopRsp) {
        // Device stopped recording
        Log.d("BLE", "Recording stopped: ${response.sessionId}")
    }
    
    override fun batteryLevelUpdate(sn: String?, level: Int) {
        // Battery level changes
        Log.d("BLE", "Battery level: $level%")
    }
    
    override fun rssiChange(sn: String?, rssi: Int) {
        // Signal strength changes
        Log.d("BLE", "RSSI changed: $rssi")
    }
    
    override fun mtuChange(sn: String?, mtu: Int, success: Boolean) {
        // MTU size changes
        Log.d("BLE", "MTU changed: $mtu, success: $success")
    }
    
    override fun chargingStatusChange(sn: String?, isCharging: Boolean) {
        // Charging status changes
        Log.d("BLE", "Charging: $isCharging")
    }
    
    override fun deviceStatusRsp(sn: String?, response: sdk.penblesdk.entity.bean.ble.response.GetStateRsp) {
        // Device status response
        Log.d("BLE", "Device status: ${response.state}")
    }
    
    override fun scanFail(reason: Constants.ScanFailed) {
        // Bluetooth scan failed
        Log.e("BLE", "Scan failed: ${reason.errMsg}")
    }
}

// Initialize SDK with appKey/appSecret (for older Note/NotePin firmware)
NiceBuildSdk.initSdk(
    context = applicationContext,
    appKey = "your_app_key",
    appSecret = "your_app_secret",
    bleAgentListener = bleAgentListener,
    hostName = "YourAppName",
    extra = null,
    customDomain = null,
    partnerToken = null
)

// Initialize SDK with partnerToken (for NotePro, NotePins, and newer Note/NotePin firmware)
NiceBuildSdk.initSdk(
    context = applicationContext,
    appKey = "placeholder",           // Can be any non-empty value
    appSecret = "placeholder",        // Can be any non-empty value
    bleAgentListener = bleAgentListener,
    hostName = "YourAppName",
    extra = null,
    customDomain = null,
    partnerToken = "your_partner_token"
)

Callbacks Overview

The most common use for these callbacks is to trigger UI updates based on host app’s user experience.
  • scanBleDeviceReceiver - Called when discovered BLE devices are received during scanning.
  • btStatusChange - Called when Bluetooth connection status changes(BluetoothStatus.CONNECTED, BluetoothStatus.CONNECTED).
  • bleConnectFail - Called when Bluetooth connection fails. This is userful for implementing retry logic for higher stability.
  • handshakeWaitSure - Called when device pair handshake needs to be confirmed by users.
  • deviceOpRecordStart - Called when device starts recording. This event can be used to update custom UI.
  • deviceOpRecordStop - Called when device stops recording. This event can be used to update custom UI.
  • batteryLevelUpdate - Called when battery level changes.
  • rssiChange - Called when signal strength changes.
  • mtuChange - Called when MTU size negotiation.
  • chargingStatusChange - Called when charging status changes.
  • deviceStatusRsp - Called when device status reponses.
  • scanFail - Called when Bluetooth scan fails.

Methods

Device Scanning

val bleAgent = TntAgent.getInstant().bleAgent
bleAgent.scanBle(true) { errorCode ->
    Log.e("BLE", "Scan error: $errorCode")
}

Connect Device

import sdk.penblesdk.TntAgent
import sdk.penblesdk.entity.BleDevice

// Sample BleDevice (obtained from scanBleDeviceReceiver callback)
val scannedDevice = BleDevice(
    name = "PLAUD NOTE",
    ...
)

val bindToken = "{unique_device_binding_identifier}" // e.g., "user123_device456_token"
val connectTimeout = 10000L // 10 seconds
val handshakeTimeout = 30000L // 30 seconds

agent.connectionBLE(
    device = scannedDevice,
    bindToken = bindToken,
    devToken = null, // Reserved for future use
    userName = null, // Reserved for future use
    connectTimeout = connectTimeout,
    handshakeTimeout = handshakeTimeout
)

Disconnect Device

Disconnect from the currently connected device while keeping the binding relationship.
val bleAgent = TntAgent.getInstant().bleAgent
bleAgent.disconnectBle()

Unbind Device

Unbind (depair) the device, removing the binding relationship. After unbinding, the device needs to be re-paired.
val bleAgent = TntAgent.getInstant().bleAgent
bleAgent.unBindDevice()
Disconnect vs Unbind:
  • disconnectBle(): Only disconnects Bluetooth connection. The device remains bound and can reconnect without re-pairing.
  • unBindDevice(): Removes the binding relationship. The device must go through the pairing process again to connect.

Get File List

Retrieve the list of recording files stored on the device.
val bleCore = BleCore.getInstance()
bleCore.getFileList(sessionId = 0L) { files ->
    // sessionId = 0 means get all files
    // sessionId > 0 means get files after this sessionId
    files?.forEach { file ->
        Log.d("BLE", "File: sessionId=${file.sessionId}, size=${file.fileSize}")
    }
}

Start Recording

bleAgent.startRecord(scene = recordingScene)

Stop Recording

A method to manually end the current recording.
bleAgent.stopRecord(scene = recordingScene)

Export Audio

Export a recording file to a specified format. This method handles downloading from device, E2EE decryption (if needed), and format conversion automatically.
import sdk.NiceBuildSdk
import sdk.audio.AudioExportFormat
import sdk.audio.AudioExporter

val sessionId = 1234567890L
val outputDir = File(context.getExternalFilesDir(null), "exports")

NiceBuildSdk.exportAudio(
    sessionId = sessionId,
    outputDir = outputDir,
    format = AudioExportFormat.WAV,  // WAV (recommended) or PCM
    channels = 1,  // 1 = mono, 2 = stereo
    callback = object : AudioExporter.ExportCallback {
        override fun onProgress(progress: Int, message: String) {
            Log.d("Export", "Progress: $progress% - $message")
        }
        
        override fun onComplete(outputFile: File) {
            Log.d("Export", "Export completed: ${outputFile.absolutePath}")
        }
        
        override fun onError(error: String) {
            Log.e("Export", "Export failed: $error")
        }
    }
)

// Get supported export formats
val formats = NiceBuildSdk.getSupportedExportFormats()
// Returns: [AudioExportFormat.WAV, AudioExportFormat.PCM]
Export process:
  1. Check if local cache exists
  2. If not, download from device via BLE
  3. Decrypt E2EE encryption (if applicable)
  4. Convert to target format (WAV/PCM)

Export Audio via Wi-Fi

Export a recording file using Wi-Fi Fast Transfer for faster download speeds. The API signature is identical to exportAudio, making it easy to switch between BLE and Wi-Fi transfer channels.
Prerequisites: Wi-Fi transfer must be started and in READY state before calling this method. See the Wi-Fi Fast Transfer section below for connection setup.
import sdk.NiceBuildSdk
import sdk.audio.AudioExportFormat
import sdk.audio.AudioExporter

val sessionId = 1234567890L
val outputDir = File(context.getExternalFilesDir(null), "exports")

NiceBuildSdk.exportAudioViaWiFi(
    sessionId = sessionId,
    outputDir = outputDir,
    format = AudioExportFormat.WAV,  // WAV (recommended) or PCM
    channels = 1,  // 1 = mono, 2 = stereo
    callback = object : AudioExporter.ExportCallback {
        override fun onProgress(progress: Int, message: String) {
            Log.d("Export", "Progress: $progress% - $message")
        }
        
        override fun onComplete(outputFile: File) {
            Log.d("Export", "Export completed: ${outputFile.absolutePath}")
        }
        
        override fun onError(error: String) {
            Log.e("Export", "Export failed: $error")
        }
    }
)
Export via Wi-Fi process:
  1. Check if local cache exists
  2. If not, download from device via Wi-Fi (high-speed)
  3. Decrypt E2EE encryption (if applicable)
  4. Convert to target format (WAV/PCM)
The only difference from exportAudio is that the download step uses Wi-Fi instead of BLE, resulting in much faster transfer speeds.

Delete File

Delete a recording file from the device.
val bleAgent = TntAgent.getInstant().bleAgent
bleAgent.deleteFile(sessionId = 1234567890L) { success ->
    if (success) {
        Log.d("BLE", "File deleted successfully")
    } else {
        Log.e("BLE", "Failed to delete file")
    }
}

Wi-Fi Fast Transfer

Wi-Fi Fast Transfer enables high-speed file transfer between the Plaud device and your app via the device’s built-in Wi-Fi hotspot.
Ensure the device is connected via Bluetooth before initiating Wi-Fi Fast Transfer. Wi-Fi related permissions (ACCESS_WIFI_STATE, CHANGE_WIFI_STATE, ACCESS_FINE_LOCATION) are required. On Android 10+, location permission must be granted at runtime to connect to Wi-Fi networks programmatically.

Integration Overview

1

Start Wi-Fi Transfer

Call startWifiTransfer() which automatically opens the device hotspot, connects, and performs handshake.
2

Receive File List

Once connected, retrieve the file list from the device.
3

Download Files

Download files individually or in batch via Wi-Fi.
4

Stop Transfer

Call stopWifiTransfer() when done to disconnect and release resources.

Get Wi-Fi Agent

// Get the Wi-Fi agent from NiceBuildSdk
val wifiAgent: IWifiAgent? = NiceBuildSdk.getWifiAgent()

Start Wi-Fi Transfer

The startWifiTransfer method handles the full connection flow automatically: opening the device hotspot, connecting to it, and performing the WebSocket handshake.
val success = NiceBuildSdk.startWifiTransfer(userId, object : IWifiAgent.WifiTransferCallback {

    override fun onConnectionStateChanged(state: WifiConnectionState) {
        // NONE -> CONNECTING -> CONNECTED -> HANDSHAKING -> READY
        Log.d("WiFi", "Connection state: $state")
    }

    override fun onHandshakeCompleted(sessionId: String) {
        Log.d("WiFi", "Handshake completed, session: $sessionId")
        // Wi-Fi is now ready for file operations
    }

    override fun onError(errorCode: Int, errorMessage: String) {
        Log.e("WiFi", "Error $errorCode: $errorMessage")
    }
})

Connection States

enum class WifiConnectionState {
    NONE,           // Not connected
    CONNECTING,     // Connecting to device hotspot
    CONNECTED,      // Wi-Fi connected, not yet handshaken
    HANDSHAKING,    // WebSocket handshake in progress
    READY,          // Ready for file operations
    DISCONNECTED,   // Disconnected
    ERROR           // Connection error
}

WifiTransferCallback Reference

CallbackDescription
onConnectionStateChanged(state)Connection state changed
onHandshakeCompleted(sessionId)WebSocket handshake completed, ready for transfer
onFileListReceived(files)File list received from device
onTransferProgress(sessionId, progress, speedKbps, bytesTransferred, totalBytes)Per-file download progress
onFileTransferCompleted(sessionId, filePath)Single file download completed
onWifiTransferStopped()Wi-Fi transfer stopped
onDeviceBatteryUpdate(batteryLevel, isCharging, voltage)Device battery status update
onBatchDownloadStarted(totalFiles)Batch download started
onBatchDownloadProgress(currentIndex, totalFiles, currentFileName)Batch download per-file progress
onBatchDownloadCompleted(successCount, failedCount, results)Batch download completed with results
onError(errorCode, errorMessage)Error occurred

Check Transfer Status

// Check if Wi-Fi transfer is currently active
val isActive: Boolean = NiceBuildSdk.isWifiTransferActive()

// Get current connection state
val state: WifiConnectionState? = wifiAgent?.getConnectionState()

// Check if prerequisites are met (Bluetooth connected, permissions granted)
val ready: Boolean = wifiAgent?.checkPrerequisites() ?: false

Get File List via Wi-Fi

Prerequisite: Wi-Fi transfer must be started and in READY state. See Wi-Fi Fast Transfer above.
wifiAgent?.getFileList()

// Callback
override fun onFileListReceived(files: List<WifiFileInfo>) {
    for (file in files) {
        Log.d("WiFi", "File: sessionId=${file.sessionId}, size=${file.fileSize}")
    }
}

Download Single File via Wi-Fi

For advanced use cases where you need the raw downloaded file without format conversion:
Prerequisite: Wi-Fi transfer must be started and in READY state. See Wi-Fi Fast Transfer above.
wifiAgent?.downloadFile(sessionId, null)

// Per-file transfer progress
override fun onTransferProgress(sessionId: Long, progress: Int, speedKbps: Float,
                                 bytesTransferred: Long, totalBytes: Long) {
    Log.d("WiFi", "File $sessionId: $progress%, speed: ${speedKbps}KB/s")
}

// File download completed
override fun onFileTransferCompleted(sessionId: Long, filePath: String) {
    Log.d("WiFi", "File $sessionId saved to: $filePath")
}

Download All Files via Wi-Fi

Download all files from the device in batch via Wi-Fi:
Prerequisite: Wi-Fi transfer must be started and in READY state. See Wi-Fi Fast Transfer above.
wifiAgent?.downloadAllFiles()

// Batch download started
override fun onBatchDownloadStarted(totalFiles: Int) {
    Log.d("WiFi", "Starting batch download of $totalFiles files")
}

// Per-file progress within batch
override fun onBatchDownloadProgress(currentIndex: Int, totalFiles: Int, currentFileName: String) {
    Log.d("WiFi", "Downloading file $currentIndex/$totalFiles: $currentFileName")
}

// Batch download completed
override fun onBatchDownloadCompleted(
    successCount: Int,
    failedCount: Int,
    results: List<IWifiAgent.BatchDownloadResult>
) {
    Log.d("WiFi", "Batch complete: $successCount succeeded, $failedCount failed")
    results.forEach { result ->
        if (result.success) {
            Log.d("WiFi", "  ${result.fileInfo.sessionId} -> ${result.filePath}")
        } else {
            Log.e("WiFi", "  ${result.fileInfo.sessionId} failed: ${result.errorMessage}")
        }
    }
}

Stop Wi-Fi Transfer and Reconnect BLE

After Wi-Fi file transfer is complete, the device will automatically close its Wi-Fi hotspot after approximately 15 seconds of inactivity. You should handle this transition by stopping the Wi-Fi transfer and reconnecting via BLE.
// Step 1: Stop Wi-Fi transfer
NiceBuildSdk.stopWifiTransfer()

// Step 2: Wait ~15 seconds for the device to close Wi-Fi and resume BLE advertising
Handler(Looper.getMainLooper()).postDelayed({
    // Step 3: Start a BLE scan to find the device
    val bleAgent = TntAgent.getInstant().bleAgent
    bleAgent.scanBle(true) { errorCode ->
        Log.e("BLE", "Scan error: $errorCode")
    }
}, 15_000) // 15 seconds delay

// Step 4: In the scan callback, find the target device by serial number and reconnect
// override fun scanBleDeviceReceiver(device: BleDevice) {
//     if (device.serialNumber == targetSerialNumber) {
//         val bleAgent = TntAgent.getInstant().bleAgent
//         bleAgent.scanBle(false) {} // Stop scan
//         bleAgent.connectionBLE(
//             device = device,
//             bindToken = bindToken,
//             devToken = null,
//             userName = null,
//             connectTimeout = 10000L,
//             handshakeTimeout = 30000L
//         )
//     }
// }
After Wi-Fi transfer, you must perform a BLE scan and use the newly discovered BleDevice object to reconnect. Do not reuse the BleDevice object from before the Wi-Fi transfer.

Example implementation

An example Android app using this SDK can be found here
For an example implementation, see the example app in the Plaud Android SDK repository. The app demostrates:
  • Device scan/connect/disconnect
  • Recording control(start/stop/pause/resume)
  • Recording File Management(Upload/Download)
  • Device Wi-Fi Setting/Test
  • Transcription and Summary