feat: foreground service & automatic reconnecting
This commit is contained in:
@@ -22,6 +22,8 @@ android {
|
||||
|
||||
buildConfigField "String", "API_BASE_URL", "\"${props['apiBaseUrl']}\""
|
||||
buildConfigField "String", "API_KEY", "\"${props['apiKey']}\""
|
||||
buildConfigField "String", "SERVICE_UUID", "\"6e400001-b5a3-f393-e1a9-e50e24dcac9e\""
|
||||
buildConfigField "String", "CHAR_UUID", "\"6e400005-b5a3-f393-e1a9-e50e24dcac9e\""
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
android:name="android.permission.ACCESS_FINE_LOCATION"
|
||||
android:maxSdkVersion="30" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.BLUETOOTH_SCAN"
|
||||
android:usesPermissionFlags="neverForLocation"
|
||||
@@ -39,7 +42,6 @@
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.SensorTestingApp">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
@@ -47,6 +49,7 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service android:name=".BLEService" android:foregroundServiceType="connectedDevice" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
69
app/src/main/java/com/example/sensortestingapp/BLEService.kt
Normal file
69
app/src/main/java/com/example/sensortestingapp/BLEService.kt
Normal file
@@ -0,0 +1,69 @@
|
||||
package com.example.sensortestingapp
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
class BLEService : Service() {
|
||||
private val CHANNEL_ID = "BLEService Kotlin"
|
||||
|
||||
companion object {
|
||||
|
||||
fun startService(context: Context, message: String) {
|
||||
val startIntent = Intent(context,
|
||||
BLEService::class.java)
|
||||
startIntent.putExtra("inputExtra", message)
|
||||
ContextCompat.startForegroundService(context, startIntent)
|
||||
}
|
||||
|
||||
fun stopService(context: Context) {
|
||||
val stopIntent = Intent(context, BLEService::class.java)
|
||||
context.stopService(stopIntent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
|
||||
//do heavy work on a background thread
|
||||
val input = intent?.getStringExtra("inputExtra")
|
||||
createNotificationChannel()
|
||||
val notificationIntent = Intent(this, MainActivity::class.java)
|
||||
|
||||
val pendingIntent: PendingIntent =
|
||||
Intent(this, MainActivity::class.java).let { notificationIntent ->
|
||||
PendingIntent.getActivity(this, 0, notificationIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("BLEService Service Kotlin Example")
|
||||
.setContentText(input)
|
||||
//.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentIntent(pendingIntent)
|
||||
.build()
|
||||
|
||||
startForeground(1, notification)
|
||||
//stopSelf();
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val serviceChannel = NotificationChannel(CHANNEL_ID, "BLEService Service Channel",
|
||||
NotificationManager.IMPORTANCE_DEFAULT)
|
||||
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
manager!!.createNotificationChannel(serviceChannel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.example.sensortestingapp
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Service
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothDevice.BOND_BONDED
|
||||
@@ -14,12 +15,14 @@ import android.bluetooth.BluetoothGattDescriptor
|
||||
import android.bluetooth.BluetoothProfile
|
||||
import android.bluetooth.BluetoothProfile.STATE_DISCONNECTED
|
||||
import android.bluetooth.le.ScanCallback
|
||||
import android.bluetooth.le.ScanFilter
|
||||
import android.bluetooth.le.ScanResult
|
||||
import android.bluetooth.le.ScanSettings
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.ParcelUuid
|
||||
import android.util.Log
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
@@ -209,6 +212,8 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
||||
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
|
||||
.build()
|
||||
|
||||
private val scanFilters = listOf( ScanFilter.Builder().setServiceUuid(ParcelUuid.fromString(BuildConfig.SERVICE_UUID)).build())
|
||||
|
||||
private val bleScanner by lazy {
|
||||
bleAdapter.bluetoothLeScanner
|
||||
}
|
||||
@@ -274,6 +279,8 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
||||
broadcastReceiver,
|
||||
IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
||||
var isScanning = false
|
||||
@@ -307,7 +314,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
||||
fun startScan() {
|
||||
if (!isScanning) {
|
||||
isScanning = true
|
||||
bleScanner.startScan(null, scanSettings, scanCallback)
|
||||
bleScanner.startScan( null, scanSettings, scanCallback)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,7 +435,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
||||
with(operation) {
|
||||
Log.w("ConnectionManager", "Connecting to ${device.address}")
|
||||
device.connectGatt(
|
||||
context, false, callback, BluetoothDevice.TRANSPORT_LE
|
||||
context, true, callback, BluetoothDevice.TRANSPORT_LE
|
||||
)
|
||||
}
|
||||
return
|
||||
@@ -520,6 +527,7 @@ class ConnectionManager(val context: Context, bleAdapter: BluetoothAdapter) {
|
||||
|
||||
is SetNotification -> with(operation) {
|
||||
val characteristic = gatt.getService(serviceId)?.getCharacteristic(charId);
|
||||
|
||||
if (characteristic == null) {
|
||||
Log.e("ConnectionManager", "Char $charId (${serviceId}) not found!")
|
||||
signalEndOfOperation(operation)
|
||||
|
||||
@@ -25,9 +25,8 @@ import java.util.UUID
|
||||
import java.util.stream.Collectors
|
||||
|
||||
|
||||
// Kirby service uuid: 6e400001-b5a3-f393-e1a9-e50e24dcac9e
|
||||
private val DEMO_SERVICE_UUID = UUID.fromString("6e400001-b5a3-f393-e1a9-e50e24dcac9e")
|
||||
private val DEMO_CHAR_UUID = UUID.fromString("6e400005-b5a3-f393-e1a9-e50e24dcac9e")
|
||||
private val SERVICE_UUID = UUID.fromString(BuildConfig.SERVICE_UUID)
|
||||
private val CHAR_UUID = UUID.fromString(BuildConfig.CHAR_UUID)
|
||||
|
||||
enum class DeviceStatus {
|
||||
CONNECTED, BONDED, SUBSCRIBED, MISSING
|
||||
@@ -40,8 +39,27 @@ class KirbyDevice(
|
||||
private val bleDevice: BluetoothDevice,
|
||||
private val onStateChange: (device: KirbyDevice) -> Unit,
|
||||
|
||||
) : BleListener(bleDevice.address), DeviceListEntry {
|
||||
private val queue : RequestQueue = Volley.newRequestQueue(context)
|
||||
) : BleListener(bleDevice.address), DeviceListEntry {
|
||||
private val queue: RequestQueue = Volley.newRequestQueue(context)
|
||||
|
||||
fun subscribe() {
|
||||
|
||||
connectionManager.enableNotification(
|
||||
bleDevice, SERVICE_UUID, CHAR_UUID
|
||||
)
|
||||
}
|
||||
|
||||
fun readIaq() {
|
||||
connectionManager.readChar(bleDevice, SERVICE_UUID, CHAR_UUID)
|
||||
}
|
||||
|
||||
override fun onSuccessfulCharRead(
|
||||
gatt: BluetoothGatt,
|
||||
characteristic: BluetoothGattCharacteristic
|
||||
) {
|
||||
addMeasurement(characteristic)
|
||||
onStateChange(this)
|
||||
}
|
||||
|
||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||
rssi = result.rssi
|
||||
@@ -51,6 +69,7 @@ class KirbyDevice(
|
||||
override fun onConnect(gatt: BluetoothGatt) {
|
||||
statuses.add(DeviceStatus.CONNECTED)
|
||||
statuses.remove(DeviceStatus.MISSING)
|
||||
|
||||
onStateChange(this)
|
||||
}
|
||||
|
||||
@@ -65,14 +84,6 @@ class KirbyDevice(
|
||||
onStateChange(this)
|
||||
}
|
||||
|
||||
override fun onSuccessfulCharRead(
|
||||
gatt: BluetoothGatt,
|
||||
characteristic: BluetoothGattCharacteristic
|
||||
) {
|
||||
addMeasurement(characteristic)
|
||||
onStateChange(this)
|
||||
}
|
||||
|
||||
override fun onCharChange(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
|
||||
addMeasurement(characteristic)
|
||||
onStateChange(this)
|
||||
@@ -82,6 +93,7 @@ class KirbyDevice(
|
||||
gatt: BluetoothGatt,
|
||||
descriptor: BluetoothGattDescriptor,
|
||||
) {
|
||||
|
||||
statuses.add(DeviceStatus.SUBSCRIBED)
|
||||
onStateChange(this)
|
||||
}
|
||||
@@ -116,25 +128,23 @@ class KirbyDevice(
|
||||
override var rssi: Int? = null
|
||||
|
||||
private fun addMeasurement(characteristic: BluetoothGattCharacteristic) {
|
||||
if (characteristic.service.uuid == DEMO_SERVICE_UUID && characteristic.uuid == DEMO_CHAR_UUID) {
|
||||
|
||||
val hexPayload = characteristic.value.toHexString()
|
||||
val payload = Payload(hexPayload)
|
||||
val base64Payload = Base64.getEncoder().encodeToString(characteristic.value)
|
||||
Log.i("BleListener", "Demo char received: $payload")
|
||||
Log.i("BleListener", "Char received: $payload")
|
||||
measurements.add(payload)
|
||||
publishMeasurement(base64Payload)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun publishMeasurement(payload: String) {
|
||||
val accessKey = BuildConfig.API_BASE_URL
|
||||
val url = BuildConfig.API_KEY
|
||||
val eui = "0000${bleDevice.address}"
|
||||
val accessKey = BuildConfig.API_KEY
|
||||
val url = BuildConfig.API_BASE_URL
|
||||
val eui = "0000${bleDevice.address.replace(":", "")}"
|
||||
|
||||
val postData = JSONObject()
|
||||
|
||||
try {
|
||||
Log.i("POST", "Transmitting for $eui: $payload")
|
||||
postData.put("accessKey", accessKey)
|
||||
postData.put("metricPayload", payload)
|
||||
postData.put("eui", eui)
|
||||
@@ -145,7 +155,7 @@ class KirbyDevice(
|
||||
val request = JsonObjectRequest(
|
||||
Request.Method.POST, url, postData,
|
||||
{ response ->
|
||||
Log.i("sendDataResponse","Response is: $response")
|
||||
Log.i("sendDataResponse", "Response is: $response")
|
||||
}
|
||||
) { error -> error.printStackTrace() }
|
||||
|
||||
@@ -257,7 +267,7 @@ class KirbyDevice(
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
connectionManager.readChar(bleDevice, DEMO_SERVICE_UUID, DEMO_CHAR_UUID)
|
||||
readIaq()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -272,9 +282,7 @@ class KirbyDevice(
|
||||
}
|
||||
|
||||
override fun execute() {
|
||||
connectionManager.enableNotification(
|
||||
bleDevice, DEMO_SERVICE_UUID, DEMO_CHAR_UUID
|
||||
)
|
||||
subscribe()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -307,7 +315,7 @@ class KirbyDevice(
|
||||
|
||||
override fun execute() {
|
||||
connectionManager.disableNotification(
|
||||
bleDevice, DEMO_SERVICE_UUID, DEMO_CHAR_UUID
|
||||
bleDevice, SERVICE_UUID, CHAR_UUID
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -341,7 +349,6 @@ data class Payload(
|
||||
)
|
||||
|
||||
|
||||
|
||||
fun bytesToUInt16(arr: ByteArray, start: Int): Int {
|
||||
return ByteBuffer.wrap(arr, start, 2)
|
||||
.order(ByteOrder.LITTLE_ENDIAN).short.toInt() and 0xFFFF
|
||||
@@ -371,8 +378,7 @@ private fun payloadToMeasurements(payload: Payload): List<Measurement> {
|
||||
override fun getIcon(): Int? {
|
||||
return R.drawable.baseline_access_time_24
|
||||
}
|
||||
},
|
||||
object : Measurement {
|
||||
}, object : Measurement {
|
||||
override fun getLabel(): String {
|
||||
return "Payload"
|
||||
}
|
||||
@@ -384,8 +390,6 @@ private fun payloadToMeasurements(payload: Payload): List<Measurement> {
|
||||
override fun getIcon(): Int? {
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
)
|
||||
}
|
||||
@@ -75,7 +75,7 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
setSupportActionBar(binding.toolbar)
|
||||
|
||||
|
||||
BLEService.startService(applicationContext, "hello ble service")
|
||||
|
||||
binding.fab.setOnClickListener { view ->
|
||||
if (!hasRequiredRuntimePermissions()) {
|
||||
@@ -103,10 +103,10 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun isKirbyDevice(device: BluetoothDevice): Boolean {
|
||||
Log.i("kby", "found device ${device.name}")
|
||||
return (device.name ?: "").lowercase().contains("kirby")
|
||||
}
|
||||
|
||||
|
||||
private fun setupDevicesList() {
|
||||
binding.devicesList.apply {
|
||||
adapter = deviceListAdapter
|
||||
@@ -134,7 +134,8 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
private fun addBondedDevices(): Unit {
|
||||
bluetoothAdapter.bondedDevices.filter { isKirbyDevice(it) }.forEach {
|
||||
newKirbyDevice(it)
|
||||
val kirbyDevice = newKirbyDevice(it)
|
||||
kirbyDevice.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,13 +180,15 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
private fun newKirbyDevice(bleDevice: BluetoothDevice): KirbyDevice {
|
||||
|
||||
val device = KirbyDevice( this.applicationContext, connectionManager, bleDevice) {
|
||||
val device = KirbyDevice(this.applicationContext, connectionManager, bleDevice) {
|
||||
val i = kirbyDevices.indexOfFirst { d -> d === it }
|
||||
runOnUiThread {
|
||||
deviceListAdapter.notifyItemChanged(i)
|
||||
}
|
||||
}
|
||||
connectionManager.register(device)
|
||||
connectionManager.connect(bleDevice)
|
||||
connectionManager.discoverServices(bleDevice)
|
||||
kirbyDevices.add(device)
|
||||
deviceListAdapter.notifyItemInserted(kirbyDevices.size - 1)
|
||||
return device
|
||||
|
||||
Reference in New Issue
Block a user