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.ScanResult.TX_POWER_NOT_PRESENT
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 txPowerLevel: Int?
){
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)
obj.put("txPowerLevel",txPowerLevel)
// 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)
}
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 txPower = if (result.txPower == TX_POWER_NOT_PRESENT) {
null
} else {
result.txPower
}
val device = BleDevice(
result.device.address,
name,
result.rssi,
connected,
bonded,
result.scanRecord?.manufacturerSpecificData,
result.scanRecord?.serviceData,
result.scanRecord?.serviceUuids,
txPower
)
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
}
}