diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/BleManagerStatus.kt b/lib_service/src/main/java/no/nordicsemi/android/service/BleManagerStatus.kt new file mode 100644 index 00000000..1ce97078 --- /dev/null +++ b/lib_service/src/main/java/no/nordicsemi/android/service/BleManagerStatus.kt @@ -0,0 +1,5 @@ +package no.nordicsemi.android.service + +enum class BleManagerStatus { + CONNECTING, OK, DISCONNECTED +} diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/BleProfileService.kt b/lib_service/src/main/java/no/nordicsemi/android/service/BleProfileService.kt index bda5a2a9..dfc280a8 100644 --- a/lib_service/src/main/java/no/nordicsemi/android/service/BleProfileService.kt +++ b/lib_service/src/main/java/no/nordicsemi/android/service/BleProfileService.kt @@ -21,22 +21,34 @@ */ package no.nordicsemi.android.service +import android.app.Service import android.bluetooth.BluetoothDevice import android.content.Intent import android.os.Handler +import android.os.IBinder +import android.util.Log import android.widget.Toast import androidx.lifecycle.LifecycleService import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import no.nordicsemi.android.ble.BleManager import no.nordicsemi.android.log.ILogSession import no.nordicsemi.android.log.Logger import javax.inject.Inject @AndroidEntryPoint -abstract class BleProfileService : LifecycleService() { +abstract class BleProfileService : Service() { + + protected val scope = CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) protected abstract val manager: BleManager + private val _status = MutableStateFlow(BleManagerStatus.CONNECTING) + val status = _status.asStateFlow() + @Inject lateinit var bluetoothDeviceHolder: SelectedBluetoothDeviceHolder @@ -71,6 +83,23 @@ abstract class BleProfileService : LifecycleService() { override fun onCreate() { super.onCreate() handler = Handler() + + manager.setConnectionObserver(object : ConnectionObserverAdapter() { + override fun onDeviceConnected(device: BluetoothDevice) { + super.onDeviceConnected(device) + _status.value = BleManagerStatus.OK + } + + override fun onDeviceDisconnected(device: BluetoothDevice, reason: Int) { + super.onDeviceDisconnected(device, reason) + _status.value = BleManagerStatus.DISCONNECTED + scope.close() + } + }) + } + + override fun onBind(intent: Intent?): IBinder? { + return null } /** @@ -89,6 +118,7 @@ abstract class BleProfileService : LifecycleService() { .useAutoConnect(shouldAutoConnect()) .retry(3, 100) .enqueue() + return START_REDELIVER_INTENT } diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/CloseableCoroutineScope.kt b/lib_service/src/main/java/no/nordicsemi/android/service/CloseableCoroutineScope.kt new file mode 100644 index 00000000..d2c1b22f --- /dev/null +++ b/lib_service/src/main/java/no/nordicsemi/android/service/CloseableCoroutineScope.kt @@ -0,0 +1,14 @@ +package no.nordicsemi.android.service + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import java.io.Closeable +import kotlin.coroutines.CoroutineContext + +class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope { + override val coroutineContext: CoroutineContext = context + + override fun close() { + coroutineContext.cancel() + } +} 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 new file mode 100644 index 00000000..cb3f90ac --- /dev/null +++ b/lib_service/src/main/java/no/nordicsemi/android/service/ConnectionObserverAdapter.kt @@ -0,0 +1,19 @@ +package no.nordicsemi.android.service + +import android.bluetooth.BluetoothDevice +import no.nordicsemi.android.ble.observer.ConnectionObserver + +abstract class ConnectionObserverAdapter : ConnectionObserver { + + override fun onDeviceConnecting(device: BluetoothDevice) { } + + override fun onDeviceConnected(device: BluetoothDevice) { } + + override fun onDeviceFailedToConnect(device: BluetoothDevice, reason: Int) { } + + override fun onDeviceReady(device: BluetoothDevice) { } + + override fun onDeviceDisconnecting(device: BluetoothDevice) { } + + override fun onDeviceDisconnected(device: BluetoothDevice, reason: Int) { } +} diff --git a/lib_theme/build.gradle b/lib_theme/build.gradle index 273b27ee..5f8dbf44 100644 --- a/lib_theme/build.gradle +++ b/lib_theme/build.gradle @@ -6,6 +6,7 @@ dependencies { implementation libs.nordic.theme implementation libs.bundles.compose + implementation libs.bundles.icons implementation libs.compose.lifecycle implementation libs.compose.activity } diff --git a/lib_theme/src/main/java/no/nordicsemi/android/theme/view/DeviceConnectingView.kt b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/DeviceConnectingView.kt new file mode 100644 index 00000000..cb498fa7 --- /dev/null +++ b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/DeviceConnectingView.kt @@ -0,0 +1,73 @@ +package no.nordicsemi.android.theme.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.HourglassTop +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.theme.R + +@Composable +fun DeviceConnectingView() { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + ScreenSection { + Icon( + imageVector = Icons.Default.HourglassTop, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSecondary, + modifier = Modifier + .background( + color = MaterialTheme.colorScheme.secondary, + shape = CircleShape + ) + .padding(8.dp) + ) + + Spacer(modifier = Modifier.size(16.dp)) + + Text( + text = stringResource(id = R.string.device_connecting), + style = MaterialTheme.typography.titleMedium + ) + + Spacer(modifier = Modifier.size(16.dp)) + + Text( + text = stringResource(id = R.string.device_explanation), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium + ) + + Spacer(modifier = Modifier.size(16.dp)) + + Text( + text = stringResource(id = R.string.device_please_wait), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleLarge + ) + } + } +} + +@Preview +@Composable +fun DeviceConnectingView_Preview() { + DeviceConnectingView() +} diff --git a/lib_theme/src/main/res/values/strings.xml b/lib_theme/src/main/res/values/strings.xml index b5bbd16c..570f2f80 100644 --- a/lib_theme/src/main/res/values/strings.xml +++ b/lib_theme/src/main/res/values/strings.xml @@ -10,4 +10,8 @@ DISCONNECT Battery + + Connecting + The mobile is trying to connect to peripheral device. + Please wait. \ No newline at end of file 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 7aa81900..19749ca9 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 @@ -2,6 +2,7 @@ package no.nordicsemi.android.cgms.data import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.* +import no.nordicsemi.android.service.BleManagerStatus import javax.inject.Inject import javax.inject.Singleton @@ -11,9 +12,12 @@ internal class CGMRepository @Inject constructor() { private val _data = MutableStateFlow(CGMData()) val data: StateFlow = _data.asStateFlow() - private val _command = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST) + 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)) } @@ -26,10 +30,14 @@ internal class CGMRepository @Inject constructor() { _data.tryEmit(_data.value.copy(requestStatus = requestStatus)) } - fun requestNewWorkingMode(workingMode: WorkingMode) { + fun sendNewServiceCommand(workingMode: CGMServiceCommand) { _command.tryEmit(workingMode) } + fun setNewStatus(status: BleManagerStatus) { + _status.value = status + } + fun clear() { _data.tryEmit(CGMData()) } diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMServiceCommand.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMServiceCommand.kt new file mode 100644 index 00000000..486b1da2 --- /dev/null +++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMServiceCommand.kt @@ -0,0 +1,8 @@ +package no.nordicsemi.android.cgms.data + +internal enum class CGMServiceCommand { + REQUEST_ALL_RECORDS, + REQUEST_LAST_RECORD, + REQUEST_FIRST_RECORD, + DISCONNECT +} diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/WorkingMode.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/WorkingMode.kt deleted file mode 100644 index d2f9bc52..00000000 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/WorkingMode.kt +++ /dev/null @@ -1,7 +0,0 @@ -package no.nordicsemi.android.cgms.data - -internal enum class WorkingMode { - ALL, - LAST, - FIRST -} 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 7b7e15e9..5ce8f8da 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,11 +1,10 @@ package no.nordicsemi.android.cgms.repository -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.WorkingMode +import no.nordicsemi.android.cgms.data.CGMServiceCommand import no.nordicsemi.android.service.ForegroundBleService import no.nordicsemi.android.utils.exhaustive import javax.inject.Inject @@ -14,19 +13,24 @@ import javax.inject.Inject internal class CGMService : ForegroundBleService() { @Inject - lateinit var dataHolder: CGMRepository + lateinit var repository: CGMRepository - override val manager: CGMManager by lazy { CGMManager(this, dataHolder) } + override val manager: CGMManager by lazy { CGMManager(this, repository) } override fun onCreate() { super.onCreate() - dataHolder.command.onEach { + status.onEach { + repository.setNewStatus(it) + }.launchIn(scope) + + repository.command.onEach { when (it) { - WorkingMode.ALL -> manager.requestAllRecords() - WorkingMode.LAST -> manager.requestLastRecord() - WorkingMode.FIRST -> manager.requestFirstRecord() + CGMServiceCommand.REQUEST_ALL_RECORDS -> manager.requestAllRecords() + CGMServiceCommand.REQUEST_LAST_RECORD -> manager.requestLastRecord() + CGMServiceCommand.REQUEST_FIRST_RECORD -> manager.requestFirstRecord() + CGMServiceCommand.DISCONNECT -> stopSelf() }.exhaustive - }.launchIn(lifecycleScope) + }.launchIn(scope) } } diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMContentView.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMContentView.kt index 21011594..f1f12c30 100644 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMContentView.kt +++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMContentView.kt @@ -18,7 +18,7 @@ import no.nordicsemi.android.cgms.R import no.nordicsemi.android.cgms.data.CGMData import no.nordicsemi.android.cgms.data.CGMRecord import no.nordicsemi.android.cgms.data.RequestStatus -import no.nordicsemi.android.cgms.data.WorkingMode +import no.nordicsemi.android.cgms.data.CGMServiceCommand import no.nordicsemi.android.material.you.CircularProgressIndicator import no.nordicsemi.android.theme.view.BatteryLevelView import no.nordicsemi.android.theme.view.ScreenSection @@ -29,12 +29,10 @@ internal fun CGMContentView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) { Column( modifier = Modifier .fillMaxSize() - .padding(horizontal = 16.dp) + .padding(16.dp) .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally ) { - Spacer(modifier = Modifier.height(16.dp)) - SettingsView(state, onEvent) Spacer(modifier = Modifier.height(16.dp)) @@ -71,10 +69,14 @@ private fun SettingsView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) { if (state.requestStatus == RequestStatus.PENDING) { CircularProgressIndicator() } else { - WorkingMode.values().forEach { - Button(onClick = { onEvent(OnWorkingModeSelected(it)) }) { - Text(it.toDisplayString()) - } + Button(onClick = { onEvent(OnWorkingModeSelected(CGMServiceCommand.REQUEST_ALL_RECORDS)) }) { + Text(stringResource(id = R.string.cgms__working_mode__all)) + } + Button(onClick = { onEvent(OnWorkingModeSelected(CGMServiceCommand.REQUEST_LAST_RECORD)) }) { + Text(stringResource(id = R.string.cgms__working_mode__last)) + } + Button(onClick = { onEvent(OnWorkingModeSelected(CGMServiceCommand.REQUEST_FIRST_RECORD)) }) { + Text(stringResource(id = R.string.cgms__working_mode__first)) } } } diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMMapper.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMMapper.kt index 42bbf071..3fb7871b 100644 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMMapper.kt +++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMMapper.kt @@ -4,19 +4,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import no.nordicsemi.android.cgms.R import no.nordicsemi.android.cgms.data.CGMRecord -import no.nordicsemi.android.cgms.data.WorkingMode +import no.nordicsemi.android.cgms.data.CGMServiceCommand import java.text.SimpleDateFormat import java.util.* -@Composable -internal fun WorkingMode.toDisplayString(): String { - return when (this) { - WorkingMode.ALL -> stringResource(id = R.string.cgms__working_mode__all) - WorkingMode.LAST -> stringResource(id = R.string.cgms__working_mode__last) - WorkingMode.FIRST -> stringResource(id = R.string.cgms__working_mode__first) - } -} - internal fun CGMRecord.formattedTime(): String { val timeFormat = SimpleDateFormat("dd.MM.yyyy HH:mm", Locale.US) return timeFormat.format(Date(timestamp)) 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 33be257e..32a53631 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 @@ -9,49 +9,43 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import no.nordicsemi.android.cgms.R -import no.nordicsemi.android.cgms.data.CGMData import no.nordicsemi.android.cgms.repository.CGMService import no.nordicsemi.android.cgms.viewmodel.CGMScreenViewModel import no.nordicsemi.android.theme.view.BackIconAppBar -import no.nordicsemi.android.utils.isServiceRunning +import no.nordicsemi.android.theme.view.DeviceConnectingView +import no.nordicsemi.android.utils.exhaustive @Composable fun CGMScreen(finishAction: () -> Unit) { val viewModel: CGMScreenViewModel = hiltViewModel() val state = viewModel.state.collectAsState().value - val isScreenActive = viewModel.isActive.collectAsState().value val context = LocalContext.current - LaunchedEffect(isScreenActive) { - if (!isScreenActive) { - finishAction() - } - if (context.isServiceRunning(CGMService::class.java.name)) { - val intent = Intent(context, CGMService::class.java) - context.stopService(intent) - } - } - - LaunchedEffect("start-service") { - if (!context.isServiceRunning(CGMService::class.java.name)) { + LaunchedEffect(state.isActive) { + if (state.isActive) { val intent = Intent(context, CGMService::class.java) context.startService(intent) + } else if (!state.isActive) { + finishAction() } } - CGMView(state) { + CGMView(state.viewState) { viewModel.onEvent(it) } } @Composable -private fun CGMView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) { +private fun CGMView(state: CGMViewState, onEvent: (CGMViewEvent) -> Unit) { Column { BackIconAppBar(stringResource(id = R.string.cgms_title)) { onEvent(DisconnectEvent) } - CGMContentView(state, onEvent) + when (state) { + is DisplayDataState -> CGMContentView(state.data, onEvent) + LoadingState -> DeviceConnectingView() + }.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 7599efab..865116da 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 @@ -1,9 +1,9 @@ package no.nordicsemi.android.cgms.view -import no.nordicsemi.android.cgms.data.WorkingMode +import no.nordicsemi.android.cgms.data.CGMServiceCommand internal sealed class CGMViewEvent -internal data class OnWorkingModeSelected(val workingMode: WorkingMode) : CGMViewEvent() +internal data class OnWorkingModeSelected(val workingMode: CGMServiceCommand) : 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 new file mode 100644 index 00000000..b2f1116b --- /dev/null +++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMViewState.kt @@ -0,0 +1,14 @@ +package no.nordicsemi.android.cgms.view + +import no.nordicsemi.android.cgms.data.CGMData + +internal data class CGMState( + val viewState: CGMViewState, + val isActive: Boolean = true +) + +internal sealed class CGMViewState + +internal object LoadingState : CGMViewState() + +internal data class DisplayDataState(val data: CGMData) : CGMViewState() 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 17c99d4f..997fccab 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,30 +1,45 @@ package no.nordicsemi.android.cgms.viewmodel +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn import no.nordicsemi.android.cgms.data.CGMRepository +import no.nordicsemi.android.cgms.data.CGMServiceCommand +import no.nordicsemi.android.cgms.view.CGMState import no.nordicsemi.android.cgms.view.CGMViewEvent import no.nordicsemi.android.cgms.view.DisconnectEvent +import no.nordicsemi.android.cgms.view.DisplayDataState +import no.nordicsemi.android.cgms.view.LoadingState import no.nordicsemi.android.cgms.view.OnWorkingModeSelected -import no.nordicsemi.android.theme.viewmodel.CloseableViewModel +import no.nordicsemi.android.service.BleManagerStatus import no.nordicsemi.android.utils.exhaustive import javax.inject.Inject @HiltViewModel internal class CGMScreenViewModel @Inject constructor( - private val dataHolder: CGMRepository -) : CloseableViewModel() { + private val repository: CGMRepository +) : ViewModel() { - val state = dataHolder.data + val state = repository.data.combine(repository.status) { data, status -> + when (status) { + BleManagerStatus.CONNECTING -> CGMState(LoadingState) + BleManagerStatus.OK -> CGMState(DisplayDataState(data)) + BleManagerStatus.DISCONNECTED -> CGMState(DisplayDataState(data), false) + } + }.stateIn(viewModelScope, SharingStarted.Lazily, CGMState(LoadingState)) fun onEvent(event: CGMViewEvent) { when (event) { DisconnectEvent -> disconnect() - is OnWorkingModeSelected -> dataHolder.requestNewWorkingMode(event.workingMode) + is OnWorkingModeSelected -> repository.sendNewServiceCommand(event.workingMode) }.exhaustive } private fun disconnect() { - finish() - dataHolder.clear() + repository.clear() + repository.sendNewServiceCommand(CGMServiceCommand.DISCONNECT) } } diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCContentView.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCContentView.kt index 07e27442..08d98610 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCContentView.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCContentView.kt @@ -4,6 +4,8 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Button @@ -26,24 +28,24 @@ internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) { SelectWheelSizeDialog { onEvent(it) } } - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(horizontal = 16.dp) - ) { - Spacer(modifier = Modifier.height(16.dp)) - - SettingsSection(state, onEvent) - - Spacer(modifier = Modifier.height(16.dp)) - - SensorsReadingView(state = state) - - Spacer(modifier = Modifier.height(16.dp)) - - Button( - onClick = { onEvent(OnDisconnectButtonClick) } + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(16.dp) ) { - Text(text = stringResource(id = R.string.disconnect)) + SettingsSection(state, onEvent) + + Spacer(modifier = Modifier.height(16.dp)) + + SensorsReadingView(state = state) + + Spacer(modifier = Modifier.height(16.dp)) + + Button( + onClick = { onEvent(OnDisconnectButtonClick) } + ) { + Text(text = stringResource(id = R.string.disconnect)) + } } } } diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/service/PRXService.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/service/PRXService.kt index 675981e0..2d71f6ef 100644 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/service/PRXService.kt +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/service/PRXService.kt @@ -39,7 +39,7 @@ internal class PRXService : ForegroundBleService() { DisableAlarm -> manager.writeImmediateAlert(false) EnableAlarm -> manager.writeImmediateAlert(true) }.exhaustive - }.launchIn(lifecycleScope) + }.launchIn(scope) dataHolder.data.onEach { if (it.localAlarmLevel != AlarmLevel.NONE) { @@ -47,7 +47,7 @@ internal class PRXService : ForegroundBleService() { } else { alarmHandler.pauseAlarm() } - }.launchIn(lifecycleScope) + }.launchIn(scope) } override fun onDestroy() { diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCMeasurementParser.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCMeasurementParser.kt deleted file mode 100644 index d0246d2c..00000000 --- a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCMeasurementParser.kt +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (c) 2015, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE - * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package no.nordicsemi.android.rscs.service - -import no.nordicsemi.android.ble.data.Data -import java.util.* - -internal object RSCMeasurementParser { - - private const val INSTANTANEOUS_STRIDE_LENGTH_PRESENT: Byte = 0x01 // 1 bit - private const val TOTAL_DISTANCE_PRESENT: Byte = 0x02 // 1 bit - private const val WALKING_OR_RUNNING_STATUS_BITS: Byte = 0x04 // 1 bit - - fun parse(data: Data): String { - var offset = 0 - val flags = data.value!![offset].toInt() // 1 byte - offset += 1 - val islmPresent = flags and INSTANTANEOUS_STRIDE_LENGTH_PRESENT.toInt() > 0 - val tdPreset = flags and TOTAL_DISTANCE_PRESENT.toInt() > 0 - val running = flags and WALKING_OR_RUNNING_STATUS_BITS.toInt() > 0 - val walking = !running - val instantaneousSpeed = - data.getIntValue(Data.FORMAT_UINT16, offset) as Float / 256.0f // 1/256 m/s - offset += 2 - val instantaneousCadence = data.getIntValue(Data.FORMAT_UINT8, offset)!! - offset += 1 - var instantaneousStrideLength = 0f - if (islmPresent) { - instantaneousStrideLength = - data.getIntValue(Data.FORMAT_UINT16, offset) as Float / 100.0f // 1/100 m - offset += 2 - } - var totalDistance = 0f - if (tdPreset) { - totalDistance = data.getIntValue(Data.FORMAT_UINT32, offset) as Float / 10.0f - // offset += 4; - } - val builder = StringBuilder() - builder.append( - String.format( - Locale.US, - "Speed: %.2f m/s, Cadence: %d RPM,\n", - instantaneousSpeed, - instantaneousCadence - ) - ) - if (islmPresent) builder.append( - String.format( - Locale.US, - "Instantaneous Stride Length: %.2f m,\n", - instantaneousStrideLength - ) - ) - if (tdPreset) builder.append( - String.format( - Locale.US, - "Total Distance: %.1f m,\n", - totalDistance - ) - ) - if (walking) builder.append("Status: WALKING") else builder.append("Status: RUNNING") - return builder.toString() - } -} diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCSManager.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCSManager.kt index 99982c00..a84847db 100644 --- a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCSManager.kt +++ b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCSManager.kt @@ -26,8 +26,6 @@ import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCharacteristic import android.content.Context import no.nordicsemi.android.ble.common.callback.rsc.RunningSpeedAndCadenceMeasurementDataCallback -import no.nordicsemi.android.ble.data.Data -import no.nordicsemi.android.log.LogContract import no.nordicsemi.android.rscs.data.RSCSRepository import no.nordicsemi.android.service.BatteryManager import java.util.* @@ -46,13 +44,6 @@ internal class RSCSManager internal constructor( private var rscMeasurementCharacteristic: BluetoothGattCharacteristic? = null private val callback = object : RunningSpeedAndCadenceMeasurementDataCallback() { - override fun onDataReceived(device: BluetoothDevice, data: Data) { - log( - LogContract.Log.Level.APPLICATION, - "\"" + RSCMeasurementParser.parse(data).toString() + "\" received" - ) - super.onDataReceived(device, data) - } override fun onRSCMeasurementReceived( device: BluetoothDevice, diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTRepository.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTRepository.kt index 5fbf9934..82c0faaf 100644 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTRepository.kt +++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTRepository.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import no.nordicsemi.android.service.BleManagerStatus import javax.inject.Inject import javax.inject.Singleton @@ -17,6 +18,9 @@ internal class UARTRepository @Inject constructor() { 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 addNewMacro(macro: UARTMacro) { _data.tryEmit(_data.value.copy(macros = _data.value.macros + macro)) } @@ -39,4 +43,8 @@ internal class UARTRepository @Inject constructor() { fun sendNewCommand(command: UARTServiceCommand) { _command.tryEmit(command) } + + fun setNewStatus(status: BleManagerStatus) { + _status.value = status + } } diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTServiceCommand.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTServiceCommand.kt index eb6fb906..d7235ede 100644 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTServiceCommand.kt +++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTServiceCommand.kt @@ -1,3 +1,7 @@ package no.nordicsemi.android.uart.data -data class UARTServiceCommand(val command: String) +internal sealed class UARTServiceCommand + +internal data class SendTextCommand(val command: String) : UARTServiceCommand() + +internal object DisconnectCommand : UARTServiceCommand() diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTService.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTService.kt index 282145c8..19301c10 100644 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTService.kt +++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTService.kt @@ -1,26 +1,35 @@ package no.nordicsemi.android.uart.repository -import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import no.nordicsemi.android.service.ForegroundBleService +import no.nordicsemi.android.uart.data.DisconnectCommand +import no.nordicsemi.android.uart.data.SendTextCommand import no.nordicsemi.android.uart.data.UARTRepository +import no.nordicsemi.android.utils.exhaustive import javax.inject.Inject @AndroidEntryPoint internal class UARTService : ForegroundBleService() { @Inject - lateinit var dataHolder: UARTRepository + lateinit var repository: UARTRepository - override val manager: UARTManager by lazy { UARTManager(this, dataHolder) } + override val manager: UARTManager by lazy { UARTManager(this, repository) } override fun onCreate() { super.onCreate() - dataHolder.command.onEach { - manager.send(it.command) - }.launchIn(lifecycleScope) + status.onEach { + repository.setNewStatus(it) + }.launchIn(scope) + + repository.command.onEach { + when (it) { + DisconnectCommand -> stopSelf() + is SendTextCommand -> manager.send(it.command) + }.exhaustive + }.launchIn(scope) } } diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTScreen.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTScreen.kt index ddbddeff..97ae8ed9 100644 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTScreen.kt +++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTScreen.kt @@ -9,46 +9,40 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import no.nordicsemi.android.theme.view.BackIconAppBar +import no.nordicsemi.android.theme.view.DeviceConnectingView import no.nordicsemi.android.uart.R -import no.nordicsemi.android.uart.data.UARTData import no.nordicsemi.android.uart.repository.UARTService import no.nordicsemi.android.uart.viewmodel.UARTViewModel -import no.nordicsemi.android.utils.isServiceRunning +import no.nordicsemi.android.utils.exhaustive @Composable fun UARTScreen(finishAction: () -> Unit) { val viewModel: UARTViewModel = hiltViewModel() val state = viewModel.state.collectAsState().value - val isScreenActive = viewModel.isActive.collectAsState().value val context = LocalContext.current - LaunchedEffect(isScreenActive) { - if (!isScreenActive) { - finishAction() - } - if (context.isServiceRunning(UARTService::class.java.name)) { - val intent = Intent(context, UARTService::class.java) - context.stopService(intent) - } - } - - LaunchedEffect("start-service") { - if (!context.isServiceRunning(UARTService::class.java.name)) { + LaunchedEffect(state.isActive) { + if (state.isActive) { val intent = Intent(context, UARTService::class.java) context.startService(intent) + } else if (!state.isActive) { + finishAction() } } - UARTView(state) { viewModel.onEvent(it) } + UARTView(state.viewState) { viewModel.onEvent(it) } } @Composable -private fun UARTView(state: UARTData, onEvent: (UARTViewEvent) -> Unit) { +private fun UARTView(state: UARTViewState, onEvent: (UARTViewEvent) -> Unit) { Column { BackIconAppBar(stringResource(id = R.string.uart_title)) { onEvent(OnDisconnectButtonClick) } - UARTContentView(state) { onEvent(it) } + when (state) { + is DisplayDataState -> UARTContentView(state.data) { onEvent(it) } + LoadingState -> DeviceConnectingView() + }.exhaustive } } diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTState.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTState.kt new file mode 100644 index 00000000..823a336b --- /dev/null +++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTState.kt @@ -0,0 +1,14 @@ +package no.nordicsemi.android.uart.view + +import no.nordicsemi.android.uart.data.UARTData + +internal data class UARTState( + val viewState: UARTViewState, + val isActive: Boolean = true +) + +internal sealed class UARTViewState + +internal object LoadingState : UARTViewState() + +internal data class DisplayDataState(val data: UARTData) : UARTViewState() diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/viewmodel/UARTViewModel.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/viewmodel/UARTViewModel.kt index 322ce94d..137a3894 100644 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/viewmodel/UARTViewModel.kt +++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/viewmodel/UARTViewModel.kt @@ -1,26 +1,46 @@ package no.nordicsemi.android.uart.viewmodel +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import no.nordicsemi.android.service.BleManagerStatus import no.nordicsemi.android.theme.viewmodel.CloseableViewModel +import no.nordicsemi.android.uart.data.DisconnectCommand +import no.nordicsemi.android.uart.data.SendTextCommand import no.nordicsemi.android.uart.data.UARTRepository -import no.nordicsemi.android.uart.data.UARTServiceCommand -import no.nordicsemi.android.uart.view.* +import no.nordicsemi.android.uart.view.DisplayDataState +import no.nordicsemi.android.uart.view.LoadingState +import no.nordicsemi.android.uart.view.OnCreateMacro +import no.nordicsemi.android.uart.view.OnDeleteMacro +import no.nordicsemi.android.uart.view.OnDisconnectButtonClick +import no.nordicsemi.android.uart.view.OnRunMacro +import no.nordicsemi.android.uart.view.UARTState +import no.nordicsemi.android.uart.view.UARTViewEvent import no.nordicsemi.android.utils.exhaustive import javax.inject.Inject @HiltViewModel internal class UARTViewModel @Inject constructor( - private val dataHolder: UARTRepository -) : CloseableViewModel() { + private val repository: UARTRepository +) : ViewModel() { - val state = dataHolder.data + val state = repository.data.combine(repository.status) { data, status -> + when (status) { + BleManagerStatus.CONNECTING -> UARTState(LoadingState) + BleManagerStatus.OK -> UARTState(DisplayDataState(data)) + BleManagerStatus.DISCONNECTED -> UARTState(DisplayDataState(data), false) + } + }.stateIn(viewModelScope, SharingStarted.Lazily, UARTState(LoadingState)) fun onEvent(event: UARTViewEvent) { when (event) { - is OnCreateMacro -> dataHolder.addNewMacro(event.macro) - is OnDeleteMacro -> dataHolder.deleteMacro(event.macro) - OnDisconnectButtonClick -> finish() - is OnRunMacro -> dataHolder.sendNewCommand(UARTServiceCommand(event.macro.command)) + is OnCreateMacro -> repository.addNewMacro(event.macro) + is OnDeleteMacro -> repository.deleteMacro(event.macro) + OnDisconnectButtonClick -> repository.sendNewCommand(DisconnectCommand) + is OnRunMacro -> repository.sendNewCommand(SendTextCommand(event.macro.command)) }.exhaustive } } diff --git a/settings.gradle b/settings.gradle index beda6bd0..be26bef9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -37,6 +37,10 @@ dependencyResolutionManagement { alias('compose-navigation').to('androidx.navigation:navigation-compose:2.4.0-alpha09') bundle('compose', ['compose-livedata', 'compose-ui', 'compose-material', 'compose-tooling-preview', 'compose-navigation']) + alias('material-icons').to('androidx.compose.material', 'material-icons-core').versionRef('compose') + alias('material-icons-extended').to('androidx.compose.material', 'material-icons-extended').versionRef('compose') + bundle('icons', ['material-icons', 'material-icons-extended']) + version('hilt', '2.38.1') alias('hilt-android').to('com.google.dagger', 'hilt-android').versionRef('hilt') alias('hilt-compiler').to('com.google.dagger', 'hilt-compiler').versionRef('hilt')