tauri-plugin-blec 0.8.1

BLE-Client plugin for Tauri
package com.plugin.blec

import Peripheral
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.bluetooth.le.BluetoothLeScanner
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanFilter.Builder
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.Context.MODE_PRIVATE
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.ParcelUuid
import android.provider.Settings
import android.util.SparseArray
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.app.ActivityCompat.startActivityForResult
import androidx.core.content.ContextCompat.getSystemService
import app.tauri.annotation.InvokeArg
import app.tauri.plugin.Channel
import app.tauri.plugin.Invoke
import app.tauri.plugin.JSArray
import app.tauri.plugin.JSObject
import java.util.Base64

class BleDevice(
    val address: String,
    private val name: String,
    private val rssi: Int,
    private val connected: Boolean,
    private val bonded: Boolean,
    private val manufacturerData: SparseArray<ByteArray>?,
    private val serviceData: Map<ParcelUuid, ByteArray>?,
    private val services: List<ParcelUuid>?
){
    private val base64Encoder: Base64.Encoder = Base64.getEncoder()

    fun toJsObject():JSObject{
        val obj = JSObject()
        obj.put("address",address)
        obj.put("id",address)
        obj.put("name",name)
        obj.put("connected",connected)
        obj.put("bonded",bonded)
        obj.put("rssi",rssi)
        // create Json Array from services
        val services = if (services != null) {
            val arr = JSArray();
            for (service in services){
                arr.put(service)
            }
            arr
        } else { null }
        obj.put("services",services)
        // crate object from sparse Array
        val manufacturerData = if (manufacturerData != null) {
            val subObj = JSObject()
            for (i in 0 until manufacturerData.size()) {
                val key = manufacturerData.keyAt(i)
                // get the object by the key.
                val value = manufacturerData.get(key)
                subObj.put(key.toString(),base64Encoder.encodeToString(value))
            }
            subObj
        } else { null }
        obj.put("manufacturerData",manufacturerData)
        // crate object from serviceData
        val serviceData = if (serviceData != null) {
            val subObj = JSObject()
            for ((key, value) in serviceData){
                subObj.put(key.toString(),base64Encoder.encodeToString(value))
            }
            subObj
        } else { null }
        obj.put("serviceData",serviceData)
        return obj
    }
}

class BleClient(private val activity: Activity, private val plugin: BleClientPlugin) {
    private var scanner: BluetoothLeScanner? = null;
    private var manager: BluetoothManager? = null;
    private var scanCb: ScanCallback? = null;

    private fun markFirstPermissionRequest(perm: String) {
        val sharedPreference: SharedPreferences =
            activity.getSharedPreferences("PREFS_PERMISSION_FIRST_TIME_ASKING", MODE_PRIVATE)
        sharedPreference.edit().putBoolean(perm, false).apply()
    }

    private fun firstPermissionRequest(perm: String): Boolean {
        return activity.getSharedPreferences("PREFS_PERMISSION_FIRST_TIME_ASKING", MODE_PRIVATE)
            .getBoolean(perm, true)
    }

    public fun checkPermissions(allowIbeacons: Boolean, askIfDenied: Boolean): Boolean {

        var permissions =  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            arrayOf(
                Manifest.permission.BLUETOOTH_SCAN,
                Manifest.permission.BLUETOOTH_CONNECT
            )
        } else {
            arrayOf(
                Manifest.permission.BLUETOOTH_ADMIN,
                Manifest.permission.BLUETOOTH,
            )
        };
        if (allowIbeacons) {
            permissions += Manifest.permission.ACCESS_FINE_LOCATION
        }
        for (perm in permissions){
            if (ActivityCompat.checkSelfPermission(
                    activity,
                    perm
                ) != PackageManager.PERMISSION_GRANTED
            ) {
                if (firstPermissionRequest(perm) || activity.shouldShowRequestPermissionRationale(perm)) {
                    // this will open the permission dialog
                    markFirstPermissionRequest(perm)
                    activity.requestPermissions(permissions, 1)
                    return false
                } else{
                    if (!askIfDenied) {
                        return false
                    }

                    // this will open settings which asks for permission
                    val intent = Intent(
                        Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
                        Uri.parse("package:${activity.packageName}")
                    )
                    activity.startActivity(intent)
                    Toast.makeText(activity, "Allow Permission: $perm", Toast.LENGTH_SHORT).show()
                    return false
                }
            }
        }
        return true
    }

    @InvokeArg
    class ScanParams {
        val services: ArrayList<String> = ArrayList()
        val onDevice: Channel? = null
        val allowIbeacons: Boolean = false
    }
    @SuppressLint("MissingPermission")
    fun startScan(invoke: Invoke) {
        // check if running
        if (scanCb != null){
            invoke.reject("Scan already running")
            return
        }
        val args = invoke.parseArgs(ScanParams::class.java)
        // check permission
        if (!checkPermissions(args.allowIbeacons, false)){
            invoke.reject("Missing permissions");
            return
        }

        // get scanner
        if (scanner == null) {
            manager = getSystemService(activity, BluetoothManager::class.java)
                ?: throw RuntimeException("No bluetooth manager found")
            val bluetoothAdapter: BluetoothAdapter = manager!!.adapter
                ?: throw RuntimeException("No bluetooth adapter available")
            // check if bluetooth is on
            if (!bluetoothAdapter.isEnabled ) {
                val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
                startActivityForResult(activity, enableBtIntent,0,null)
            }
            scanner = bluetoothAdapter.bluetoothLeScanner
                ?: throw RuntimeException("No bluetooth scanner available for adapter")
        }

        // clear old devices
        this.plugin.devices.clear()

        var filters: ArrayList<ScanFilter?>? = null
        if (args.services.size > 0) {
            filters = ArrayList()
            for (uuid in args.services) {
                filters.add(Builder().setServiceUuid(ParcelUuid.fromString(uuid)).build())
            }
        }
        val settings = ScanSettings.Builder()
            .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
            .setLegacy(false)
            .build()

        scanCb = object: ScanCallback(){
            private fun sendResult(result: ScanResult){
                var name = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                    result.device.alias
                } else {
                    result.device.name
                };
                if (name==null){
                    name = result.scanRecord?.deviceName
                }
                if (name == null) {
                    name = ""
                }
                val connected = this@BleClient.manager!!.getConnectionState(result.device,BluetoothProfile.GATT_SERVER) == BluetoothProfile.STATE_CONNECTED
                val bonded = result.device.getBondState() == BluetoothDevice.BOND_BONDED
                val device = BleDevice(
                    result.device.address,
                    name,
                    result.rssi,
                    connected,
                    bonded,
                    result.scanRecord?.manufacturerSpecificData,
                    result.scanRecord?.serviceData,
                    result.scanRecord?.serviceUuids
                )
                this@BleClient.plugin.devices[device.address] = Peripheral(this@BleClient.activity, result.device, this@BleClient.plugin)
                val res = JSObject()
                res.put("result", device.toJsObject())
                args.onDevice!!.send(res)
            }
            override fun onBatchScanResults(results: List<ScanResult>){
                for(result in results){
                    sendResult(result)
                }
            }
            override fun onScanFailed(errorCode: Int){
                println("Scan failed with error code $errorCode")
            }
            override fun onScanResult(callbackType: Int, result: ScanResult){
                sendResult(result)
            }
        }
        scanner?.startScan(filters, settings, scanCb!!)
        invoke.resolve()
    }

    @SuppressLint("MissingPermission")
    fun stopScan(invoke: Invoke){
        if (scanCb!=null) {
            scanner?.stopScan(scanCb!!)
            scanCb = null
        }
        invoke.resolve()
    }

    fun adapterState(invoke: Invoke) {
        val response = JSObject()
        manager = getSystemService(activity, BluetoothManager::class.java)
        if (manager == null){
            response.put("result","unknown")
        } else {
            val adapter = manager?.adapter
            if (adapter == null){
                response.put("result","unknown")
            } else {
                // check if bluetooth is on
                if (adapter.isEnabled ) {
                    response.put("result","on")
                } else {
                    response.put("result","off")
                }
            }
        }

        invoke.resolve(response)
        return
    }
}