diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/ConnectionObserverAdapter.kt b/lib_service/src/main/java/no/nordicsemi/android/service/ConnectionObserverAdapter.kt index 76343ab8..2b76f84d 100644 --- a/lib_service/src/main/java/no/nordicsemi/android/service/ConnectionObserverAdapter.kt +++ b/lib_service/src/main/java/no/nordicsemi/android/service/ConnectionObserverAdapter.kt @@ -45,7 +45,6 @@ class ConnectionObserverAdapter : ConnectionObserver { } fun setValue(value: T) { - Log.d("AAATESTAAA", "setValue()") _status.value = SuccessResult(value) } } diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/NotificationService.kt b/lib_service/src/main/java/no/nordicsemi/android/service/NotificationService.kt new file mode 100644 index 00000000..681941d5 --- /dev/null +++ b/lib_service/src/main/java/no/nordicsemi/android/service/NotificationService.kt @@ -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,

+ * f.e. `%s is connected` + * @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 + } +} diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/ServiceManager.kt b/lib_service/src/main/java/no/nordicsemi/android/service/ServiceManager.kt index f7b1e87b..6f02565a 100644 --- a/lib_service/src/main/java/no/nordicsemi/android/service/ServiceManager.kt +++ b/lib_service/src/main/java/no/nordicsemi/android/service/ServiceManager.kt @@ -1,5 +1,6 @@ package no.nordicsemi.android.service +import android.bluetooth.BluetoothDevice import android.content.Context import android.content.Intent import dagger.hilt.android.qualifiers.ApplicationContext @@ -19,4 +20,21 @@ class ServiceManager @Inject constructor( } context.startService(intent) } + + fun startService(service: Class, device: BluetoothDevice) { + val intent = Intent(context, service).apply { + putExtra(DEVICE_DATA, device) + } + context.startService(intent) + } + + fun startService(service: Class) { + val intent = Intent(context, service) + context.startService(intent) + } + + fun stopService(service: Class) { + val intent = Intent(context, service) + context.stopService(intent) + } } diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/data/BPSRepository.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/data/BPSRepository.kt index 4f0ef555..35cfaef5 100644 --- a/profile_bps/src/main/java/no/nordicsemi/android/bps/data/BPSRepository.kt +++ b/profile_bps/src/main/java/no/nordicsemi/android/bps/data/BPSRepository.kt @@ -2,18 +2,13 @@ package no.nordicsemi.android.bps.data import android.bluetooth.BluetoothDevice import android.content.Context -import android.util.Log import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.scopes.ViewModelScoped -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.launchIn 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.service.BleManagerResult import javax.inject.Inject @@ -32,19 +27,13 @@ internal class BPSRepository @Inject constructor( trySend(it) }.launchIn(scope) - try { - manager.connect(device) - .useAutoConnect(false) - .retry(3, 100) - .suspend() - } catch (e: Exception) { - e.printStackTrace() - } + manager.connect(device) + .useAutoConnect(false) + .retry(3, 100) + .enqueue() awaitClose { - launch { - manager.disconnect().suspend() - } + manager.disconnect().enqueue() } } } diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSScreen.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSScreen.kt index 8a1488fa..63400a73 100644 --- a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSScreen.kt +++ b/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSScreen.kt @@ -1,6 +1,5 @@ package no.nordicsemi.android.bps.view -import android.util.Log import androidx.compose.foundation.layout.Column import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -29,8 +28,6 @@ fun BPSScreen() { viewModel.onEvent(DisconnectEvent) } - Log.d("AAATESTAAA", "state: $state") - Column(modifier = Modifier.verticalScroll(rememberScrollState())) { when (state) { NoDeviceState -> NoDeviceView() diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMRepository.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMRepository.kt index 303f6645..bf20a1c6 100644 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMRepository.kt +++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMRepository.kt @@ -1,49 +1,64 @@ package no.nordicsemi.android.cgms.data -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.* -import no.nordicsemi.android.service.BleManagerStatus +import android.bluetooth.BluetoothDevice +import android.content.Context +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.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()) - val data: StateFlow = _data.asStateFlow() + private val _data = MutableStateFlow>(ConnectingResult()) + val data = _data.asStateFlow() - private val _command = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST) - val command = _command.asSharedFlow() - - private val _status = MutableStateFlow(BleManagerStatus.CONNECTING) - val status = _status.asStateFlow() - - fun emitNewBatteryLevel(batterLevel: Int) { - _data.tryEmit(_data.value.copy(batteryLevel = batterLevel)) + fun launch(device: BluetoothDevice) { + serviceManager.startService(CGMService::class.java, device) } - fun emitNewRecords(records: List) { - _data.tryEmit(_data.value.copy(records = records)) - } + fun startManager(device: BluetoothDevice, scope: CoroutineScope) { + val manager = CGMManager(context, scope) - fun setRequestStatus(requestStatus: RequestStatus) { - _data.tryEmit(_data.value.copy(requestStatus = requestStatus)) + manager.dataHolder.status.onEach { + _data.value = it + Log.d("AAATESTAAA", "data: $it") + }.launchIn(scope) + + manager.connect(device) + .useAutoConnect(false) + .retry(3, 100) + .enqueue() } fun sendNewServiceCommand(workingMode: CGMServiceCommand) { - if (_command.subscriptionCount.value > 0) { - _command.tryEmit(workingMode) - } else { - _status.tryEmit(BleManagerStatus.DISCONNECTED) - } + when (workingMode) { + CGMServiceCommand.REQUEST_ALL_RECORDS -> manager?.requestAllRecords() + CGMServiceCommand.REQUEST_LAST_RECORD -> manager?.requestLastRecord() + CGMServiceCommand.REQUEST_FIRST_RECORD -> manager?.requestFirstRecord() + CGMServiceCommand.DISCONNECT -> release() + }.exhaustive } - fun setNewStatus(status: BleManagerStatus) { - _status.value = status - } - - fun clear() { - _status.value = BleManagerStatus.CONNECTING - _data.tryEmit(CGMData()) + private fun release() { + serviceManager.stopService(CGMService::class.java) + manager?.disconnect()?.enqueue() + manager = null } } diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMManager.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMManager.kt index 8feec5f3..c22818a3 100644 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMManager.kt +++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMManager.kt @@ -26,11 +26,11 @@ import android.bluetooth.BluetoothGattCharacteristic import android.content.Context import android.util.Log import android.util.SparseArray -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.launchIn 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.cgm.CGMFeatureResponse 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.suspend 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.CGMRepository import no.nordicsemi.android.cgms.data.RequestStatus -import no.nordicsemi.android.service.BatteryManager +import no.nordicsemi.android.service.ConnectionObserverAdapter import java.util.* 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 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( context: Context, - scope: CoroutineScope, - private val repository: CGMRepository -) : BatteryManager(context, scope) { + private val scope: CoroutineScope +) : BleManager(context) { private var cgmStatusCharacteristic: BluetoothGattCharacteristic? = null private var cgmFeatureCharacteristic: BluetoothGattCharacteristic? = null @@ -69,6 +71,7 @@ internal class CGMManager( private var cgmSpecificOpsControlPointCharacteristic: BluetoothGattCharacteristic? = null private var recordAccessControlPointCharacteristic: BluetoothGattCharacteristic? = null private val records: SparseArray = SparseArray() + private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null private var secured = false @@ -80,15 +83,22 @@ internal class CGMManager( Log.e("COROUTINE-EXCEPTION", "Uncaught exception", t) } - override fun onBatteryLevelChanged(batteryLevel: Int) { - repository.emitNewBatteryLevel(batteryLevel) + private val data = MutableStateFlow(CGMData()) + val dataHolder = ConnectionObserverAdapter() + + init { + setConnectionObserver(dataHolder) + + data.onEach { + dataHolder.setValue(it) + }.launchIn(scope) } - override fun getGattCallback(): BatteryManagerGattCallback { + override fun getGattCallback(): BleManagerGattCallback { return CGMManagerGattCallback() } - private inner class CGMManagerGattCallback : BatteryManagerGattCallback() { + private inner class CGMManagerGattCallback : BleManagerGattCallback() { override fun initialize() { super.initialize() @@ -98,6 +108,12 @@ internal class CGMManager( this@CGMManager.secured = response.features.e2eCrcSupported } + scope.launch(exceptionHandler) { + val response = + readCharacteristic(cgmFeatureCharacteristic).suspendForValidResponse() + this@CGMManager.secured = response.features.e2eCrcSupported + } + scope.launch(exceptionHandler) { val response = readCharacteristic(cgmStatusCharacteristic).suspendForValidResponse() @@ -115,7 +131,8 @@ internal class CGMManager( val timestamp = sessionStartTime + it.timeOffset * 60000L val record = CGMRecord(it.timeOffset, it.glucoseConcentration, timestamp) records.put(record.sequenceNumber, record) - repository.emitNewRecords(records.toList()) + + data.value = data.value.copy(records = records.toList()) }.launchIn(scope) setIndicationCallback(cgmSpecificOpsControlPointCharacteristic).asValidResponseFlow() @@ -152,15 +169,10 @@ internal class CGMManager( } }.launchIn(scope) - scope.launch(exceptionHandler) { - enableNotifications(cgmMeasurementCharacteristic).suspend() - } - scope.launch(exceptionHandler) { - enableIndications(cgmSpecificOpsControlPointCharacteristic).suspend() - } - scope.launch(exceptionHandler) { - enableIndications(recordAccessControlPointCharacteristic).suspend() - } + enableNotifications(cgmMeasurementCharacteristic).enqueue() + enableIndications(cgmSpecificOpsControlPointCharacteristic).enqueue() + enableIndications(recordAccessControlPointCharacteristic).enqueue() + enableNotifications(batteryLevelCharacteristic).enqueue() if (sessionStartTime == 0L) { scope.launch(exceptionHandler) { @@ -179,9 +191,7 @@ internal class CGMManager( val sequenceNumber = records.keyAt(records.size() - 1) + 1 writeCharacteristic( recordAccessControlPointCharacteristic, - RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo( - sequenceNumber - ), + RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber), BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT ).suspend() } else { @@ -193,32 +203,31 @@ internal class CGMManager( } } else { recordAccessRequestInProgress = false - repository.setRequestStatus(RequestStatus.SUCCESS) + data.value = data.value.copy(requestStatus = RequestStatus.SUCCESS) } } private fun onNoRecordsFound() { recordAccessRequestInProgress = false - repository.setRequestStatus(RequestStatus.SUCCESS) + data.value = data.value.copy(requestStatus = RequestStatus.SUCCESS) } private fun onOperationCompleted(response: RecordAccessControlPointResponse) { when (response.requestCode) { - RecordAccessControlPointCallback.RACP_OP_CODE_ABORT_OPERATION -> repository.setRequestStatus( - RequestStatus.ABORTED - ) + RecordAccessControlPointCallback.RACP_OP_CODE_ABORT_OPERATION -> + data.value = data.value.copy(requestStatus = RequestStatus.ABORTED) else -> { recordAccessRequestInProgress = false - repository.setRequestStatus(RequestStatus.SUCCESS) + data.value = data.value.copy(requestStatus = RequestStatus.SUCCESS) } } } private fun onError(response: RecordAccessControlPointResponse) { 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 { - repository.setRequestStatus(RequestStatus.FAILED) + data.value = data.value.copy(requestStatus = RequestStatus.FAILED) } } @@ -255,7 +264,7 @@ internal class CGMManager( fun requestLastRecord() { if (recordAccessControlPointCharacteristic == null) return clear() - repository.setRequestStatus(RequestStatus.PENDING) + data.value = data.value.copy(requestStatus = RequestStatus.PENDING) recordAccessRequestInProgress = true scope.launch(exceptionHandler) { writeCharacteristic( @@ -269,7 +278,7 @@ internal class CGMManager( fun requestFirstRecord() { if (recordAccessControlPointCharacteristic == null) return clear() - repository.setRequestStatus(RequestStatus.PENDING) + data.value = data.value.copy(requestStatus = RequestStatus.PENDING) recordAccessRequestInProgress = true scope.launch(exceptionHandler) { writeCharacteristic( @@ -283,7 +292,7 @@ internal class CGMManager( fun requestAllRecords() { if (recordAccessControlPointCharacteristic == null) return clear() - repository.setRequestStatus(RequestStatus.PENDING) + data.value = data.value.copy(requestStatus = RequestStatus.PENDING) recordAccessRequestInProgress = true scope.launch(exceptionHandler) { writeCharacteristic( diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMService.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMService.kt index 2793e313..74eed617 100644 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMService.kt +++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMService.kt @@ -1,38 +1,27 @@ package no.nordicsemi.android.cgms.repository +import android.bluetooth.BluetoothDevice +import android.content.Intent +import androidx.lifecycle.lifecycleScope 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.CGMServiceCommand -import no.nordicsemi.android.service.ForegroundBleService -import no.nordicsemi.android.utils.exhaustive +import no.nordicsemi.android.service.DEVICE_DATA +import no.nordicsemi.android.service.NotificationService import javax.inject.Inject @AndroidEntryPoint -internal class CGMService : ForegroundBleService() { +internal class CGMService : NotificationService() { @Inject 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() { - super.onCreate() + val device = intent!!.getParcelableExtra(DEVICE_DATA)!! -// status.onEach { -// val status = it.mapToSimpleManagerStatus() -// repository.setNewStatus(status) -// stopIfDisconnected(status) -// }.launchIn(scope) + repository.startManager(device, lifecycleScope) - repository.command.onEach { - 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) + return START_REDELIVER_INTENT } } diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMScreen.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMScreen.kt index 88413141..ef18c059 100644 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMScreen.kt +++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMScreen.kt @@ -10,7 +10,13 @@ import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import no.nordicsemi.android.cgms.R 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.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 fun CGMScreen() { @@ -23,10 +29,17 @@ fun CGMScreen() { } Column(modifier = Modifier.verticalScroll(rememberScrollState())) { -// when (state) { -// is DisplayDataState -> CGMContentView(state.data) { viewModel.onEvent(it) } -// LoadingState -> DeviceConnectingView() -// }.exhaustive + when (state) { + NoDeviceState -> NoDeviceView() + is WorkingState -> when (state.result) { + 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 } } } diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMViewEvent.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMViewEvent.kt index 865116da..3edc2976 100644 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMViewEvent.kt +++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMViewEvent.kt @@ -6,4 +6,6 @@ internal sealed class CGMViewEvent internal data class OnWorkingModeSelected(val workingMode: CGMServiceCommand) : CGMViewEvent() +internal object NavigateUp : CGMViewEvent() + internal object DisconnectEvent : CGMViewEvent() diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMViewState.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMViewState.kt index aad60987..f25acb02 100644 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMViewState.kt +++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMViewState.kt @@ -1,9 +1,9 @@ package no.nordicsemi.android.cgms.view 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 DisplayDataState(val data: CGMData) : CGMViewState() +internal data class WorkingState(val result: BleManagerResult) : BPSViewState() +internal object NoDeviceState : BPSViewState() diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/viewmodel/CGMScreenViewModel.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/viewmodel/CGMScreenViewModel.kt index 74c5367c..cb9f3b7e 100644 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/viewmodel/CGMScreenViewModel.kt +++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/viewmodel/CGMScreenViewModel.kt @@ -1,36 +1,32 @@ package no.nordicsemi.android.cgms.viewmodel +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope 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.CGMServiceCommand 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.navigation.* -import no.nordicsemi.android.service.BleManagerStatus -import no.nordicsemi.android.service.ServiceManager import no.nordicsemi.android.utils.exhaustive import no.nordicsemi.android.utils.getDevice +import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice import no.nordicsemi.ui.scanner.ScannerDestinationId import javax.inject.Inject @HiltViewModel internal class CGMScreenViewModel @Inject constructor( private val repository: CGMRepository, - private val serviceManager: ServiceManager, private val navigationManager: NavigationManager ) : ViewModel() { - val state = repository.data.combine(repository.status) { data, status -> -// when (status) { -// BleManagerStatus.CONNECTING -> LoadingState -// BleManagerStatus.OK, -// BleManagerStatus.DISCONNECTED -> DisplayDataState(data) -// } - }.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState) + private val _state = MutableStateFlow(NoDeviceState) + val state = _state.asStateFlow() init { navigationManager.navigateTo(ScannerDestinationId, UUIDArgument(CGMS_SERVICE_UUID)) @@ -41,17 +37,16 @@ internal class CGMScreenViewModel @Inject constructor( } }.launchIn(viewModelScope) - repository.status.onEach { - if (it == BleManagerStatus.DISCONNECTED) { - navigationManager.navigateUp() - } + repository.data.onEach { + _state.value = WorkingState(it) + Log.d("AAATESTAAA", "vm data: $it") }.launchIn(viewModelScope) } private fun handleArgs(args: DestinationResult) { when (args) { is CancelDestinationResult -> navigationManager.navigateUp() - is SuccessDestinationResult -> serviceManager.startService(CGMService::class.java, args.getDevice()) + is SuccessDestinationResult -> connectDevice(args.getDevice()) }.exhaustive } @@ -59,16 +54,15 @@ internal class CGMScreenViewModel @Inject constructor( when (event) { DisconnectEvent -> disconnect() is OnWorkingModeSelected -> repository.sendNewServiceCommand(event.workingMode) + NavigateUp -> navigationManager.navigateUp() }.exhaustive } + private fun connectDevice(deviceHolder: DiscoveredBluetoothDevice) { + repository.launch(deviceHolder.device) + } + private fun disconnect() { repository.sendNewServiceCommand(CGMServiceCommand.DISCONNECT) - repository.clear() - } - - override fun onCleared() { - super.onCleared() - repository.clear() } } diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreen.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreen.kt index 7e9393fb..042bd900 100644 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreen.kt +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreen.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource diff --git a/settings.gradle b/settings.gradle index 745384b1..6ca60227 100644 --- a/settings.gradle +++ b/settings.gradle @@ -37,7 +37,7 @@ dependencyResolutionManagement { alias('compose-activity').to('androidx.activity:activity-compose:1.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-ui').to('androidx.compose.ui', 'ui').versionRef('compose') alias('compose-material').to('androidx.compose.material3:material3:1.0.0-alpha04')