Redesign service approach

This commit is contained in:
Sylwester Zieliński
2022-02-10 16:37:22 +01:00
parent 7c69fe14a5
commit d12409cffd
14 changed files with 275 additions and 141 deletions

View File

@@ -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)
} }
} }

View File

@@ -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
}
}

View File

@@ -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)
}
} }

View File

@@ -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()
}
} }
} }
} }

View File

@@ -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()

View File

@@ -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())
} }
} }

View File

@@ -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(

View File

@@ -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)
} }
} }

View File

@@ -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
} }
} }
} }

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()
} }
} }

View File

@@ -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

View File

@@ -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')