mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2025-12-19 23:44:24 +01:00
Redesign service approach
This commit is contained in:
@@ -45,7 +45,6 @@ class ConnectionObserverAdapter<T> : ConnectionObserver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setValue(value: T) {
|
fun setValue(value: T) {
|
||||||
Log.d("AAATESTAAA", "setValue()")
|
|
||||||
_status.value = SuccessResult(value)
|
_status.value = SuccessResult(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package no.nordicsemi.android.service
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.lifecycle.LifecycleService
|
||||||
|
|
||||||
|
private const val CHANNEL_ID = "FOREGROUND_BLE_SERVICE"
|
||||||
|
|
||||||
|
abstract class NotificationService : LifecycleService() {
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
val result = super.onStartCommand(intent, flags, startId)
|
||||||
|
startForegroundService()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
// when user has disconnected from the sensor, we have to cancel the notification that we've created some milliseconds before using unbindService
|
||||||
|
cancelNotification()
|
||||||
|
stopForegroundService()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the service as a foreground service
|
||||||
|
*/
|
||||||
|
private fun startForegroundService() {
|
||||||
|
// when the activity closes we need to show the notification that user is connected to the peripheral sensor
|
||||||
|
// We start the service as a foreground service as Android 8.0 (Oreo) onwards kills any running background services
|
||||||
|
val notification = createNotification(R.string.csc_notification_connected_message, 0)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
startForeground(NOTIFICATION_ID, notification)
|
||||||
|
} else {
|
||||||
|
val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
nm.notify(NOTIFICATION_ID, notification)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the service as a foreground service
|
||||||
|
*/
|
||||||
|
private fun stopForegroundService() {
|
||||||
|
// when the activity rebinds to the service, remove the notification and stop the foreground service
|
||||||
|
// on devices running Android 8.0 (Oreo) or above
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
stopForeground(true)
|
||||||
|
} else {
|
||||||
|
cancelNotification()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the notification
|
||||||
|
*
|
||||||
|
* @param messageResId the message resource id. The message must have one String parameter,<br></br>
|
||||||
|
* f.e. `<string name="name">%s is connected</string>`
|
||||||
|
* @param defaults
|
||||||
|
*/
|
||||||
|
private fun createNotification(messageResId: Int, defaults: Int): Notification {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
createNotificationChannel(CHANNEL_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent: Intent? = packageManager.getLaunchIntentForPackage(packageName)
|
||||||
|
val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
|
||||||
|
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
|
.setContentTitle(getString(R.string.app_name))
|
||||||
|
.setContentText(getString(messageResId, "Device"))
|
||||||
|
.setSmallIcon(R.drawable.ic_notification_icon)
|
||||||
|
.setColor(ContextCompat.getColor(this, R.color.md_theme_primary))
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
private fun createNotificationChannel(channelName: String) {
|
||||||
|
val channel = NotificationChannel(
|
||||||
|
channelName,
|
||||||
|
getString(R.string.channel_connected_devices_title),
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
)
|
||||||
|
channel.description = getString(R.string.channel_connected_devices_description)
|
||||||
|
channel.setShowBadge(false)
|
||||||
|
channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
||||||
|
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
notificationManager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels the existing notification. If there is no active notification this method does nothing
|
||||||
|
*/
|
||||||
|
private fun cancelNotification() {
|
||||||
|
val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
nm.cancel(NOTIFICATION_ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val NOTIFICATION_ID = 200
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package no.nordicsemi.android.service
|
package no.nordicsemi.android.service
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
@@ -19,4 +20,21 @@ class ServiceManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T> startService(service: Class<T>, device: BluetoothDevice) {
|
||||||
|
val intent = Intent(context, service).apply {
|
||||||
|
putExtra(DEVICE_DATA, device)
|
||||||
|
}
|
||||||
|
context.startService(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> startService(service: Class<T>) {
|
||||||
|
val intent = Intent(context, service)
|
||||||
|
context.startService(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> stopService(service: Class<T>) {
|
||||||
|
val intent = Intent(context, service)
|
||||||
|
context.stopService(intent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,13 @@ package no.nordicsemi.android.bps.data
|
|||||||
|
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.android.scopes.ViewModelScoped
|
import dagger.hilt.android.scopes.ViewModelScoped
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
|
||||||
import kotlinx.coroutines.awaitCancellation
|
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import no.nordicsemi.android.ble.ktx.suspend
|
|
||||||
import no.nordicsemi.android.bps.repository.BPSManager
|
import no.nordicsemi.android.bps.repository.BPSManager
|
||||||
import no.nordicsemi.android.service.BleManagerResult
|
import no.nordicsemi.android.service.BleManagerResult
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -32,19 +27,13 @@ internal class BPSRepository @Inject constructor(
|
|||||||
trySend(it)
|
trySend(it)
|
||||||
}.launchIn(scope)
|
}.launchIn(scope)
|
||||||
|
|
||||||
try {
|
manager.connect(device)
|
||||||
manager.connect(device)
|
.useAutoConnect(false)
|
||||||
.useAutoConnect(false)
|
.retry(3, 100)
|
||||||
.retry(3, 100)
|
.enqueue()
|
||||||
.suspend()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
|
|
||||||
awaitClose {
|
awaitClose {
|
||||||
launch {
|
manager.disconnect().enqueue()
|
||||||
manager.disconnect().suspend()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package no.nordicsemi.android.bps.view
|
package no.nordicsemi.android.bps.view
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
@@ -29,8 +28,6 @@ fun BPSScreen() {
|
|||||||
viewModel.onEvent(DisconnectEvent)
|
viewModel.onEvent(DisconnectEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d("AAATESTAAA", "state: $state")
|
|
||||||
|
|
||||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||||
when (state) {
|
when (state) {
|
||||||
NoDeviceState -> NoDeviceView()
|
NoDeviceState -> NoDeviceView()
|
||||||
|
|||||||
@@ -1,49 +1,64 @@
|
|||||||
package no.nordicsemi.android.cgms.data
|
package no.nordicsemi.android.cgms.data
|
||||||
|
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import android.bluetooth.BluetoothDevice
|
||||||
import kotlinx.coroutines.flow.*
|
import android.content.Context
|
||||||
import no.nordicsemi.android.service.BleManagerStatus
|
import android.util.Log
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import no.nordicsemi.android.cgms.repository.CGMManager
|
||||||
|
import no.nordicsemi.android.cgms.repository.CGMService
|
||||||
|
import no.nordicsemi.android.service.BleManagerResult
|
||||||
|
import no.nordicsemi.android.service.ConnectingResult
|
||||||
|
import no.nordicsemi.android.service.ServiceManager
|
||||||
|
import no.nordicsemi.android.utils.exhaustive
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
internal class CGMRepository @Inject constructor() {
|
internal class CGMRepository @Inject constructor(
|
||||||
|
@ApplicationContext
|
||||||
|
private val context: Context,
|
||||||
|
private val serviceManager: ServiceManager,
|
||||||
|
) {
|
||||||
|
private var manager: CGMManager? = null
|
||||||
|
|
||||||
private val _data = MutableStateFlow(CGMData())
|
private val _data = MutableStateFlow<BleManagerResult<CGMData>>(ConnectingResult())
|
||||||
val data: StateFlow<CGMData> = _data.asStateFlow()
|
val data = _data.asStateFlow()
|
||||||
|
|
||||||
private val _command = MutableSharedFlow<CGMServiceCommand>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST)
|
fun launch(device: BluetoothDevice) {
|
||||||
val command = _command.asSharedFlow()
|
serviceManager.startService(CGMService::class.java, device)
|
||||||
|
|
||||||
private val _status = MutableStateFlow(BleManagerStatus.CONNECTING)
|
|
||||||
val status = _status.asStateFlow()
|
|
||||||
|
|
||||||
fun emitNewBatteryLevel(batterLevel: Int) {
|
|
||||||
_data.tryEmit(_data.value.copy(batteryLevel = batterLevel))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun emitNewRecords(records: List<CGMRecord>) {
|
fun startManager(device: BluetoothDevice, scope: CoroutineScope) {
|
||||||
_data.tryEmit(_data.value.copy(records = records))
|
val manager = CGMManager(context, scope)
|
||||||
}
|
|
||||||
|
|
||||||
fun setRequestStatus(requestStatus: RequestStatus) {
|
manager.dataHolder.status.onEach {
|
||||||
_data.tryEmit(_data.value.copy(requestStatus = requestStatus))
|
_data.value = it
|
||||||
|
Log.d("AAATESTAAA", "data: $it")
|
||||||
|
}.launchIn(scope)
|
||||||
|
|
||||||
|
manager.connect(device)
|
||||||
|
.useAutoConnect(false)
|
||||||
|
.retry(3, 100)
|
||||||
|
.enqueue()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendNewServiceCommand(workingMode: CGMServiceCommand) {
|
fun sendNewServiceCommand(workingMode: CGMServiceCommand) {
|
||||||
if (_command.subscriptionCount.value > 0) {
|
when (workingMode) {
|
||||||
_command.tryEmit(workingMode)
|
CGMServiceCommand.REQUEST_ALL_RECORDS -> manager?.requestAllRecords()
|
||||||
} else {
|
CGMServiceCommand.REQUEST_LAST_RECORD -> manager?.requestLastRecord()
|
||||||
_status.tryEmit(BleManagerStatus.DISCONNECTED)
|
CGMServiceCommand.REQUEST_FIRST_RECORD -> manager?.requestFirstRecord()
|
||||||
}
|
CGMServiceCommand.DISCONNECT -> release()
|
||||||
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setNewStatus(status: BleManagerStatus) {
|
private fun release() {
|
||||||
_status.value = status
|
serviceManager.stopService(CGMService::class.java)
|
||||||
}
|
manager?.disconnect()?.enqueue()
|
||||||
|
manager = null
|
||||||
fun clear() {
|
|
||||||
_status.value = BleManagerStatus.CONNECTING
|
|
||||||
_data.tryEmit(CGMData())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ import android.bluetooth.BluetoothGattCharacteristic
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.SparseArray
|
import android.util.SparseArray
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import no.nordicsemi.android.ble.BleManager
|
||||||
import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointResponse
|
import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointResponse
|
||||||
import no.nordicsemi.android.ble.common.callback.cgm.CGMFeatureResponse
|
import no.nordicsemi.android.ble.common.callback.cgm.CGMFeatureResponse
|
||||||
import no.nordicsemi.android.ble.common.callback.cgm.CGMSpecificOpsControlPointResponse
|
import no.nordicsemi.android.ble.common.callback.cgm.CGMSpecificOpsControlPointResponse
|
||||||
@@ -43,10 +43,10 @@ import no.nordicsemi.android.ble.common.profile.cgm.CGMSpecificOpsControlPointCa
|
|||||||
import no.nordicsemi.android.ble.ktx.asValidResponseFlow
|
import no.nordicsemi.android.ble.ktx.asValidResponseFlow
|
||||||
import no.nordicsemi.android.ble.ktx.suspend
|
import no.nordicsemi.android.ble.ktx.suspend
|
||||||
import no.nordicsemi.android.ble.ktx.suspendForValidResponse
|
import no.nordicsemi.android.ble.ktx.suspendForValidResponse
|
||||||
|
import no.nordicsemi.android.cgms.data.CGMData
|
||||||
import no.nordicsemi.android.cgms.data.CGMRecord
|
import no.nordicsemi.android.cgms.data.CGMRecord
|
||||||
import no.nordicsemi.android.cgms.data.CGMRepository
|
|
||||||
import no.nordicsemi.android.cgms.data.RequestStatus
|
import no.nordicsemi.android.cgms.data.RequestStatus
|
||||||
import no.nordicsemi.android.service.BatteryManager
|
import no.nordicsemi.android.service.ConnectionObserverAdapter
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
val CGMS_SERVICE_UUID: UUID = UUID.fromString("0000181F-0000-1000-8000-00805f9b34fb")
|
val CGMS_SERVICE_UUID: UUID = UUID.fromString("0000181F-0000-1000-8000-00805f9b34fb")
|
||||||
@@ -57,11 +57,13 @@ private val CGM_OPS_CONTROL_POINT_UUID = UUID.fromString("00002AAC-0000-1000-800
|
|||||||
|
|
||||||
private val RACP_UUID = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb")
|
private val RACP_UUID = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb")
|
||||||
|
|
||||||
|
private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb")
|
||||||
|
private val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb")
|
||||||
|
|
||||||
internal class CGMManager(
|
internal class CGMManager(
|
||||||
context: Context,
|
context: Context,
|
||||||
scope: CoroutineScope,
|
private val scope: CoroutineScope
|
||||||
private val repository: CGMRepository
|
) : BleManager(context) {
|
||||||
) : BatteryManager(context, scope) {
|
|
||||||
|
|
||||||
private var cgmStatusCharacteristic: BluetoothGattCharacteristic? = null
|
private var cgmStatusCharacteristic: BluetoothGattCharacteristic? = null
|
||||||
private var cgmFeatureCharacteristic: BluetoothGattCharacteristic? = null
|
private var cgmFeatureCharacteristic: BluetoothGattCharacteristic? = null
|
||||||
@@ -69,6 +71,7 @@ internal class CGMManager(
|
|||||||
private var cgmSpecificOpsControlPointCharacteristic: BluetoothGattCharacteristic? = null
|
private var cgmSpecificOpsControlPointCharacteristic: BluetoothGattCharacteristic? = null
|
||||||
private var recordAccessControlPointCharacteristic: BluetoothGattCharacteristic? = null
|
private var recordAccessControlPointCharacteristic: BluetoothGattCharacteristic? = null
|
||||||
private val records: SparseArray<CGMRecord> = SparseArray<CGMRecord>()
|
private val records: SparseArray<CGMRecord> = SparseArray<CGMRecord>()
|
||||||
|
private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null
|
||||||
|
|
||||||
private var secured = false
|
private var secured = false
|
||||||
|
|
||||||
@@ -80,15 +83,22 @@ internal class CGMManager(
|
|||||||
Log.e("COROUTINE-EXCEPTION", "Uncaught exception", t)
|
Log.e("COROUTINE-EXCEPTION", "Uncaught exception", t)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBatteryLevelChanged(batteryLevel: Int) {
|
private val data = MutableStateFlow(CGMData())
|
||||||
repository.emitNewBatteryLevel(batteryLevel)
|
val dataHolder = ConnectionObserverAdapter<CGMData>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
setConnectionObserver(dataHolder)
|
||||||
|
|
||||||
|
data.onEach {
|
||||||
|
dataHolder.setValue(it)
|
||||||
|
}.launchIn(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getGattCallback(): BatteryManagerGattCallback {
|
override fun getGattCallback(): BleManagerGattCallback {
|
||||||
return CGMManagerGattCallback()
|
return CGMManagerGattCallback()
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class CGMManagerGattCallback : BatteryManagerGattCallback() {
|
private inner class CGMManagerGattCallback : BleManagerGattCallback() {
|
||||||
override fun initialize() {
|
override fun initialize() {
|
||||||
super.initialize()
|
super.initialize()
|
||||||
|
|
||||||
@@ -98,6 +108,12 @@ internal class CGMManager(
|
|||||||
this@CGMManager.secured = response.features.e2eCrcSupported
|
this@CGMManager.secured = response.features.e2eCrcSupported
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scope.launch(exceptionHandler) {
|
||||||
|
val response =
|
||||||
|
readCharacteristic(cgmFeatureCharacteristic).suspendForValidResponse<CGMFeatureResponse>()
|
||||||
|
this@CGMManager.secured = response.features.e2eCrcSupported
|
||||||
|
}
|
||||||
|
|
||||||
scope.launch(exceptionHandler) {
|
scope.launch(exceptionHandler) {
|
||||||
val response =
|
val response =
|
||||||
readCharacteristic(cgmStatusCharacteristic).suspendForValidResponse<CGMStatusResponse>()
|
readCharacteristic(cgmStatusCharacteristic).suspendForValidResponse<CGMStatusResponse>()
|
||||||
@@ -115,7 +131,8 @@ internal class CGMManager(
|
|||||||
val timestamp = sessionStartTime + it.timeOffset * 60000L
|
val timestamp = sessionStartTime + it.timeOffset * 60000L
|
||||||
val record = CGMRecord(it.timeOffset, it.glucoseConcentration, timestamp)
|
val record = CGMRecord(it.timeOffset, it.glucoseConcentration, timestamp)
|
||||||
records.put(record.sequenceNumber, record)
|
records.put(record.sequenceNumber, record)
|
||||||
repository.emitNewRecords(records.toList())
|
|
||||||
|
data.value = data.value.copy(records = records.toList())
|
||||||
}.launchIn(scope)
|
}.launchIn(scope)
|
||||||
|
|
||||||
setIndicationCallback(cgmSpecificOpsControlPointCharacteristic).asValidResponseFlow<CGMSpecificOpsControlPointResponse>()
|
setIndicationCallback(cgmSpecificOpsControlPointCharacteristic).asValidResponseFlow<CGMSpecificOpsControlPointResponse>()
|
||||||
@@ -152,15 +169,10 @@ internal class CGMManager(
|
|||||||
}
|
}
|
||||||
}.launchIn(scope)
|
}.launchIn(scope)
|
||||||
|
|
||||||
scope.launch(exceptionHandler) {
|
enableNotifications(cgmMeasurementCharacteristic).enqueue()
|
||||||
enableNotifications(cgmMeasurementCharacteristic).suspend()
|
enableIndications(cgmSpecificOpsControlPointCharacteristic).enqueue()
|
||||||
}
|
enableIndications(recordAccessControlPointCharacteristic).enqueue()
|
||||||
scope.launch(exceptionHandler) {
|
enableNotifications(batteryLevelCharacteristic).enqueue()
|
||||||
enableIndications(cgmSpecificOpsControlPointCharacteristic).suspend()
|
|
||||||
}
|
|
||||||
scope.launch(exceptionHandler) {
|
|
||||||
enableIndications(recordAccessControlPointCharacteristic).suspend()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sessionStartTime == 0L) {
|
if (sessionStartTime == 0L) {
|
||||||
scope.launch(exceptionHandler) {
|
scope.launch(exceptionHandler) {
|
||||||
@@ -179,9 +191,7 @@ internal class CGMManager(
|
|||||||
val sequenceNumber = records.keyAt(records.size() - 1) + 1
|
val sequenceNumber = records.keyAt(records.size() - 1) + 1
|
||||||
writeCharacteristic(
|
writeCharacteristic(
|
||||||
recordAccessControlPointCharacteristic,
|
recordAccessControlPointCharacteristic,
|
||||||
RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(
|
RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber),
|
||||||
sequenceNumber
|
|
||||||
),
|
|
||||||
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
|
||||||
).suspend()
|
).suspend()
|
||||||
} else {
|
} else {
|
||||||
@@ -193,32 +203,31 @@ internal class CGMManager(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
recordAccessRequestInProgress = false
|
recordAccessRequestInProgress = false
|
||||||
repository.setRequestStatus(RequestStatus.SUCCESS)
|
data.value = data.value.copy(requestStatus = RequestStatus.SUCCESS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onNoRecordsFound() {
|
private fun onNoRecordsFound() {
|
||||||
recordAccessRequestInProgress = false
|
recordAccessRequestInProgress = false
|
||||||
repository.setRequestStatus(RequestStatus.SUCCESS)
|
data.value = data.value.copy(requestStatus = RequestStatus.SUCCESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onOperationCompleted(response: RecordAccessControlPointResponse) {
|
private fun onOperationCompleted(response: RecordAccessControlPointResponse) {
|
||||||
when (response.requestCode) {
|
when (response.requestCode) {
|
||||||
RecordAccessControlPointCallback.RACP_OP_CODE_ABORT_OPERATION -> repository.setRequestStatus(
|
RecordAccessControlPointCallback.RACP_OP_CODE_ABORT_OPERATION ->
|
||||||
RequestStatus.ABORTED
|
data.value = data.value.copy(requestStatus = RequestStatus.ABORTED)
|
||||||
)
|
|
||||||
else -> {
|
else -> {
|
||||||
recordAccessRequestInProgress = false
|
recordAccessRequestInProgress = false
|
||||||
repository.setRequestStatus(RequestStatus.SUCCESS)
|
data.value = data.value.copy(requestStatus = RequestStatus.SUCCESS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onError(response: RecordAccessControlPointResponse) {
|
private fun onError(response: RecordAccessControlPointResponse) {
|
||||||
if (response.errorCode == RecordAccessControlPointCallback.RACP_ERROR_OP_CODE_NOT_SUPPORTED) {
|
if (response.errorCode == RecordAccessControlPointCallback.RACP_ERROR_OP_CODE_NOT_SUPPORTED) {
|
||||||
repository.setRequestStatus(RequestStatus.NOT_SUPPORTED)
|
data.value = data.value.copy(requestStatus = RequestStatus.NOT_SUPPORTED)
|
||||||
} else {
|
} else {
|
||||||
repository.setRequestStatus(RequestStatus.FAILED)
|
data.value = data.value.copy(requestStatus = RequestStatus.FAILED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +264,7 @@ internal class CGMManager(
|
|||||||
fun requestLastRecord() {
|
fun requestLastRecord() {
|
||||||
if (recordAccessControlPointCharacteristic == null) return
|
if (recordAccessControlPointCharacteristic == null) return
|
||||||
clear()
|
clear()
|
||||||
repository.setRequestStatus(RequestStatus.PENDING)
|
data.value = data.value.copy(requestStatus = RequestStatus.PENDING)
|
||||||
recordAccessRequestInProgress = true
|
recordAccessRequestInProgress = true
|
||||||
scope.launch(exceptionHandler) {
|
scope.launch(exceptionHandler) {
|
||||||
writeCharacteristic(
|
writeCharacteristic(
|
||||||
@@ -269,7 +278,7 @@ internal class CGMManager(
|
|||||||
fun requestFirstRecord() {
|
fun requestFirstRecord() {
|
||||||
if (recordAccessControlPointCharacteristic == null) return
|
if (recordAccessControlPointCharacteristic == null) return
|
||||||
clear()
|
clear()
|
||||||
repository.setRequestStatus(RequestStatus.PENDING)
|
data.value = data.value.copy(requestStatus = RequestStatus.PENDING)
|
||||||
recordAccessRequestInProgress = true
|
recordAccessRequestInProgress = true
|
||||||
scope.launch(exceptionHandler) {
|
scope.launch(exceptionHandler) {
|
||||||
writeCharacteristic(
|
writeCharacteristic(
|
||||||
@@ -283,7 +292,7 @@ internal class CGMManager(
|
|||||||
fun requestAllRecords() {
|
fun requestAllRecords() {
|
||||||
if (recordAccessControlPointCharacteristic == null) return
|
if (recordAccessControlPointCharacteristic == null) return
|
||||||
clear()
|
clear()
|
||||||
repository.setRequestStatus(RequestStatus.PENDING)
|
data.value = data.value.copy(requestStatus = RequestStatus.PENDING)
|
||||||
recordAccessRequestInProgress = true
|
recordAccessRequestInProgress = true
|
||||||
scope.launch(exceptionHandler) {
|
scope.launch(exceptionHandler) {
|
||||||
writeCharacteristic(
|
writeCharacteristic(
|
||||||
|
|||||||
@@ -1,38 +1,27 @@
|
|||||||
package no.nordicsemi.android.cgms.repository
|
package no.nordicsemi.android.cgms.repository
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import no.nordicsemi.android.cgms.data.CGMRepository
|
import no.nordicsemi.android.cgms.data.CGMRepository
|
||||||
import no.nordicsemi.android.cgms.data.CGMServiceCommand
|
import no.nordicsemi.android.service.DEVICE_DATA
|
||||||
import no.nordicsemi.android.service.ForegroundBleService
|
import no.nordicsemi.android.service.NotificationService
|
||||||
import no.nordicsemi.android.utils.exhaustive
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
internal class CGMService : ForegroundBleService() {
|
internal class CGMService : NotificationService() {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var repository: CGMRepository
|
lateinit var repository: CGMRepository
|
||||||
|
|
||||||
override val manager: CGMManager by lazy { CGMManager(this, scope, repository) }
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
super.onStartCommand(intent, flags, startId)
|
||||||
|
|
||||||
override fun onCreate() {
|
val device = intent!!.getParcelableExtra<BluetoothDevice>(DEVICE_DATA)!!
|
||||||
super.onCreate()
|
|
||||||
|
|
||||||
// status.onEach {
|
repository.startManager(device, lifecycleScope)
|
||||||
// val status = it.mapToSimpleManagerStatus()
|
|
||||||
// repository.setNewStatus(status)
|
|
||||||
// stopIfDisconnected(status)
|
|
||||||
// }.launchIn(scope)
|
|
||||||
|
|
||||||
repository.command.onEach {
|
return START_REDELIVER_INTENT
|
||||||
when (it) {
|
|
||||||
CGMServiceCommand.REQUEST_ALL_RECORDS -> manager.requestAllRecords()
|
|
||||||
CGMServiceCommand.REQUEST_LAST_RECORD -> manager.requestLastRecord()
|
|
||||||
CGMServiceCommand.REQUEST_FIRST_RECORD -> manager.requestFirstRecord()
|
|
||||||
CGMServiceCommand.DISCONNECT -> stopSelf()
|
|
||||||
}.exhaustive
|
|
||||||
}.launchIn(scope)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,13 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import no.nordicsemi.android.cgms.R
|
import no.nordicsemi.android.cgms.R
|
||||||
import no.nordicsemi.android.cgms.viewmodel.CGMScreenViewModel
|
import no.nordicsemi.android.cgms.viewmodel.CGMScreenViewModel
|
||||||
|
import no.nordicsemi.android.service.*
|
||||||
import no.nordicsemi.android.theme.view.BackIconAppBar
|
import no.nordicsemi.android.theme.view.BackIconAppBar
|
||||||
|
import no.nordicsemi.android.theme.view.scanner.DeviceConnectingView
|
||||||
|
import no.nordicsemi.android.theme.view.scanner.DeviceDisconnectedView
|
||||||
|
import no.nordicsemi.android.theme.view.scanner.NoDeviceView
|
||||||
|
import no.nordicsemi.android.theme.view.scanner.Reason
|
||||||
|
import no.nordicsemi.android.utils.exhaustive
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CGMScreen() {
|
fun CGMScreen() {
|
||||||
@@ -23,10 +29,17 @@ fun CGMScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||||
// when (state) {
|
when (state) {
|
||||||
// is DisplayDataState -> CGMContentView(state.data) { viewModel.onEvent(it) }
|
NoDeviceState -> NoDeviceView()
|
||||||
// LoadingState -> DeviceConnectingView()
|
is WorkingState -> when (state.result) {
|
||||||
// }.exhaustive
|
is ConnectingResult -> DeviceConnectingView()
|
||||||
|
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER)
|
||||||
|
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS)
|
||||||
|
is MissingServiceResult -> DeviceDisconnectedView(Reason.MISSING_SERVICE)
|
||||||
|
is ReadyResult -> DeviceConnectingView()
|
||||||
|
is SuccessResult -> CGMContentView(state.result.data) { viewModel.onEvent(it) }
|
||||||
|
}
|
||||||
|
}.exhaustive
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,4 +6,6 @@ internal sealed class CGMViewEvent
|
|||||||
|
|
||||||
internal data class OnWorkingModeSelected(val workingMode: CGMServiceCommand) : CGMViewEvent()
|
internal data class OnWorkingModeSelected(val workingMode: CGMServiceCommand) : CGMViewEvent()
|
||||||
|
|
||||||
|
internal object NavigateUp : CGMViewEvent()
|
||||||
|
|
||||||
internal object DisconnectEvent : CGMViewEvent()
|
internal object DisconnectEvent : CGMViewEvent()
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package no.nordicsemi.android.cgms.view
|
package no.nordicsemi.android.cgms.view
|
||||||
|
|
||||||
import no.nordicsemi.android.cgms.data.CGMData
|
import no.nordicsemi.android.cgms.data.CGMData
|
||||||
|
import no.nordicsemi.android.service.BleManagerResult
|
||||||
|
|
||||||
internal sealed class CGMViewState
|
internal sealed class BPSViewState
|
||||||
|
|
||||||
internal object LoadingState : CGMViewState()
|
internal data class WorkingState(val result: BleManagerResult<CGMData>) : BPSViewState()
|
||||||
|
internal object NoDeviceState : BPSViewState()
|
||||||
internal data class DisplayDataState(val data: CGMData) : CGMViewState()
|
|
||||||
|
|||||||
@@ -1,36 +1,32 @@
|
|||||||
package no.nordicsemi.android.cgms.viewmodel
|
package no.nordicsemi.android.cgms.viewmodel
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import no.nordicsemi.android.cgms.data.CGMRepository
|
import no.nordicsemi.android.cgms.data.CGMRepository
|
||||||
import no.nordicsemi.android.cgms.data.CGMServiceCommand
|
import no.nordicsemi.android.cgms.data.CGMServiceCommand
|
||||||
import no.nordicsemi.android.cgms.repository.CGMS_SERVICE_UUID
|
import no.nordicsemi.android.cgms.repository.CGMS_SERVICE_UUID
|
||||||
import no.nordicsemi.android.cgms.repository.CGMService
|
|
||||||
import no.nordicsemi.android.cgms.view.*
|
import no.nordicsemi.android.cgms.view.*
|
||||||
import no.nordicsemi.android.navigation.*
|
import no.nordicsemi.android.navigation.*
|
||||||
import no.nordicsemi.android.service.BleManagerStatus
|
|
||||||
import no.nordicsemi.android.service.ServiceManager
|
|
||||||
import no.nordicsemi.android.utils.exhaustive
|
import no.nordicsemi.android.utils.exhaustive
|
||||||
import no.nordicsemi.android.utils.getDevice
|
import no.nordicsemi.android.utils.getDevice
|
||||||
|
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
|
||||||
import no.nordicsemi.ui.scanner.ScannerDestinationId
|
import no.nordicsemi.ui.scanner.ScannerDestinationId
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
internal class CGMScreenViewModel @Inject constructor(
|
internal class CGMScreenViewModel @Inject constructor(
|
||||||
private val repository: CGMRepository,
|
private val repository: CGMRepository,
|
||||||
private val serviceManager: ServiceManager,
|
|
||||||
private val navigationManager: NavigationManager
|
private val navigationManager: NavigationManager
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val state = repository.data.combine(repository.status) { data, status ->
|
private val _state = MutableStateFlow<BPSViewState>(NoDeviceState)
|
||||||
// when (status) {
|
val state = _state.asStateFlow()
|
||||||
// BleManagerStatus.CONNECTING -> LoadingState
|
|
||||||
// BleManagerStatus.OK,
|
|
||||||
// BleManagerStatus.DISCONNECTED -> DisplayDataState(data)
|
|
||||||
// }
|
|
||||||
}.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState)
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
navigationManager.navigateTo(ScannerDestinationId, UUIDArgument(CGMS_SERVICE_UUID))
|
navigationManager.navigateTo(ScannerDestinationId, UUIDArgument(CGMS_SERVICE_UUID))
|
||||||
@@ -41,17 +37,16 @@ internal class CGMScreenViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}.launchIn(viewModelScope)
|
}.launchIn(viewModelScope)
|
||||||
|
|
||||||
repository.status.onEach {
|
repository.data.onEach {
|
||||||
if (it == BleManagerStatus.DISCONNECTED) {
|
_state.value = WorkingState(it)
|
||||||
navigationManager.navigateUp()
|
Log.d("AAATESTAAA", "vm data: $it")
|
||||||
}
|
|
||||||
}.launchIn(viewModelScope)
|
}.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleArgs(args: DestinationResult) {
|
private fun handleArgs(args: DestinationResult) {
|
||||||
when (args) {
|
when (args) {
|
||||||
is CancelDestinationResult -> navigationManager.navigateUp()
|
is CancelDestinationResult -> navigationManager.navigateUp()
|
||||||
is SuccessDestinationResult -> serviceManager.startService(CGMService::class.java, args.getDevice())
|
is SuccessDestinationResult -> connectDevice(args.getDevice())
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,16 +54,15 @@ internal class CGMScreenViewModel @Inject constructor(
|
|||||||
when (event) {
|
when (event) {
|
||||||
DisconnectEvent -> disconnect()
|
DisconnectEvent -> disconnect()
|
||||||
is OnWorkingModeSelected -> repository.sendNewServiceCommand(event.workingMode)
|
is OnWorkingModeSelected -> repository.sendNewServiceCommand(event.workingMode)
|
||||||
|
NavigateUp -> navigationManager.navigateUp()
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun connectDevice(deviceHolder: DiscoveredBluetoothDevice) {
|
||||||
|
repository.launch(deviceHolder.device)
|
||||||
|
}
|
||||||
|
|
||||||
private fun disconnect() {
|
private fun disconnect() {
|
||||||
repository.sendNewServiceCommand(CGMServiceCommand.DISCONNECT)
|
repository.sendNewServiceCommand(CGMServiceCommand.DISCONNECT)
|
||||||
repository.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared() {
|
|
||||||
super.onCleared()
|
|
||||||
repository.clear()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ dependencyResolutionManagement {
|
|||||||
alias('compose-activity').to('androidx.activity:activity-compose:1.4.0')
|
alias('compose-activity').to('androidx.activity:activity-compose:1.4.0')
|
||||||
alias('compose-lifecycle').to('androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0')
|
alias('compose-lifecycle').to('androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0')
|
||||||
|
|
||||||
version('compose', '1.0.5')
|
version('compose', '1.1.0')
|
||||||
alias('compose-livedata').to('androidx.compose.runtime', 'runtime-livedata').versionRef('compose')
|
alias('compose-livedata').to('androidx.compose.runtime', 'runtime-livedata').versionRef('compose')
|
||||||
alias('compose-ui').to('androidx.compose.ui', 'ui').versionRef('compose')
|
alias('compose-ui').to('androidx.compose.ui', 'ui').versionRef('compose')
|
||||||
alias('compose-material').to('androidx.compose.material3:material3:1.0.0-alpha04')
|
alias('compose-material').to('androidx.compose.material3:material3:1.0.0-alpha04')
|
||||||
|
|||||||
Reference in New Issue
Block a user