diff --git a/app/src/androidTest/java/no/nordicsemi/android/nrftoolbox/ExampleInstrumentedTest.kt b/app/src/androidTest/java/no/nordicsemi/android/nrftoolbox/ExampleInstrumentedTest.kt index 3b505247..8c2487ff 100644 --- a/app/src/androidTest/java/no/nordicsemi/android/nrftoolbox/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/no/nordicsemi/android/nrftoolbox/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package no.nordicsemi.android.nrftoolbox -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt index aeee333c..b6266847 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt @@ -2,7 +2,6 @@ package no.nordicsemi.android.nrftoolbox import android.app.Activity import android.os.ParcelUuid -import android.util.Log import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -46,15 +45,15 @@ internal fun HomeScreen() { navController = navController, startDestination = NavigationId.HOME.id ) { + composable(NavigationId.HOME.id) { + HomeView(viewModel) + } composable(NavigationId.SCANNER.id) { val profile = viewModel.profile!! FindDeviceScreen(ParcelUuid(profile.uuid)) { viewModel.onScannerFlowResult(it) } } - composable(NavigationId.HOME.id) { - HomeView(viewModel) - } composable(NavigationId.CSC.id) { CSCScreen(navigateUp) } diff --git a/app/src/test/java/no/nordicsemi/android/nrftoolbox/ExampleUnitTest.kt b/app/src/test/java/no/nordicsemi/android/nrftoolbox/ExampleUnitTest.kt index 7faa27e0..f5fcf868 100644 --- a/app/src/test/java/no/nordicsemi/android/nrftoolbox/ExampleUnitTest.kt +++ b/app/src/test/java/no/nordicsemi/android/nrftoolbox/ExampleUnitTest.kt @@ -1,9 +1,8 @@ package no.nordicsemi.android.nrftoolbox +import org.junit.Assert.assertEquals import org.junit.Test -import org.junit.Assert.* - /** * Example local unit test, which will execute on the development machine (host). * diff --git a/lib_service/src/androidTest/java/no/nordicsemi/android/service/ExampleInstrumentedTest.kt b/lib_service/src/androidTest/java/no/nordicsemi/android/service/ExampleInstrumentedTest.kt index f0343138..0bca298d 100644 --- a/lib_service/src/androidTest/java/no/nordicsemi/android/service/ExampleInstrumentedTest.kt +++ b/lib_service/src/androidTest/java/no/nordicsemi/android/service/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package no.nordicsemi.android.service -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/BatteryManager.kt b/lib_service/src/main/java/no/nordicsemi/android/service/BatteryManager.kt index e93cecd1..30c1bb28 100644 --- a/lib_service/src/main/java/no/nordicsemi/android/service/BatteryManager.kt +++ b/lib_service/src/main/java/no/nordicsemi/android/service/BatteryManager.kt @@ -21,6 +21,8 @@ import java.util.* */ abstract class BatteryManager(context: Context) : BleManager(context) { + private val TAG = "BLE-MANAGER" + private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null private val batteryLevelDataCallback: DataReceivedCallback = @@ -67,6 +69,11 @@ abstract class BatteryManager(context: Context) : BleManager(context) { } } + override fun log(priority: Int, message: String) { + super.log(priority, message) + Log.println(priority, TAG, message) + } + protected abstract inner class BatteryManagerGattCallback : BleManagerGattCallback() { override fun initialize() { readBatteryLevelCharacteristic() 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 dfc280a8..fc69b65a 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 @@ -26,9 +26,7 @@ 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 @@ -90,6 +88,13 @@ abstract class BleProfileService : Service() { _status.value = BleManagerStatus.OK } + override fun onDeviceFailedToConnect(device: BluetoothDevice, reason: Int) { + super.onDeviceFailedToConnect(device, reason) + _status.value = BleManagerStatus.DISCONNECTED + stopSelf() + scope.close() + } + override fun onDeviceDisconnected(device: BluetoothDevice, reason: Int) { super.onDeviceDisconnected(device, reason) _status.value = BleManagerStatus.DISCONNECTED 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 index d2c1b22f..ae35f3d5 100644 --- a/lib_service/src/main/java/no/nordicsemi/android/service/CloseableCoroutineScope.kt +++ b/lib_service/src/main/java/no/nordicsemi/android/service/CloseableCoroutineScope.kt @@ -1,7 +1,7 @@ package no.nordicsemi.android.service import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.cancel +import kotlinx.coroutines.cancelChildren import java.io.Closeable import kotlin.coroutines.CoroutineContext @@ -9,6 +9,6 @@ class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineS override val coroutineContext: CoroutineContext = context override fun close() { - coroutineContext.cancel() + coroutineContext.cancelChildren() } } 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 cb3f90ac..ea556c75 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 @@ -1,19 +1,34 @@ package no.nordicsemi.android.service import android.bluetooth.BluetoothDevice +import android.util.Log import no.nordicsemi.android.ble.observer.ConnectionObserver abstract class ConnectionObserverAdapter : ConnectionObserver { - override fun onDeviceConnecting(device: BluetoothDevice) { } + private val TAG = "BLE-CONNECTION" - override fun onDeviceConnected(device: BluetoothDevice) { } + override fun onDeviceConnecting(device: BluetoothDevice) { + Log.d(TAG, "onDeviceConnecting()") + } - override fun onDeviceFailedToConnect(device: BluetoothDevice, reason: Int) { } + override fun onDeviceConnected(device: BluetoothDevice) { + Log.d(TAG, "onDeviceConnected()") + } - override fun onDeviceReady(device: BluetoothDevice) { } + override fun onDeviceFailedToConnect(device: BluetoothDevice, reason: Int) { + Log.d(TAG, "onDeviceFailedToConnect()") + } - override fun onDeviceDisconnecting(device: BluetoothDevice) { } + override fun onDeviceReady(device: BluetoothDevice) { + Log.d(TAG, "onDeviceReady()") + } - override fun onDeviceDisconnected(device: BluetoothDevice, reason: Int) { } + override fun onDeviceDisconnecting(device: BluetoothDevice) { + Log.d(TAG, "onDeviceDisconnecting()") + } + + override fun onDeviceDisconnected(device: BluetoothDevice, reason: Int) { + Log.d(TAG, "onDeviceDisconnected()") + } } diff --git a/lib_service/src/test/java/no/nordicsemi/android/service/ExampleUnitTest.kt b/lib_service/src/test/java/no/nordicsemi/android/service/ExampleUnitTest.kt index dfd5daa2..9f52602e 100644 --- a/lib_service/src/test/java/no/nordicsemi/android/service/ExampleUnitTest.kt +++ b/lib_service/src/test/java/no/nordicsemi/android/service/ExampleUnitTest.kt @@ -1,9 +1,8 @@ package no.nordicsemi.android.service +import org.junit.Assert.assertEquals import org.junit.Test -import org.junit.Assert.* - /** * Example local unit test, which will execute on the development machine (host). * diff --git a/lib_theme/src/main/java/no/nordicsemi/android/theme/view/SectionTitle.kt b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/SectionTitle.kt index 1afa8f98..29fdc4ff 100644 --- a/lib_theme/src/main/java/no/nordicsemi/android/theme/view/SectionTitle.kt +++ b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/SectionTitle.kt @@ -3,7 +3,12 @@ package no.nordicsemi.android.theme.view import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme diff --git a/lib_theme/src/main/java/no/nordicsemi/android/theme/view/dialog/StringListDialog.kt b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/dialog/StringListDialog.kt index 3ce0d0a9..3f7c08bb 100644 --- a/lib_theme/src/main/java/no/nordicsemi/android/theme/view/dialog/StringListDialog.kt +++ b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/dialog/StringListDialog.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -19,6 +18,7 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -62,18 +62,18 @@ fun StringListView(config: StringListDialogConfig) { ) { config.items.forEachIndexed { i, entry -> - Column(modifier = Modifier.clickable { config.onResult(ItemSelectedResult(i)) }) { - Spacer(modifier = Modifier.height(16.dp)) - + Column( + modifier = Modifier + .clip(RoundedCornerShape(10.dp)) + .clickable { config.onResult(ItemSelectedResult(i)) } + .padding(8.dp), + ) { Row { config.leftIcon?.let { Image( modifier = Modifier.padding(horizontal = 4.dp), painter = painterResource(it), contentDescription = "Content image", -// colorFilter = ColorFilter.tint( -// NordicColors.NordicDarkGray.value() -// ) ) } Text( @@ -83,10 +83,6 @@ fun StringListView(config: StringListDialogConfig) { .fillMaxWidth() ) } - - if (i != config.items.size - 1) { - Spacer(modifier = Modifier.height(16.dp)) - } } } diff --git a/lib_utils/src/androidTest/java/no/nordicsemi/android/utils/ExampleInstrumentedTest.kt b/lib_utils/src/androidTest/java/no/nordicsemi/android/utils/ExampleInstrumentedTest.kt index 53bad000..f9a859af 100644 --- a/lib_utils/src/androidTest/java/no/nordicsemi/android/utils/ExampleInstrumentedTest.kt +++ b/lib_utils/src/androidTest/java/no/nordicsemi/android/utils/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package no.nordicsemi.android.utils -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * diff --git a/lib_utils/src/test/java/no/nordicsemi/android/utils/ExampleUnitTest.kt b/lib_utils/src/test/java/no/nordicsemi/android/utils/ExampleUnitTest.kt index b4c1d93e..3468a2d2 100644 --- a/lib_utils/src/test/java/no/nordicsemi/android/utils/ExampleUnitTest.kt +++ b/lib_utils/src/test/java/no/nordicsemi/android/utils/ExampleUnitTest.kt @@ -1,8 +1,7 @@ package no.nordicsemi.android.utils -import org.junit.Test - import org.junit.Assert.* +import org.junit.Test /** * Example local unit test, which will execute on the development machine (host). diff --git a/profile_bps/src/androidTest/java/no/nordicsemi/android/bps/ExampleInstrumentedTest.kt b/profile_bps/src/androidTest/java/no/nordicsemi/android/bps/ExampleInstrumentedTest.kt index 5dd697c4..1d17d36c 100644 --- a/profile_bps/src/androidTest/java/no/nordicsemi/android/bps/ExampleInstrumentedTest.kt +++ b/profile_bps/src/androidTest/java/no/nordicsemi/android/bps/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package no.nordicsemi.android.bps -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * diff --git a/profile_bps/src/main/AndroidManifest.xml b/profile_bps/src/main/AndroidManifest.xml index 457baeb5..984c6c4e 100644 --- a/profile_bps/src/main/AndroidManifest.xml +++ b/profile_bps/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + \ No newline at end of file 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 0441cf91..fe82569b 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 @@ -63,6 +63,7 @@ internal class BPSRepository @Inject constructor() { } fun clear() { + _status.value = BleManagerStatus.CONNECTING _data.tryEmit(BPSData()) } diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/repository/BPSManager.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/repository/BPSManager.kt index bb63b469..5c3922a4 100644 --- a/profile_bps/src/main/java/no/nordicsemi/android/bps/repository/BPSManager.kt +++ b/profile_bps/src/main/java/no/nordicsemi/android/bps/repository/BPSManager.kt @@ -32,7 +32,6 @@ import no.nordicsemi.android.ble.common.callback.bps.IntermediateCuffPressureDat import no.nordicsemi.android.ble.common.profile.bp.BloodPressureTypes import no.nordicsemi.android.ble.data.Data import no.nordicsemi.android.bps.data.BPSRepository -import no.nordicsemi.android.log.LogContract import no.nordicsemi.android.service.BatteryManager import java.util.* import javax.inject.Inject diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/UARTState.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSState.kt similarity index 100% rename from profile_bps/src/main/java/no/nordicsemi/android/bps/view/UARTState.kt rename to profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSState.kt diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/viewmodel/BPSViewModel.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/viewmodel/BPSViewModel.kt index f933b272..ead6a6c7 100644 --- a/profile_bps/src/main/java/no/nordicsemi/android/bps/viewmodel/BPSViewModel.kt +++ b/profile_bps/src/main/java/no/nordicsemi/android/bps/viewmodel/BPSViewModel.kt @@ -42,6 +42,11 @@ internal class BPSViewModel @Inject constructor( repository.setNewStatus(BleManagerStatus.OK) } + override fun onDeviceFailedToConnect(device: BluetoothDevice, reason: Int) { + super.onDeviceFailedToConnect(device, reason) + repository.setNewStatus(BleManagerStatus.DISCONNECTED) + } + override fun onDeviceDisconnected(device: BluetoothDevice, reason: Int) { super.onDeviceDisconnected(device, reason) repository.setNewStatus(BleManagerStatus.DISCONNECTED) @@ -63,7 +68,13 @@ internal class BPSViewModel @Inject constructor( } private fun onDisconnectButtonClick() { - deviceHolder.forgetDevice() bpsManager.disconnect().enqueue() + deviceHolder.forgetDevice() + repository.clear() + } + + override fun onCleared() { + super.onCleared() + repository.clear() } } diff --git a/profile_bps/src/test/java/no/nordicsemi/android/bps/ExampleUnitTest.kt b/profile_bps/src/test/java/no/nordicsemi/android/bps/ExampleUnitTest.kt index 6621f261..0d32bb2e 100644 --- a/profile_bps/src/test/java/no/nordicsemi/android/bps/ExampleUnitTest.kt +++ b/profile_bps/src/test/java/no/nordicsemi/android/bps/ExampleUnitTest.kt @@ -1,9 +1,8 @@ package no.nordicsemi.android.bps +import org.junit.Assert.assertEquals import org.junit.Test -import org.junit.Assert.* - /** * Example local unit test, which will execute on the development machine (host). * diff --git a/profile_cgms/src/androidTest/java/no/nordicsemi/android/cgms/ExampleInstrumentedTest.kt b/profile_cgms/src/androidTest/java/no/nordicsemi/android/cgms/ExampleInstrumentedTest.kt index 5febd881..c66608ba 100644 --- a/profile_cgms/src/androidTest/java/no/nordicsemi/android/cgms/ExampleInstrumentedTest.kt +++ b/profile_cgms/src/androidTest/java/no/nordicsemi/android/cgms/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package no.nordicsemi.android.cgms -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * 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 19749ca9..3cf0cd2d 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,7 +1,11 @@ package no.nordicsemi.android.cgms.data import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow import no.nordicsemi.android.service.BleManagerStatus import javax.inject.Inject import javax.inject.Singleton @@ -39,6 +43,7 @@ internal class CGMRepository @Inject constructor() { } fun clear() { + _status.value = BleManagerStatus.CONNECTING _data.tryEmit(CGMData()) } } 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 f1f12c30..b8812ce5 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 @@ -1,6 +1,14 @@ package no.nordicsemi.android.cgms.view -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -17,8 +25,8 @@ import androidx.compose.ui.unit.dp 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.CGMServiceCommand +import no.nordicsemi.android.cgms.data.RequestStatus import no.nordicsemi.android.material.you.CircularProgressIndicator import no.nordicsemi.android.theme.view.BatteryLevelView import no.nordicsemi.android.theme.view.ScreenSection 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 3fb7871b..1f7b0ae4 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,7 +4,6 @@ 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.CGMServiceCommand import java.text.SimpleDateFormat import java.util.* 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 997fccab..156e3e89 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 @@ -39,7 +39,12 @@ internal class CGMScreenViewModel @Inject constructor( } private fun disconnect() { - repository.clear() repository.sendNewServiceCommand(CGMServiceCommand.DISCONNECT) + repository.clear() + } + + override fun onCleared() { + super.onCleared() + repository.clear() } } diff --git a/profile_cgms/src/test/java/no/nordicsemi/android/cgms/ExampleUnitTest.kt b/profile_cgms/src/test/java/no/nordicsemi/android/cgms/ExampleUnitTest.kt index 307c6590..78c91bd8 100644 --- a/profile_cgms/src/test/java/no/nordicsemi/android/cgms/ExampleUnitTest.kt +++ b/profile_cgms/src/test/java/no/nordicsemi/android/cgms/ExampleUnitTest.kt @@ -1,9 +1,8 @@ package no.nordicsemi.android.cgms +import org.junit.Assert.assertEquals import org.junit.Test -import org.junit.Assert.* - /** * Example local unit test, which will execute on the development machine (host). * diff --git a/profile_csc/src/androidTest/java/no/nordicsemi/android/csc/ExampleInstrumentedTest.kt b/profile_csc/src/androidTest/java/no/nordicsemi/android/csc/ExampleInstrumentedTest.kt index 14296ead..5795b0fb 100644 --- a/profile_csc/src/androidTest/java/no/nordicsemi/android/csc/ExampleInstrumentedTest.kt +++ b/profile_csc/src/androidTest/java/no/nordicsemi/android/csc/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package no.nordicsemi.android.csc -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCData.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCData.kt index b8bbf4af..800b0d82 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCData.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCData.kt @@ -1,6 +1,5 @@ package no.nordicsemi.android.csc.data -import no.nordicsemi.android.csc.view.CSCSettings import no.nordicsemi.android.csc.view.SpeedUnit import no.nordicsemi.android.material.you.RadioButtonItem import no.nordicsemi.android.material.you.RadioGroupViewEntity @@ -11,7 +10,6 @@ private const val DISPLAY_KM_H = "km/h" private const val DISPLAY_MPH = "mph" internal data class CSCData( - val showDialog: Boolean = false, val scanDevices: Boolean = false, val selectedSpeedUnit: SpeedUnit = SpeedUnit.M_S, val speed: Float = 0f, @@ -20,8 +18,7 @@ internal data class CSCData( val totalDistance: Float = 0f, val gearRatio: Float = 0f, val batteryLevel: Int = 0, - val wheelSize: Int = CSCSettings.DefaultWheelSize.VALUE, - val wheelSizeDisplay: String = CSCSettings.DefaultWheelSize.NAME + val wheelSize: WheelSize = WheelSize() ) { private val speedWithUnit = when (selectedSpeedUnit) { diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCRepository.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCRepository.kt index 4cbc4aac..e5c452ac 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCRepository.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCRepository.kt @@ -5,7 +5,9 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow import no.nordicsemi.android.csc.view.SpeedUnit +import no.nordicsemi.android.service.BleManagerStatus import javax.inject.Inject import javax.inject.Singleton @@ -13,37 +15,38 @@ import javax.inject.Singleton internal class CSCRepository @Inject constructor() { private val _data = MutableStateFlow(CSCData()) - val data: StateFlow = _data + val data: StateFlow = _data.asStateFlow() private val _command = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST) val command = _command.asSharedFlow() - fun setWheelSize(wheelSize: Int, wheelSizeDisplay: String) { - _data.tryEmit(_data.value.copy( - wheelSize = wheelSize, - wheelSizeDisplay = wheelSizeDisplay, - showDialog = false - )) - } + private val _status = MutableStateFlow(BleManagerStatus.CONNECTING) + val status = _status.asStateFlow() fun setSpeedUnit(selectedSpeedUnit: SpeedUnit) { _data.tryEmit(_data.value.copy(selectedSpeedUnit = selectedSpeedUnit)) } - fun setHideWheelSizeDialog() { - _data.tryEmit(_data.value.copy(showDialog = false)) + fun setNewDistance( + totalDistance: Float, + distance: Float, + speed: Float, + wheelSize: WheelSize + ) { + _data.tryEmit(_data.value.copy( + totalDistance = totalDistance, + distance = distance, + speed = speed, + wheelSize = wheelSize + )) } - fun setDisplayWheelSizeDialog() { - _data.tryEmit(_data.value.copy(showDialog = true)) - } - - fun setNewDistance(totalDistance: Float, distance: Float, speed: Float) { - _data.tryEmit(_data.value.copy(totalDistance = totalDistance, distance = distance, speed = speed)) - } - - fun setNewCrankCadence(crankCadence: Float, gearRatio: Float) { - _data.tryEmit(_data.value.copy(cadence = crankCadence, gearRatio = gearRatio)) + fun setNewCrankCadence( + crankCadence: Float, + gearRatio: Float, + wheelSize: WheelSize + ) { + _data.tryEmit(_data.value.copy(cadence = crankCadence, gearRatio = gearRatio, wheelSize = wheelSize)) } fun setBatteryLevel(batteryLevel: Int) { @@ -54,7 +57,12 @@ internal class CSCRepository @Inject constructor() { _command.tryEmit(workingMode) } + fun setNewStatus(status: BleManagerStatus) { + _status.value = status + } + fun clear() { + _status.value = BleManagerStatus.CONNECTING _data.tryEmit(CSCData()) } } diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCServiceCommand.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCServiceCommand.kt index de0364ca..f8dae6ed 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCServiceCommand.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCServiceCommand.kt @@ -2,6 +2,6 @@ package no.nordicsemi.android.csc.data internal sealed class CSCServiceCommand -internal data class SetWheelSizeCommand(val size: Int) : CSCServiceCommand() +internal data class SetWheelSizeCommand(val wheelSize: WheelSize) : CSCServiceCommand() internal object DisconnectCommand : CSCServiceCommand() diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/data/WheelSize.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/data/WheelSize.kt new file mode 100644 index 00000000..3af4a00e --- /dev/null +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/data/WheelSize.kt @@ -0,0 +1,8 @@ +package no.nordicsemi.android.csc.data + +import no.nordicsemi.android.csc.view.CSCSettings + +data class WheelSize( + val value: Int = CSCSettings.DefaultWheelSize.VALUE, + val name: String = CSCSettings.DefaultWheelSize.NAME +) diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCManager.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCManager.kt index b4919787..35e3569c 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCManager.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCManager.kt @@ -30,9 +30,7 @@ import androidx.annotation.FloatRange import no.nordicsemi.android.ble.common.callback.csc.CyclingSpeedAndCadenceMeasurementDataCallback import no.nordicsemi.android.ble.data.Data import no.nordicsemi.android.csc.data.CSCRepository -import no.nordicsemi.android.csc.repository.CSCMeasurementParser.parse -import no.nordicsemi.android.csc.view.CSCSettings -import no.nordicsemi.android.log.LogContract +import no.nordicsemi.android.csc.data.WheelSize import no.nordicsemi.android.service.BatteryManager import java.util.* @@ -42,20 +40,20 @@ val CYCLING_SPEED_AND_CADENCE_SERVICE_UUID: UUID = UUID.fromString("00001816-000 /** Cycling Speed and Cadence Measurement characteristic UUID. */ private val CSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A5B-0000-1000-8000-00805f9b34fb") -internal class CSCManager(context: Context, private val dataHolder: CSCRepository) : BatteryManager(context) { +internal class CSCManager(context: Context, private val repository: CSCRepository) : BatteryManager(context) { private var cscMeasurementCharacteristic: BluetoothGattCharacteristic? = null - private var wheelSize = CSCSettings.DefaultWheelSize.VALUE + private var wheelSize: WheelSize = WheelSize() override fun onBatteryLevelChanged(batteryLevel: Int) { - dataHolder.setBatteryLevel(batteryLevel) + repository.setBatteryLevel(batteryLevel) } override fun getGattCallback(): BatteryManagerGattCallback { return CSCManagerGattCallback() } - fun setWheelSize(value: Int) { + fun setWheelSize(value: WheelSize) { wheelSize = value } @@ -72,7 +70,7 @@ internal class CSCManager(context: Context, private val dataHolder: CSCRepositor .with(object : CyclingSpeedAndCadenceMeasurementDataCallback() { override fun getWheelCircumference(): Float { - return wheelSize.toFloat() + return wheelSize.value.toFloat() } override fun onDistanceChanged( @@ -81,7 +79,7 @@ internal class CSCManager(context: Context, private val dataHolder: CSCRepositor @FloatRange(from = 0.0) distance: Float, @FloatRange(from = 0.0) speed: Float ) { - dataHolder.setNewDistance(totalDistance, distance, speed) + repository.setNewDistance(totalDistance, distance, speed, wheelSize) } override fun onCrankDataChanged( @@ -89,7 +87,7 @@ internal class CSCManager(context: Context, private val dataHolder: CSCRepositor @FloatRange(from = 0.0) crankCadence: Float, gearRatio: Float ) { - dataHolder.setNewCrankCadence(crankCadence, gearRatio) + repository.setNewCrankCadence(crankCadence, gearRatio, wheelSize) } override fun onInvalidDataReceived( diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCService.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCService.kt index 83b85ae5..3349c54e 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCService.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCService.kt @@ -1,5 +1,6 @@ package no.nordicsemi.android.csc.repository +import android.util.Log import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -21,10 +22,14 @@ internal class CSCService : ForegroundBleService() { override fun onCreate() { super.onCreate() + status.onEach { + repository.setNewStatus(it) + }.launchIn(scope) + repository.command.onEach { when (it) { DisconnectCommand -> stopSelf() - is SetWheelSizeCommand -> manager.setWheelSize(it.size) + is SetWheelSizeCommand -> manager.setWheelSize(it.wheelSize) }.exhaustive }.launchIn(scope) } 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 08d98610..e9f87c58 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 @@ -11,21 +11,41 @@ import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import no.nordicsemi.android.csc.R import no.nordicsemi.android.csc.data.CSCData +import no.nordicsemi.android.csc.data.WheelSize import no.nordicsemi.android.material.you.RadioButtonGroup import no.nordicsemi.android.theme.view.ScreenSection import no.nordicsemi.android.theme.view.SectionTitle +import no.nordicsemi.android.theme.view.dialog.FlowCanceled +import no.nordicsemi.android.theme.view.dialog.ItemSelectedResult +import no.nordicsemi.android.utils.exhaustive @Composable internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) { - if (state.showDialog) { - SelectWheelSizeDialog { onEvent(it) } + val showDialog = rememberSaveable { mutableStateOf(false) } + + if (showDialog.value) { + val wheelEntries = stringArrayResource(R.array.wheel_entries) + val wheelValues = stringArrayResource(R.array.wheel_values) + + SelectWheelSizeDialog { + when (it) { + FlowCanceled -> showDialog.value = false + is ItemSelectedResult -> { + onEvent(OnWheelSizeSelected(WheelSize(wheelValues[it.index].toInt(), wheelEntries[it.index]))) + showDialog.value = false + } + }.exhaustive + } } Column(modifier = Modifier.verticalScroll(rememberScrollState())) { @@ -33,7 +53,7 @@ internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) { horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(16.dp) ) { - SettingsSection(state, onEvent) + SettingsSection(state, onEvent) { showDialog.value = true } Spacer(modifier = Modifier.height(16.dp)) @@ -51,7 +71,7 @@ internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) { } @Composable -private fun SettingsSection(state: CSCData, onEvent: (CSCViewEvent) -> Unit) { +private fun SettingsSection(state: CSCData, onEvent: (CSCViewEvent) -> Unit, onWheelButtonClick: () -> Unit) { ScreenSection { Column( horizontalAlignment = Alignment.CenterHorizontally @@ -60,7 +80,7 @@ private fun SettingsSection(state: CSCData, onEvent: (CSCViewEvent) -> Unit) { Spacer(modifier = Modifier.height(16.dp)) - WheelSizeView(state, onEvent) + WheelSizeView(state, onWheelButtonClick) Spacer(modifier = Modifier.height(16.dp)) diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCScreen.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCScreen.kt index bc65d257..1ce42f26 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCScreen.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCScreen.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.csc.R -import no.nordicsemi.android.csc.data.CSCData import no.nordicsemi.android.csc.repository.CSCService import no.nordicsemi.android.csc.viewmodel.CSCViewModel 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 CSCScreen(finishAction: () -> Unit) { val viewModel: CSCViewModel = 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(CSCService::class.java.name)) { - val intent = Intent(context, CSCService::class.java) - context.stopService(intent) - } - } - - LaunchedEffect("start-service") { - if (!context.isServiceRunning(CSCService::class.java.name)) { + LaunchedEffect(state.isActive) { + if (state.isActive) { val intent = Intent(context, CSCService::class.java) context.startService(intent) + } else { + finishAction() } } - CSCView(state) { viewModel.onEvent(it) } + CSCView(state.viewState) { viewModel.onEvent(it) } } @Composable -private fun CSCView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) { +private fun CSCView(state: CSCViewState, onEvent: (CSCViewEvent) -> Unit) { Column { BackIconAppBar(stringResource(id = R.string.csc_title)) { onEvent(OnDisconnectButtonClick) } - CSCContentView(state) { onEvent(it) } + when (state) { + is DisplayDataState -> CSCContentView(state.data, onEvent) + LoadingState -> DeviceConnectingView() + }.exhaustive } } diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCState.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCState.kt new file mode 100644 index 00000000..792db91d --- /dev/null +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCState.kt @@ -0,0 +1,14 @@ +package no.nordicsemi.android.csc.view + +import no.nordicsemi.android.csc.data.CSCData + +internal data class CSCState( + val viewState: CSCViewState, + val isActive: Boolean = true +) + +internal sealed class CSCViewState + +internal object LoadingState : CSCViewState() + +internal data class DisplayDataState(val data: CSCData) : CSCViewState() diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCViewEvent.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCViewEvent.kt index d1c60edb..77edd51d 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCViewEvent.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCViewEvent.kt @@ -1,12 +1,10 @@ package no.nordicsemi.android.csc.view +import no.nordicsemi.android.csc.data.WheelSize + internal sealed class CSCViewEvent -internal object OnShowEditWheelSizeDialogButtonClick : CSCViewEvent() - -internal data class OnWheelSizeSelected(val wheelSize: Int, val wheelSizeDisplayInfo: String) : CSCViewEvent() - -internal object OnCloseSelectWheelSizeDialog : CSCViewEvent() +internal data class OnWheelSizeSelected(val wheelSize: WheelSize) : CSCViewEvent() internal data class OnSelectedSpeedUnitSelected(val selectedSpeedUnit: SpeedUnit) : CSCViewEvent() diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/SelectWheelSizeDialog.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/SelectWheelSizeDialog.kt index 6705c8c7..ee357925 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/SelectWheelSizeDialog.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/SelectWheelSizeDialog.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import no.nordicsemi.android.csc.R +import no.nordicsemi.android.csc.data.WheelSize import no.nordicsemi.android.material.you.NordicTheme import no.nordicsemi.android.theme.view.dialog.FlowCanceled import no.nordicsemi.android.theme.view.dialog.ItemSelectedResult @@ -15,16 +16,12 @@ import no.nordicsemi.android.theme.view.dialog.toAnnotatedString import no.nordicsemi.android.utils.exhaustive @Composable -internal fun SelectWheelSizeDialog(onEvent: (CSCViewEvent) -> Unit) { +internal fun SelectWheelSizeDialog(onEvent: (StringListDialogResult) -> Unit) { val wheelEntries = stringArrayResource(R.array.wheel_entries) val wheelValues = stringArrayResource(R.array.wheel_values) StringListDialog(createConfig(wheelEntries) { - when (it) { - FlowCanceled -> onEvent(OnCloseSelectWheelSizeDialog) - is ItemSelectedResult -> - onEvent(OnWheelSizeSelected(wheelValues[it.index].toInt(), wheelEntries[it.index])) - }.exhaustive + onEvent(it) }) } diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/WheelSizeView.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/WheelSizeView.kt index 841ba33d..f4741416 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/WheelSizeView.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/WheelSizeView.kt @@ -19,8 +19,8 @@ import no.nordicsemi.android.csc.R import no.nordicsemi.android.csc.data.CSCData @Composable -internal fun WheelSizeView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) { - OutlinedButton(onClick = { onEvent(OnShowEditWheelSizeDialogButtonClick) }) { +internal fun WheelSizeView(state: CSCData, onClick: () -> Unit) { + OutlinedButton(onClick = { onClick() }) { Row( modifier = Modifier.fillMaxWidth(0.5f), verticalAlignment = Alignment.CenterVertically, @@ -31,7 +31,7 @@ internal fun WheelSizeView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) { text = stringResource(id = R.string.csc_field_wheel_size), style = MaterialTheme.typography.labelSmall ) - Text(text = state.wheelSizeDisplay, style = MaterialTheme.typography.bodyMedium) + Text(text = state.wheelSize.name, style = MaterialTheme.typography.bodyMedium) } Icon(Icons.Default.ArrowDropDown, contentDescription = "") diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/viewmodel/CSCViewModel.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/viewmodel/CSCViewModel.kt index 5be6d88f..6ba45cbd 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/viewmodel/CSCViewModel.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/viewmodel/CSCViewModel.kt @@ -1,52 +1,61 @@ package no.nordicsemi.android.csc.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.csc.data.CSCRepository +import no.nordicsemi.android.csc.data.DisconnectCommand +import no.nordicsemi.android.csc.data.SetWheelSizeCommand +import no.nordicsemi.android.csc.view.CSCState import no.nordicsemi.android.csc.view.CSCViewEvent -import no.nordicsemi.android.csc.view.OnCloseSelectWheelSizeDialog +import no.nordicsemi.android.csc.view.DisplayDataState +import no.nordicsemi.android.csc.view.LoadingState import no.nordicsemi.android.csc.view.OnDisconnectButtonClick import no.nordicsemi.android.csc.view.OnSelectedSpeedUnitSelected -import no.nordicsemi.android.csc.view.OnShowEditWheelSizeDialogButtonClick import no.nordicsemi.android.csc.view.OnWheelSizeSelected +import no.nordicsemi.android.service.BleManagerStatus import no.nordicsemi.android.utils.exhaustive import javax.inject.Inject @HiltViewModel internal class CSCViewModel @Inject constructor( - private val dataHolder: CSCRepository + private val repository: CSCRepository ) : ViewModel() { - val state = dataHolder.data + val state = repository.data.combine(repository.status) { data, status -> + when (status) { + BleManagerStatus.CONNECTING -> CSCState(LoadingState) + BleManagerStatus.OK -> CSCState(DisplayDataState(data)) + BleManagerStatus.DISCONNECTED -> CSCState(DisplayDataState(data), false) + } + }.stateIn(viewModelScope, SharingStarted.Lazily, CSCState(LoadingState)) fun onEvent(event: CSCViewEvent) { when (event) { is OnSelectedSpeedUnitSelected -> onSelectedSpeedUnit(event) - OnShowEditWheelSizeDialogButtonClick -> onShowDialogEvent() is OnWheelSizeSelected -> onWheelSizeChanged(event) OnDisconnectButtonClick -> onDisconnectButtonClick() - OnCloseSelectWheelSizeDialog -> onHideDialogEvent() }.exhaustive } private fun onSelectedSpeedUnit(event: OnSelectedSpeedUnitSelected) { - dataHolder.setSpeedUnit(event.selectedSpeedUnit) - } - - private fun onShowDialogEvent() { - dataHolder.setDisplayWheelSizeDialog() + repository.setSpeedUnit(event.selectedSpeedUnit) } private fun onWheelSizeChanged(event: OnWheelSizeSelected) { - dataHolder.setWheelSize(event.wheelSize, event.wheelSizeDisplayInfo) + repository.sendNewServiceCommand(SetWheelSizeCommand(event.wheelSize)) } private fun onDisconnectButtonClick() { - finish() - dataHolder.clear() + repository.sendNewServiceCommand(DisconnectCommand) + repository.clear() } - private fun onHideDialogEvent() { - dataHolder.setHideWheelSizeDialog() + override fun onCleared() { + super.onCleared() + repository.clear() } } diff --git a/profile_dfu/src/androidTest/java/no/nordicsemi/dfu/ExampleInstrumentedTest.kt b/profile_dfu/src/androidTest/java/no/nordicsemi/dfu/ExampleInstrumentedTest.kt index 9e50f754..1910519d 100644 --- a/profile_dfu/src/androidTest/java/no/nordicsemi/dfu/ExampleInstrumentedTest.kt +++ b/profile_dfu/src/androidTest/java/no/nordicsemi/dfu/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package no.nordicsemi.dfu -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFURepository.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFURepository.kt index 952a27cc..493a4159 100644 --- a/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFURepository.kt +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFURepository.kt @@ -1,9 +1,13 @@ package no.nordicsemi.dfu.data import android.net.Uri +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import no.nordicsemi.android.service.BleManagerStatus import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder import javax.inject.Inject import javax.inject.Singleton @@ -17,6 +21,12 @@ internal class DFURepository @Inject constructor( private val _data = MutableStateFlow(NoFileSelectedState()) val data: StateFlow = _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 setZipFile(file: Uri) { val currentState = _data.value as NoFileSelectedState _data.value = fileManger.createFile(file)?.let { @@ -36,7 +46,16 @@ internal class DFURepository @Inject constructor( _data.value = FileInstallingState() } + fun sendNewCommand(command: DisconnectCommand) { + _command.tryEmit(command) + } + + fun setNewStatus(status: BleManagerStatus) { + _status.value = status + } + fun clear() { + _status.value = BleManagerStatus.CONNECTING _data.value = NoFileSelectedState() } } diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUServiceCommand.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUServiceCommand.kt new file mode 100644 index 00000000..fb0399b9 --- /dev/null +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/data/DFUServiceCommand.kt @@ -0,0 +1,3 @@ +package no.nordicsemi.dfu.data + +internal object DisconnectCommand diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/repository/DFUService.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/repository/DFUService.kt index d450d6b9..865b005f 100644 --- a/profile_dfu/src/main/java/no/nordicsemi/dfu/repository/DFUService.kt +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/repository/DFUService.kt @@ -28,10 +28,25 @@ import android.app.NotificationManager import android.content.Context import android.os.Build import androidx.annotation.RequiresApi +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import no.nordicsemi.android.dfu.DfuBaseService +import no.nordicsemi.android.service.BleManagerStatus +import no.nordicsemi.android.service.CloseableCoroutineScope import no.nordicsemi.dfu.R +import no.nordicsemi.dfu.data.DFURepository +import javax.inject.Inject -class DFUService : DfuBaseService() { +@AndroidEntryPoint +internal class DFUService : DfuBaseService() { + + private val scope = CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + + @Inject + lateinit var repository: DFURepository override fun onCreate() { super.onCreate() @@ -39,6 +54,12 @@ class DFUService : DfuBaseService() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { createDfuNotificationChannel(this) } + + repository.command.onEach { + stopSelf() + }.launchIn(scope) + + repository.setNewStatus(BleManagerStatus.OK) } override fun getNotificationTarget(): Class? { @@ -77,4 +98,10 @@ class DFUService : DfuBaseService() { context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager notificationManager?.createNotificationChannel(channel) } + + override fun onDestroy() { + repository.setNewStatus(BleManagerStatus.DISCONNECTED) + super.onDestroy() + scope.close() + } } diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUContentView.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUContentView.kt index 62fd8284..baa7e38a 100644 --- a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUContentView.kt +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUContentView.kt @@ -6,7 +6,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import no.nordicsemi.android.utils.exhaustive -import no.nordicsemi.dfu.data.* +import no.nordicsemi.dfu.data.DFUData +import no.nordicsemi.dfu.data.FileInstallingState +import no.nordicsemi.dfu.data.FileReadyState +import no.nordicsemi.dfu.data.NoFileSelectedState +import no.nordicsemi.dfu.data.UploadFailureState +import no.nordicsemi.dfu.data.UploadSuccessState @Composable internal fun DFUContentView(state: DFUData, onEvent: (DFUViewEvent) -> Unit) { diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUScreen.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUScreen.kt index f657d6c8..af3372a0 100644 --- a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUScreen.kt +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUScreen.kt @@ -9,9 +9,9 @@ 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.utils.isServiceRunning +import no.nordicsemi.android.theme.view.DeviceConnectingView +import no.nordicsemi.android.utils.exhaustive import no.nordicsemi.dfu.R -import no.nordicsemi.dfu.data.DFUData import no.nordicsemi.dfu.repository.DFUService import no.nordicsemi.dfu.viewmodel.DFUViewModel @@ -19,36 +19,30 @@ import no.nordicsemi.dfu.viewmodel.DFUViewModel fun DFUScreen(finishAction: () -> Unit) { val viewModel: DFUViewModel = 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(DFUService::class.java.name)) { - val intent = Intent(context, DFUService::class.java) - context.stopService(intent) - } - } - - LaunchedEffect("start-service") { - if (!context.isServiceRunning(DFUService::class.java.name)) { + LaunchedEffect(state.isActive) { + if (state.isActive) { val intent = Intent(context, DFUService::class.java) context.startService(intent) + } else { + finishAction() } } - DFUView(state) { viewModel.onEvent(it) } + DFUView(state.viewState) { viewModel.onEvent(it) } } @Composable -private fun DFUView(state: DFUData, onEvent: (DFUViewEvent) -> Unit) { +private fun DFUView(state: DFUViewState, onEvent: (DFUViewEvent) -> Unit) { Column { BackIconAppBar(stringResource(id = R.string.dfu_title)) { onEvent(OnDisconnectButtonClick) } - DFUContentView(state) { onEvent(it) } + when (state) { + is DisplayDataState -> DFUContentView(state.data) { onEvent(it) } + LoadingState -> DeviceConnectingView() + }.exhaustive } } diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUState.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUState.kt new file mode 100644 index 00000000..23a11830 --- /dev/null +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUState.kt @@ -0,0 +1,14 @@ +package no.nordicsemi.dfu.view + +import no.nordicsemi.dfu.data.DFUData + +internal data class DFUState( + val viewState: DFUViewState, + val isActive: Boolean = true +) + +internal sealed class DFUViewState + +internal object LoadingState : DFUViewState() + +internal data class DisplayDataState(val data: DFUData) : DFUViewState() diff --git a/profile_dfu/src/main/java/no/nordicsemi/dfu/viewmodel/DFUViewModel.kt b/profile_dfu/src/main/java/no/nordicsemi/dfu/viewmodel/DFUViewModel.kt index f6413a4e..db97ce78 100644 --- a/profile_dfu/src/main/java/no/nordicsemi/dfu/viewmodel/DFUViewModel.kt +++ b/profile_dfu/src/main/java/no/nordicsemi/dfu/viewmodel/DFUViewModel.kt @@ -1,24 +1,28 @@ package no.nordicsemi.dfu.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.service.SelectedBluetoothDeviceHolder -import no.nordicsemi.android.theme.viewmodel.CloseableViewModel import no.nordicsemi.android.utils.exhaustive import no.nordicsemi.dfu.data.Completed import no.nordicsemi.dfu.data.DFUManager import no.nordicsemi.dfu.data.DFUProgressManager import no.nordicsemi.dfu.data.DFURepository import no.nordicsemi.dfu.data.DFUServiceStatus +import no.nordicsemi.dfu.data.DisconnectCommand import no.nordicsemi.dfu.data.Error import no.nordicsemi.dfu.data.FileInstallingState import no.nordicsemi.dfu.data.FileReadyState -import no.nordicsemi.dfu.data.NoFileSelectedState import no.nordicsemi.dfu.data.ZipFile +import no.nordicsemi.dfu.view.DFUState import no.nordicsemi.dfu.view.DFUViewEvent +import no.nordicsemi.dfu.view.DisplayDataState +import no.nordicsemi.dfu.view.LoadingState import no.nordicsemi.dfu.view.OnDisconnectButtonClick import no.nordicsemi.dfu.view.OnInstallButtonClick import no.nordicsemi.dfu.view.OnPauseButtonClick @@ -32,13 +36,19 @@ internal class DFUViewModel @Inject constructor( private val progressManager: DFUProgressManager, private val deviceHolder: SelectedBluetoothDeviceHolder, private val dfuManager: DFUManager -) : CloseableViewModel() { +) : ViewModel() { val state = repository.data.combine(progressManager.status) { state, status -> (state as? FileInstallingState) ?.run { createInstallingStateWithNewStatus(state, status) } ?: state - }.stateIn(viewModelScope, SharingStarted.Eagerly, NoFileSelectedState()) + }.combine(repository.status) { data, status -> + when (status) { + BleManagerStatus.CONNECTING -> DFUState(LoadingState) + BleManagerStatus.OK -> DFUState(DisplayDataState(data)) + BleManagerStatus.DISCONNECTED -> DFUState(DisplayDataState(data), false) + } + }.stateIn(viewModelScope, SharingStarted.Lazily, DFUState(LoadingState)) init { progressManager.registerListener() @@ -58,9 +68,9 @@ internal class DFUViewModel @Inject constructor( } private fun closeScreen() { + repository.sendNewCommand(DisconnectCommand) repository.clear() deviceHolder.forgetDevice() - finish() } private fun requireFile(): ZipFile { @@ -82,6 +92,7 @@ internal class DFUViewModel @Inject constructor( override fun onCleared() { super.onCleared() + repository.clear() progressManager.unregisterListener() } } diff --git a/profile_dfu/src/test/java/no/nordicsemi/dfu/ExampleUnitTest.kt b/profile_dfu/src/test/java/no/nordicsemi/dfu/ExampleUnitTest.kt index d48fc46b..b24a6af4 100644 --- a/profile_dfu/src/test/java/no/nordicsemi/dfu/ExampleUnitTest.kt +++ b/profile_dfu/src/test/java/no/nordicsemi/dfu/ExampleUnitTest.kt @@ -1,9 +1,8 @@ package no.nordicsemi.dfu +import org.junit.Assert.assertEquals import org.junit.Test -import org.junit.Assert.* - /** * Example local unit test, which will execute on the development machine (host). * diff --git a/profile_gls/src/androidTest/java/no/nordicsemi/android/gls/ExampleInstrumentedTest.kt b/profile_gls/src/androidTest/java/no/nordicsemi/android/gls/ExampleInstrumentedTest.kt index 8a2887a6..0514c1e8 100644 --- a/profile_gls/src/androidTest/java/no/nordicsemi/android/gls/ExampleInstrumentedTest.kt +++ b/profile_gls/src/androidTest/java/no/nordicsemi/android/gls/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package no.nordicsemi.android.gls -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSRepository.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSRepository.kt index 2b7adb4b..ed0651b1 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSRepository.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSRepository.kt @@ -3,6 +3,7 @@ package no.nordicsemi.android.gls.data import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import no.nordicsemi.android.service.BleManagerStatus import javax.inject.Inject import javax.inject.Singleton @@ -12,6 +13,9 @@ internal class GLSRepository @Inject constructor() { private val _data = MutableStateFlow(GLSData()) val data: StateFlow = _data.asStateFlow() + private val _status = MutableStateFlow(BleManagerStatus.CONNECTING) + val status = _status.asStateFlow() + fun addNewRecord(record: GLSRecord) { val newRecords = _data.value.records.toMutableList().apply { add(record) @@ -40,7 +44,12 @@ internal class GLSRepository @Inject constructor() { _data.tryEmit(_data.value.copy(batteryLevel = batteryLevel)) } + fun setNewStatus(status: BleManagerStatus) { + _status.value = status + } + fun clear() { + _status.value = BleManagerStatus.CONNECTING _data.tryEmit(GLSData()) } } diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSManager.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSManager.kt index d2452797..b9088970 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSManager.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSManager.kt @@ -35,17 +35,23 @@ import no.nordicsemi.android.ble.common.data.RecordAccessControlPointData import no.nordicsemi.android.ble.common.profile.RecordAccessControlPointCallback.RACPErrorCode import no.nordicsemi.android.ble.common.profile.RecordAccessControlPointCallback.RACPOpCode import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementCallback.GlucoseStatus -import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.* -import no.nordicsemi.android.ble.data.Data -import no.nordicsemi.android.gls.data.* +import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Carbohydrate +import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Health +import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Meal +import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Medication +import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Tester import no.nordicsemi.android.gls.data.CarbohydrateId import no.nordicsemi.android.gls.data.ConcentrationUnit +import no.nordicsemi.android.gls.data.GLSRecord +import no.nordicsemi.android.gls.data.GLSRepository import no.nordicsemi.android.gls.data.HealthStatus +import no.nordicsemi.android.gls.data.MeasurementContext import no.nordicsemi.android.gls.data.MedicationId import no.nordicsemi.android.gls.data.MedicationUnit +import no.nordicsemi.android.gls.data.RecordType +import no.nordicsemi.android.gls.data.RequestStatus import no.nordicsemi.android.gls.data.TestType import no.nordicsemi.android.gls.data.TypeOfMeal -import no.nordicsemi.android.log.LogContract import no.nordicsemi.android.service.BatteryManager import java.util.* import javax.inject.Inject @@ -304,14 +310,7 @@ internal class GLSManager @Inject constructor( writeCharacteristic( recordAccessControlPointCharacteristic, RecordAccessControlPointData.reportLastStoredRecord() - ) - .with { device: BluetoothDevice, data: Data -> - log( - LogContract.Log.Level.APPLICATION, - "\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent" - ) - } - .enqueue() + ).enqueue() } /** @@ -327,14 +326,7 @@ internal class GLSManager @Inject constructor( writeCharacteristic( recordAccessControlPointCharacteristic, RecordAccessControlPointData.reportFirstStoredRecord() - ) - .with { device: BluetoothDevice, data: Data -> - log( - LogContract.Log.Level.APPLICATION, - "\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent" - ) - } - .enqueue() + ).enqueue() } /** @@ -351,14 +343,7 @@ internal class GLSManager @Inject constructor( writeCharacteristic( recordAccessControlPointCharacteristic, RecordAccessControlPointData.reportNumberOfAllStoredRecords() - ) - .with { device: BluetoothDevice, data: Data -> - log( - LogContract.Log.Level.APPLICATION, - "\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent" - ) - } - .enqueue() + ).enqueue() } /** @@ -385,14 +370,7 @@ internal class GLSManager @Inject constructor( writeCharacteristic( recordAccessControlPointCharacteristic, RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber) - ) - .with { device: BluetoothDevice, data: Data -> - log( - LogContract.Log.Level.APPLICATION, - "\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent" - ) - } - .enqueue() + ).enqueue() // Info: // Operators OPERATOR_LESS_THEN_OR_EQUAL and OPERATOR_RANGE are not supported by Nordic Semiconductor Glucose Service in SDK 4.4.2. } @@ -407,14 +385,7 @@ internal class GLSManager @Inject constructor( writeCharacteristic( recordAccessControlPointCharacteristic, RecordAccessControlPointData.abortOperation() - ) - .with { device: BluetoothDevice, data: Data -> - log( - LogContract.Log.Level.APPLICATION, - "\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent" - ) - } - .enqueue() + ).enqueue() } /** @@ -429,14 +400,7 @@ internal class GLSManager @Inject constructor( writeCharacteristic( recordAccessControlPointCharacteristic, RecordAccessControlPointData.deleteAllStoredRecords() - ) - .with { device: BluetoothDevice, data: Data -> - log( - LogContract.Log.Level.APPLICATION, - "\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent" - ) - } - .enqueue() + ).enqueue() val elements = listOf(1, 2, 3) val result = elements.all { it > 3 } diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSRecordAccessControlPointParser.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSRecordAccessControlPointParser.kt deleted file mode 100644 index ca6f393f..00000000 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSRecordAccessControlPointParser.kt +++ /dev/null @@ -1,137 +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.gls.repository - -import no.nordicsemi.android.ble.data.Data - -object GLSRecordAccessControlPointParser { - - private const val OP_CODE_REPORT_STORED_RECORDS = 1 - private const val OP_CODE_DELETE_STORED_RECORDS = 2 - private const val OP_CODE_ABORT_OPERATION = 3 - private const val OP_CODE_REPORT_NUMBER_OF_RECORDS = 4 - private const val OP_CODE_NUMBER_OF_STORED_RECORDS_RESPONSE = 5 - private const val OP_CODE_RESPONSE_CODE = 6 - private const val OPERATOR_NULL = 0 - private const val OPERATOR_ALL_RECORDS = 1 - private const val OPERATOR_LESS_THEN_OR_EQUAL = 2 - private const val OPERATOR_GREATER_THEN_OR_EQUAL = 3 - private const val OPERATOR_WITHING_RANGE = 4 - private const val OPERATOR_FIRST_RECORD = 5 - private const val OPERATOR_LAST_RECORD = 6 - private const val RESPONSE_SUCCESS = 1 - private const val RESPONSE_OP_CODE_NOT_SUPPORTED = 2 - private const val RESPONSE_INVALID_OPERATOR = 3 - private const val RESPONSE_OPERATOR_NOT_SUPPORTED = 4 - private const val RESPONSE_INVALID_OPERAND = 5 - private const val RESPONSE_NO_RECORDS_FOUND = 6 - private const val RESPONSE_ABORT_UNSUCCESSFUL = 7 - private const val RESPONSE_PROCEDURE_NOT_COMPLETED = 8 - private const val RESPONSE_OPERAND_NOT_SUPPORTED = 9 - - fun parse(data: Data): String { - val builder = StringBuilder() - val opCode = data.getIntValue(Data.FORMAT_UINT8, 0)!! - val operator = data.getIntValue(Data.FORMAT_UINT8, 1)!! - when (opCode) { - OP_CODE_REPORT_STORED_RECORDS, OP_CODE_DELETE_STORED_RECORDS, OP_CODE_ABORT_OPERATION, OP_CODE_REPORT_NUMBER_OF_RECORDS -> builder.append( - getOpCode(opCode) - ).append("\n") - OP_CODE_NUMBER_OF_STORED_RECORDS_RESPONSE -> { - builder.append(getOpCode(opCode)).append(": ") - val value = data.getIntValue(Data.FORMAT_UINT16, 2)!! - builder.append(value).append("\n") - } - OP_CODE_RESPONSE_CODE -> { - builder.append(getOpCode(opCode)).append(" for ") - val targetOpCode = data.getIntValue(Data.FORMAT_UINT8, 2)!! - builder.append(getOpCode(targetOpCode)).append(": ") - val status = data.getIntValue(Data.FORMAT_UINT8, 3)!! - builder.append(getStatus(status)).append("\n") - } - } - when (operator) { - OPERATOR_ALL_RECORDS, OPERATOR_FIRST_RECORD, OPERATOR_LAST_RECORD -> builder.append("Operator: ") - .append( - getOperator(operator) - ).append("\n") - OPERATOR_GREATER_THEN_OR_EQUAL, OPERATOR_LESS_THEN_OR_EQUAL -> { - val filter = data.getIntValue(Data.FORMAT_UINT8, 2)!! - val value = data.getIntValue(Data.FORMAT_UINT16, 3)!! - builder.append("Operator: ").append(getOperator(operator)).append(" ").append(value) - .append(" (filter: ").append(filter).append(")\n") - } - OPERATOR_WITHING_RANGE -> { - val filter = data.getIntValue(Data.FORMAT_UINT8, 2)!! - val value1 = data.getIntValue(Data.FORMAT_UINT16, 3)!! - val value2 = data.getIntValue(Data.FORMAT_UINT16, 5)!! - builder.append("Operator: ").append(getOperator(operator)).append(" ") - .append(value1).append("-").append(value2).append(" (filter: ").append(filter) - .append(")\n") - } - } - if (builder.isNotEmpty()) { - builder.setLength(builder.length - 1) - } - return builder.toString() - } - - private fun getOpCode(opCode: Int): String { - return when (opCode) { - OP_CODE_REPORT_STORED_RECORDS -> "Report stored records" - OP_CODE_DELETE_STORED_RECORDS -> "Delete stored records" - OP_CODE_ABORT_OPERATION -> "Abort operation" - OP_CODE_REPORT_NUMBER_OF_RECORDS -> "Report number of stored records" - OP_CODE_NUMBER_OF_STORED_RECORDS_RESPONSE -> "Number of stored records response" - OP_CODE_RESPONSE_CODE -> "Response Code" - else -> "Reserved for future use" - } - } - - private fun getOperator(operator: Int): String { - return when (operator) { - OPERATOR_NULL -> "Null" - OPERATOR_ALL_RECORDS -> "All records" - OPERATOR_LESS_THEN_OR_EQUAL -> "Less than or equal to" - OPERATOR_GREATER_THEN_OR_EQUAL -> "Greater than or equal to" - OPERATOR_WITHING_RANGE -> "Within range of" - OPERATOR_FIRST_RECORD -> "First record(i.e. oldest record)" - OPERATOR_LAST_RECORD -> "Last record (i.e. most recent record)" - else -> "Reserved for future use" - } - } - - private fun getStatus(status: Int): String { - return when (status) { - RESPONSE_SUCCESS -> "Success" - RESPONSE_OP_CODE_NOT_SUPPORTED -> "Operation not supported" - RESPONSE_INVALID_OPERATOR -> "Invalid operator" - RESPONSE_OPERATOR_NOT_SUPPORTED -> "Operator not supported" - RESPONSE_INVALID_OPERAND -> "Invalid operand" - RESPONSE_NO_RECORDS_FOUND -> "No records found" - RESPONSE_ABORT_UNSUCCESSFUL -> "Abort unsuccessful" - RESPONSE_PROCEDURE_NOT_COMPLETED -> "Procedure not completed" - RESPONSE_OPERAND_NOT_SUPPORTED -> "Operand not supported" - else -> "Reserved for future use" - } - } -} diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/view/BPSState.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/view/BPSState.kt new file mode 100644 index 00000000..ee29a992 --- /dev/null +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/view/BPSState.kt @@ -0,0 +1,14 @@ +package no.nordicsemi.android.gls.view + +import no.nordicsemi.android.gls.data.GLSData + +internal data class GLSState( + val viewState: GLSViewState, + val isActive: Boolean = true +) + +internal sealed class GLSViewState + +internal object LoadingState : GLSViewState() + +internal data class DisplayDataState(val data: GLSData) : GLSViewState() diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSContentView.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSContentView.kt index 4e71036a..73f9595e 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSContentView.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSContentView.kt @@ -1,6 +1,14 @@ package no.nordicsemi.android.gls.view -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSScreen.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSScreen.kt index d633aeb5..b67f63e6 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSScreen.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSScreen.kt @@ -1,5 +1,6 @@ package no.nordicsemi.android.gls.view +import android.util.Log import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -7,40 +8,43 @@ import androidx.compose.runtime.collectAsState import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import no.nordicsemi.android.gls.R -import no.nordicsemi.android.gls.data.GLSData import no.nordicsemi.android.gls.viewmodel.DisconnectEvent import no.nordicsemi.android.gls.viewmodel.GLSScreenViewEvent import no.nordicsemi.android.gls.viewmodel.GLSViewModel import no.nordicsemi.android.theme.view.BackIconAppBar +import no.nordicsemi.android.theme.view.DeviceConnectingView +import no.nordicsemi.android.utils.exhaustive @Composable fun GLSScreen(finishAction: () -> Unit) { val viewModel: GLSViewModel = hiltViewModel() val state = viewModel.state.collectAsState().value - val isScreenActive = viewModel.isActive.collectAsState().value - LaunchedEffect("connect") { - viewModel.connectDevice() - } + Log.d("AAATESTAAA", "$viewModel") //TODO fix screen rotation - LaunchedEffect(isScreenActive) { - if (!isScreenActive) { + LaunchedEffect(state.isActive) { + if (state.isActive) { + viewModel.connectDevice() + } else { finishAction() } } - GLSView(state) { + GLSView(state.viewState) { viewModel.onEvent(it) } } @Composable -private fun GLSView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) { +private fun GLSView(state: GLSViewState, onEvent: (GLSScreenViewEvent) -> Unit) { Column { BackIconAppBar(stringResource(id = R.string.gls_title)) { onEvent(DisconnectEvent) } - GLSContentView(state, onEvent) + when (state) { + is DisplayDataState -> GLSContentView(state.data, onEvent) + LoadingState -> DeviceConnectingView() + }.exhaustive } } diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/viewmodel/GLSViewModel.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/viewmodel/GLSViewModel.kt index 46ea088d..1fe8c212 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/viewmodel/GLSViewModel.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/viewmodel/GLSViewModel.kt @@ -1,11 +1,21 @@ package no.nordicsemi.android.gls.viewmodel +import android.bluetooth.BluetoothDevice +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.gls.data.GLSRepository import no.nordicsemi.android.gls.data.WorkingMode import no.nordicsemi.android.gls.repository.GLSManager +import no.nordicsemi.android.gls.view.DisplayDataState +import no.nordicsemi.android.gls.view.GLSState +import no.nordicsemi.android.gls.view.LoadingState +import no.nordicsemi.android.service.BleManagerStatus +import no.nordicsemi.android.service.ConnectionObserverAdapter import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder -import no.nordicsemi.android.theme.viewmodel.CloseableViewModel import no.nordicsemi.android.utils.exhaustive import javax.inject.Inject @@ -13,10 +23,35 @@ import javax.inject.Inject internal class GLSViewModel @Inject constructor( private val glsManager: GLSManager, private val deviceHolder: SelectedBluetoothDeviceHolder, - private val dataHolder: GLSRepository -) : CloseableViewModel() { + private val repository: GLSRepository +) : ViewModel() { - val state = dataHolder.data + val state = repository.data.combine(repository.status) { data, status -> + when (status) { + BleManagerStatus.CONNECTING -> GLSState(LoadingState) + BleManagerStatus.OK -> GLSState(DisplayDataState(data)) + BleManagerStatus.DISCONNECTED -> GLSState(DisplayDataState(data), false) + } + }.stateIn(viewModelScope, SharingStarted.Lazily, GLSState(LoadingState)) + + init { + glsManager.setConnectionObserver(object : ConnectionObserverAdapter() { + override fun onDeviceConnected(device: BluetoothDevice) { + super.onDeviceConnected(device) + repository.setNewStatus(BleManagerStatus.OK) + } + + override fun onDeviceFailedToConnect(device: BluetoothDevice, reason: Int) { + super.onDeviceFailedToConnect(device, reason) + repository.setNewStatus(BleManagerStatus.DISCONNECTED) + } + + override fun onDeviceDisconnected(device: BluetoothDevice, reason: Int) { + super.onDeviceDisconnected(device, reason) + repository.setNewStatus(BleManagerStatus.DISCONNECTED) + } + }) + } fun onEvent(event: GLSScreenViewEvent) { when (event) { @@ -43,8 +78,12 @@ internal class GLSViewModel @Inject constructor( } private fun disconnect() { - finish() deviceHolder.forgetDevice() - dataHolder.clear() + glsManager.disconnect().enqueue() + } + + override fun onCleared() { + super.onCleared() + repository.clear() } } diff --git a/profile_gls/src/test/java/no/nordicsemi/android/gls/ExampleUnitTest.kt b/profile_gls/src/test/java/no/nordicsemi/android/gls/ExampleUnitTest.kt index 57961909..a1f4e631 100644 --- a/profile_gls/src/test/java/no/nordicsemi/android/gls/ExampleUnitTest.kt +++ b/profile_gls/src/test/java/no/nordicsemi/android/gls/ExampleUnitTest.kt @@ -1,9 +1,8 @@ package no.nordicsemi.android.gls +import org.junit.Assert.assertEquals import org.junit.Test -import org.junit.Assert.* - /** * Example local unit test, which will execute on the development machine (host). * diff --git a/profile_hrs/src/androidTest/java/no/nordicsemi/android/hrs/ExampleInstrumentedTest.kt b/profile_hrs/src/androidTest/java/no/nordicsemi/android/hrs/ExampleInstrumentedTest.kt index 8e759ebc..c51579ce 100644 --- a/profile_hrs/src/androidTest/java/no/nordicsemi/android/hrs/ExampleInstrumentedTest.kt +++ b/profile_hrs/src/androidTest/java/no/nordicsemi/android/hrs/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package no.nordicsemi.android.hrs -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSRepository.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSRepository.kt index 7a58ae46..d0a9eee2 100644 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSRepository.kt +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSRepository.kt @@ -1,7 +1,12 @@ package no.nordicsemi.android.hrs.data +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import no.nordicsemi.android.service.BleManagerStatus import javax.inject.Inject import javax.inject.Singleton @@ -11,6 +16,12 @@ internal class HRSRepository @Inject constructor() { private val _data = MutableStateFlow(HRSData()) val data: StateFlow = _data + 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 addNewHeartRate(heartRate: Int) { val result = _data.value.heartRates.toMutableList().apply { add(heartRate) @@ -26,7 +37,16 @@ internal class HRSRepository @Inject constructor() { _data.tryEmit(_data.value.copy(batteryLevel = batteryLevel)) } + fun sendDisconnectCommand() { + _command.tryEmit(DisconnectCommand) + } + + fun setNewStatus(status: BleManagerStatus) { + _status.value = status + } + fun clear() { + _status.value = BleManagerStatus.CONNECTING _data.tryEmit(HRSData()) } } diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSServiceCommand.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSServiceCommand.kt new file mode 100644 index 00000000..938371a8 --- /dev/null +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSServiceCommand.kt @@ -0,0 +1,3 @@ +package no.nordicsemi.android.hrs.data + +internal object DisconnectCommand diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/BodySensorLocationParser.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/BodySensorLocationParser.kt deleted file mode 100644 index 5b65517c..00000000 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/BodySensorLocationParser.kt +++ /dev/null @@ -1,39 +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.hrs.service - -import no.nordicsemi.android.ble.data.Data - -internal object BodySensorLocationParser { - fun parse(data: Data): String { - return when (data.getIntValue(Data.FORMAT_UINT8, 0)!!) { - 6 -> "Foot" - 5 -> "Ear Lobe" - 4 -> "Hand" - 3 -> "Finger" - 2 -> "Wrist" - 1 -> "Chest" - 0 -> "Other" - else -> "Other" - } - } -} diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSManager.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSManager.kt index 031b841f..2b263eb6 100644 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSManager.kt +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSManager.kt @@ -30,9 +30,7 @@ import androidx.annotation.IntRange import no.nordicsemi.android.ble.common.callback.hr.BodySensorLocationDataCallback import no.nordicsemi.android.ble.common.callback.hr.HeartRateMeasurementDataCallback import no.nordicsemi.android.ble.common.profile.hr.BodySensorLocation -import no.nordicsemi.android.ble.data.Data import no.nordicsemi.android.hrs.data.HRSRepository -import no.nordicsemi.android.log.LogContract import no.nordicsemi.android.service.BatteryManager import java.util.* @@ -53,11 +51,6 @@ internal class HRSManager(context: Context, private val dataHolder: HRSRepositor private val bodySensorLocationDataCallback = object : BodySensorLocationDataCallback() { - override fun onDataReceived(device: BluetoothDevice, data: Data) { - log(LogContract.Log.Level.APPLICATION, "\"" + BodySensorLocationParser.parse(data) + "\" received") - super.onDataReceived(device, data) - } - override fun onBodySensorLocationReceived( device: BluetoothDevice, @BodySensorLocation sensorLocation: Int @@ -68,11 +61,6 @@ internal class HRSManager(context: Context, private val dataHolder: HRSRepositor private val heartRateMeasurementDataCallback = object : HeartRateMeasurementDataCallback() { - override fun onDataReceived(device: BluetoothDevice, data: Data) { - log(LogContract.Log.Level.APPLICATION, "\"" + HeartRateMeasurementParser.parse(data) + "\" received") - super.onDataReceived(device, data) - } - override fun onHeartRateMeasurementReceived( device: BluetoothDevice, @IntRange(from = 0) heartRate: Int, diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSService.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSService.kt index ab7b14b9..763ec085 100644 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSService.kt +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSService.kt @@ -1,6 +1,8 @@ package no.nordicsemi.android.hrs.service import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import no.nordicsemi.android.hrs.data.HRSRepository import no.nordicsemi.android.service.ForegroundBleService import javax.inject.Inject @@ -9,7 +11,19 @@ import javax.inject.Inject internal class HRSService : ForegroundBleService() { @Inject - lateinit var dataHolder: HRSRepository + lateinit var repository: HRSRepository - override val manager: HRSManager by lazy { HRSManager(this, dataHolder) } + override val manager: HRSManager by lazy { HRSManager(this, repository) } + + override fun onCreate() { + super.onCreate() + + status.onEach { + repository.setNewStatus(it) + }.launchIn(scope) + + repository.command.onEach { + stopSelf() + }.launchIn(scope) + } } diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HeartRateMeasurementParser.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HeartRateMeasurementParser.kt deleted file mode 100644 index 46e0729a..00000000 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HeartRateMeasurementParser.kt +++ /dev/null @@ -1,115 +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.hrs.service - -import no.nordicsemi.android.ble.data.Data -import java.util.* - -internal object HeartRateMeasurementParser { - - private const val HEART_RATE_VALUE_FORMAT: Byte = 0x01 // 1 bit - private const val SENSOR_CONTACT_STATUS: Byte = 0x06 // 2 bits - private const val ENERGY_EXPANDED_STATUS: Byte = 0x08 // 1 bit - private const val RR_INTERVAL: Byte = 0x10 // 1 bit - - fun parse(data: Data): String { - var offset = 0 - val flags = data.getIntValue(Data.FORMAT_UINT8, offset++)!! - - /* - * false Heart Rate Value Format is set to UINT8. Units: beats per minute (bpm) - * true Heart Rate Value Format is set to UINT16. Units: beats per minute (bpm) - */ - val value16bit = flags and HEART_RATE_VALUE_FORMAT.toInt() > 0 - - /* - * 0 Sensor Contact feature is not supported in the current connection - * 1 Sensor Contact feature is not supported in the current connection - * 2 Sensor Contact feature is supported, but contact is not detected - * 3 Sensor Contact feature is supported and contact is detected - */ - val sensorContactStatus = flags and SENSOR_CONTACT_STATUS.toInt() shr 1 - - /* - * false Energy Expended field is not present - * true Energy Expended field is present. Units: kilo Joules - */ - val energyExpandedStatus = flags and ENERGY_EXPANDED_STATUS.toInt() > 0 - - /* - * false RR-Interval values are not present. - * true One or more RR-Interval values are present. Units: 1/1024 seconds - */ - val rrIntervalStatus = flags and RR_INTERVAL.toInt() > 0 - - // heart rate value is 8 or 16 bit long - val heartRateValue = data.getIntValue( - if (value16bit) { - Data.FORMAT_UINT16 - } else { - Data.FORMAT_UINT8 - }, - offset++ - ) // bits per minute - if (value16bit) offset++ - - // energy expanded value is present if a flag was set - var energyExpanded = -1 - if (energyExpandedStatus) energyExpanded = data.getIntValue(Data.FORMAT_UINT16, offset)!! - offset += 2 - - // RR-interval is set when a flag is set - val rrIntervals: MutableList = ArrayList() - if (rrIntervalStatus) { - var o = offset - while (o < data.value!!.size) { - val units = data.getIntValue(Data.FORMAT_UINT16, o)!! - rrIntervals.add(units * 1000.0f / 1024.0f) // RR interval is in [1/1024s] - o += 2 - } - } - val builder = StringBuilder() - builder.append("Heart Rate Measurement: ").append(heartRateValue).append(" bpm") - when (sensorContactStatus) { - 0, 1 -> builder.append(",\nSensor Contact Not Supported") - 2 -> builder.append(",\nContact is NOT Detected") - 3 -> builder.append(",\nContact is Detected") - } - if (energyExpandedStatus) { - builder.append(",\nEnergy Expanded: ") - .append(energyExpanded) - .append(" kJ") - } - if (rrIntervalStatus) { - builder.append(",\nRR Interval: ") - for (interval in rrIntervals) builder.append( - String.format( - Locale.US, - "%.02f ms, ", - interval - ) - ) - builder.setLength(builder.length - 2) // remove the ", " at the end - } - return builder.toString() - } -} \ No newline at end of file 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 241f21f3..9c195bab 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 @@ -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.hrs.R -import no.nordicsemi.android.hrs.data.HRSData import no.nordicsemi.android.hrs.service.HRSService import no.nordicsemi.android.hrs.viewmodel.HRSViewModel 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 HRSScreen(finishAction: () -> Unit) { val viewModel: HRSViewModel = hiltViewModel() val state = viewModel.state.collectAsState().value - val isActive = viewModel.isActive.collectAsState().value val context = LocalContext.current - LaunchedEffect(isActive) { - if (!isActive) { - finishAction() - } - if (context.isServiceRunning(HRSService::class.java.name)) { - val intent = Intent(context, HRSService::class.java) - context.stopService(intent) - } - } - - LaunchedEffect("start-service") { - if (!context.isServiceRunning(HRSService::class.java.name)) { + LaunchedEffect(state.isActive) { + if (state.isActive) { val intent = Intent(context, HRSService::class.java) context.startService(intent) + } else { + finishAction() } } - HRSView(state) { viewModel.onEvent(it) } + HRSView(state.viewState) { viewModel.onEvent(it) } } @Composable -private fun HRSView(state: HRSData, onEvent: (HRSScreenViewEvent) -> Unit) { +private fun HRSView(state: HRSViewState, onEvent: (HRSScreenViewEvent) -> Unit) { Column { BackIconAppBar(stringResource(id = R.string.hrs_title)) { onEvent(DisconnectEvent) } - HRSContentView(state) { onEvent(it) } + when (state) { + is DisplayDataState -> HRSContentView(state.data) { onEvent(it) } + LoadingState -> DeviceConnectingView() + }.exhaustive } } diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSState.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSState.kt new file mode 100644 index 00000000..7846de62 --- /dev/null +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSState.kt @@ -0,0 +1,14 @@ +package no.nordicsemi.android.hrs.view + +import no.nordicsemi.android.hrs.data.HRSData + +internal data class HRSState( + val viewState: HRSViewState, + val isActive: Boolean = true +) + +internal sealed class HRSViewState + +internal object LoadingState : HRSViewState() + +internal data class DisplayDataState(val data: HRSData) : HRSViewState() diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/viewmodel/HRSViewModel.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/viewmodel/HRSViewModel.kt index 585a59d4..84c6aeee 100644 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/viewmodel/HRSViewModel.kt +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/viewmodel/HRSViewModel.kt @@ -1,18 +1,32 @@ package no.nordicsemi.android.hrs.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.hrs.data.HRSRepository import no.nordicsemi.android.hrs.view.DisconnectEvent +import no.nordicsemi.android.hrs.view.DisplayDataState import no.nordicsemi.android.hrs.view.HRSScreenViewEvent -import no.nordicsemi.android.theme.viewmodel.CloseableViewModel +import no.nordicsemi.android.hrs.view.HRSState +import no.nordicsemi.android.hrs.view.LoadingState +import no.nordicsemi.android.service.BleManagerStatus import javax.inject.Inject @HiltViewModel internal class HRSViewModel @Inject constructor( - private val dataHolder: HRSRepository -) : CloseableViewModel() { + private val repository: HRSRepository +) : ViewModel() { - val state = dataHolder.data + val state = repository.data.combine(repository.status) { data, status -> + when (status) { + BleManagerStatus.CONNECTING -> HRSState(LoadingState) + BleManagerStatus.OK -> HRSState(DisplayDataState(data)) + BleManagerStatus.DISCONNECTED -> HRSState(DisplayDataState(data), false) + } + }.stateIn(viewModelScope, SharingStarted.Lazily, HRSState(LoadingState)) fun onEvent(event: HRSScreenViewEvent) { (event as? DisconnectEvent)?.let { @@ -21,7 +35,12 @@ internal class HRSViewModel @Inject constructor( } private fun onDisconnectButtonClick() { - finish() - dataHolder.clear() + repository.sendDisconnectCommand() + repository.clear() + } + + override fun onCleared() { + super.onCleared() + repository.clear() } } diff --git a/profile_hrs/src/test/java/no/nordicsemi/android/hrs/ExampleUnitTest.kt b/profile_hrs/src/test/java/no/nordicsemi/android/hrs/ExampleUnitTest.kt index 51644e23..c4a005a9 100644 --- a/profile_hrs/src/test/java/no/nordicsemi/android/hrs/ExampleUnitTest.kt +++ b/profile_hrs/src/test/java/no/nordicsemi/android/hrs/ExampleUnitTest.kt @@ -1,9 +1,8 @@ package no.nordicsemi.android.hrs +import org.junit.Assert.assertEquals import org.junit.Test -import org.junit.Assert.* - /** * Example local unit test, which will execute on the development machine (host). * diff --git a/profile_hts/src/androidTest/java/no/nordicsemi/android/hts/ExampleInstrumentedTest.kt b/profile_hts/src/androidTest/java/no/nordicsemi/android/hts/ExampleInstrumentedTest.kt index 6b416079..1b48241c 100644 --- a/profile_hts/src/androidTest/java/no/nordicsemi/android/hts/ExampleInstrumentedTest.kt +++ b/profile_hts/src/androidTest/java/no/nordicsemi/android/hts/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package no.nordicsemi.android.hts -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSRepository.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSRepository.kt index 288827b9..cb056da3 100644 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSRepository.kt +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSRepository.kt @@ -1,7 +1,12 @@ package no.nordicsemi.android.hts.data +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import no.nordicsemi.android.service.BleManagerStatus import javax.inject.Inject import javax.inject.Singleton @@ -11,6 +16,12 @@ internal class HTSRepository @Inject constructor() { private val _data = MutableStateFlow(HTSData()) val data: StateFlow = _data + 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 setNewTemperature(temperature: Float) { _data.tryEmit(_data.value.copy(temperatureValue = temperature)) } @@ -23,7 +34,16 @@ internal class HTSRepository @Inject constructor() { _data.tryEmit(_data.value.copy(temperatureUnit = unit)) } + fun sendDisconnectCommand() { + _command.tryEmit(DisconnectCommand) + } + + fun setNewStatus(status: BleManagerStatus) { + _status.value = status + } + fun clear() { + _status.value = BleManagerStatus.CONNECTING _data.tryEmit(HTSData()) } } diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSServiceCommand.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSServiceCommand.kt new file mode 100644 index 00000000..bead4203 --- /dev/null +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSServiceCommand.kt @@ -0,0 +1,3 @@ +package no.nordicsemi.android.hts.data + +internal object DisconnectCommand diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSDateTimeParser.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSDateTimeParser.kt deleted file mode 100644 index 55d71293..00000000 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSDateTimeParser.kt +++ /dev/null @@ -1,53 +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.hts.repository - -import no.nordicsemi.android.ble.common.callback.DateTimeDataCallback -import no.nordicsemi.android.ble.data.Data -import java.util.* - -internal object HTSDateTimeParser { - /** - * Parses the date and time info. - * - * @param data - * @return time in human readable format - */ - fun parse(data: Data): String { - return parse(data, 0) - } - - /** - * Parses the date and time info. This data has 7 bytes - * - * @param data - * @param offset - * offset to start reading the time - * @return time in human readable format - */ - /* package */ - @JvmStatic - fun parse(data: Data, offset: Int): String { - val calendar = DateTimeDataCallback.readDateTime(data, offset) - return String.format(Locale.US, "%1\$te %1\$tb %1\$tY, %1\$tH:%1\$tM:%1\$tS", calendar) - } -} diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSManager.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSManager.kt index 5b4178c3..71d006ff 100644 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSManager.kt +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSManager.kt @@ -28,9 +28,7 @@ import android.content.Context import no.nordicsemi.android.ble.common.callback.ht.TemperatureMeasurementDataCallback import no.nordicsemi.android.ble.common.profile.ht.TemperatureType import no.nordicsemi.android.ble.common.profile.ht.TemperatureUnit -import no.nordicsemi.android.ble.data.Data import no.nordicsemi.android.hts.data.HTSRepository -import no.nordicsemi.android.log.LogContract import no.nordicsemi.android.service.BatteryManager import java.util.* @@ -50,13 +48,6 @@ internal class HTSManager internal constructor( private var htCharacteristic: BluetoothGattCharacteristic? = null private val temperatureMeasurementDataCallback = object : TemperatureMeasurementDataCallback() { - override fun onDataReceived(device: BluetoothDevice, data: Data) { - log( - LogContract.Log.Level.APPLICATION, - "\"" + HTSTemperatureMeasurementParser.parse(data) + "\" received" - ) - super.onDataReceived(device, data) - } override fun onTemperatureMeasurementReceived( device: BluetoothDevice, diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSService.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSService.kt index af6d4fc8..6f8d5b0a 100644 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSService.kt +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSService.kt @@ -1,6 +1,8 @@ package no.nordicsemi.android.hts.repository import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import no.nordicsemi.android.hts.data.HTSRepository import no.nordicsemi.android.service.ForegroundBleService import javax.inject.Inject @@ -9,7 +11,19 @@ import javax.inject.Inject internal class HTSService : ForegroundBleService() { @Inject - lateinit var dataHolder: HTSRepository + lateinit var repository: HTSRepository - override val manager: HTSManager by lazy { HTSManager(this, dataHolder) } + override val manager: HTSManager by lazy { HTSManager(this, repository) } + + override fun onCreate() { + super.onCreate() + + status.onEach { + repository.setNewStatus(it) + }.launchIn(scope) + + repository.command.onEach { + stopSelf() + }.launchIn(scope) + } } diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSTemperatureMeasurementParser.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSTemperatureMeasurementParser.kt deleted file mode 100644 index 1f6b8307..00000000 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSTemperatureMeasurementParser.kt +++ /dev/null @@ -1,73 +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.hts.repository - -import no.nordicsemi.android.ble.data.Data -import java.util.* - -private const val TEMPERATURE_UNIT_FLAG: Byte = 0x01 // 1 bit -private const val TIMESTAMP_FLAG: Byte = 0x02 // 1 bits -private const val TEMPERATURE_TYPE_FLAG: Byte = 0x04 // 1 bit - -internal object HTSTemperatureMeasurementParser { - - fun parse(data: Data): String { - var offset = 0 - val flags = data.getIntValue(Data.FORMAT_UINT8, offset++)!! - - /* - * false Temperature is in Celsius degrees - * true Temperature is in Fahrenheit degrees - */ - val fahrenheit = flags and TEMPERATURE_UNIT_FLAG.toInt() > 0 - - /* - * false No Timestamp in the packet - * true There is a timestamp information - */ - val timestampIncluded = flags and TIMESTAMP_FLAG.toInt() > 0 - - /* - * false Temperature type is not included - * true Temperature type included in the packet - */ - val temperatureTypeIncluded = flags and TEMPERATURE_TYPE_FLAG.toInt() > 0 - val tempValue = data.getFloatValue(Data.FORMAT_FLOAT, offset)!! - offset += 4 - var dateTime: String? = null - if (timestampIncluded) { - dateTime = HTSDateTimeParser.parse(data, offset) - offset += 7 - } - var type: String? = null - if (temperatureTypeIncluded) { - type = HTSTemperatureTypeParser.parse(data, offset) - // offset++; - } - val builder = StringBuilder() - builder.append(String.format(Locale.US, "%.02f", tempValue)) - if (fahrenheit) builder.append("°F") else builder.append("°C") - if (timestampIncluded) builder.append("\nTime: ").append(dateTime) - if (temperatureTypeIncluded) builder.append("\nType: ").append(type) - return builder.toString() - } -} diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSTemperatureTypeParser.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSTemperatureTypeParser.kt deleted file mode 100644 index def995ad..00000000 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSTemperatureTypeParser.kt +++ /dev/null @@ -1,47 +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.hts.repository - -import no.nordicsemi.android.ble.data.Data - -internal object HTSTemperatureTypeParser { - fun parse(data: Data): String { - return parse(data, 0) - } - - /* package */ - @JvmStatic - fun parse(data: Data, offset: Int): String { - return when (data.value!![offset].toInt()) { - 1 -> "Armpit" - 2 -> "Body (general)" - 3 -> "Ear (usually ear lobe)" - 4 -> "Finger" - 5 -> "Gastro-intestinal Tract" - 6 -> "Mouth" - 7 -> "Rectum" - 8 -> "Toe" - 9 -> "Tympanum (ear drum)" - else -> "Unknown" - } - } -} diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSContentView.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSContentView.kt index a7f78886..e207b557 100644 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSContentView.kt +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSContentView.kt @@ -1,6 +1,10 @@ package no.nordicsemi.android.hts.view -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreen.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreen.kt index 2ba93e44..850d14df 100644 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreen.kt +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreen.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.hts.R -import no.nordicsemi.android.hts.data.HTSData import no.nordicsemi.android.hts.repository.HTSService import no.nordicsemi.android.hts.viewmodel.HTSViewModel 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 HTSScreen(finishAction: () -> Unit) { val viewModel: HTSViewModel = hiltViewModel() val state = viewModel.state.collectAsState().value - val isActive = viewModel.isActive.collectAsState().value val context = LocalContext.current - LaunchedEffect(isActive) { - if (!isActive) { - finishAction() - } - if (context.isServiceRunning(HTSService::class.java.name)) { - val intent = Intent(context, HTSService::class.java) - context.stopService(intent) - } - } - - LaunchedEffect("start-service") { - if (!context.isServiceRunning(HTSService::class.java.name)) { + LaunchedEffect(state.isActive) { + if (state.isActive) { val intent = Intent(context, HTSService::class.java) context.startService(intent) + } else { + finishAction() } } - HTSView(state) { viewModel.onEvent(it) } + HTSView(state.viewState) { viewModel.onEvent(it) } } @Composable -private fun HTSView(state: HTSData, onEvent: (HTSScreenViewEvent) -> Unit) { +private fun HTSView(state: HTSViewState, onEvent: (HTSScreenViewEvent) -> Unit) { Column { BackIconAppBar(stringResource(id = R.string.hts_title)) { onEvent(DisconnectEvent) } - HTSContentView(state) { onEvent(it) } + when (state) { + is DisplayDataState -> HTSContentView(state.data) { onEvent(it) } + LoadingState -> DeviceConnectingView() + }.exhaustive } } diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSState.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSState.kt new file mode 100644 index 00000000..59bed010 --- /dev/null +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSState.kt @@ -0,0 +1,14 @@ +package no.nordicsemi.android.hts.view + +import no.nordicsemi.android.hts.data.HTSData + +internal data class HTSState( + val viewState: HTSViewState, + val isActive: Boolean = true +) + +internal sealed class HTSViewState + +internal object LoadingState : HTSViewState() + +internal data class DisplayDataState(val data: HTSData) : HTSViewState() diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/viewmodel/HTSViewModel.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/viewmodel/HTSViewModel.kt index aee68046..60748833 100644 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/viewmodel/HTSViewModel.kt +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/viewmodel/HTSViewModel.kt @@ -1,20 +1,34 @@ package no.nordicsemi.android.hts.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.hts.data.HTSRepository import no.nordicsemi.android.hts.view.DisconnectEvent +import no.nordicsemi.android.hts.view.DisplayDataState import no.nordicsemi.android.hts.view.HTSScreenViewEvent +import no.nordicsemi.android.hts.view.HTSState +import no.nordicsemi.android.hts.view.LoadingState import no.nordicsemi.android.hts.view.OnTemperatureUnitSelected -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 HTSViewModel @Inject constructor( - private val dataHolder: HTSRepository -) : CloseableViewModel() { + private val repository: HTSRepository +) : ViewModel() { - val state = dataHolder.data + val state = repository.data.combine(repository.status) { data, status -> + when (status) { + BleManagerStatus.CONNECTING -> HTSState(LoadingState) + BleManagerStatus.OK -> HTSState(DisplayDataState(data)) + BleManagerStatus.DISCONNECTED -> HTSState(DisplayDataState(data), false) + } + }.stateIn(viewModelScope, SharingStarted.Lazily, HTSState(LoadingState)) fun onEvent(event: HTSScreenViewEvent) { when (event) { @@ -24,11 +38,16 @@ internal class HTSViewModel @Inject constructor( } private fun onDisconnectButtonClick() { - finish() - dataHolder.clear() + repository.sendDisconnectCommand() + repository.clear() } private fun onTemperatureUnitSelected(event: OnTemperatureUnitSelected) { - dataHolder.setTemperatureUnit(event.value) + repository.setTemperatureUnit(event.value) + } + + override fun onCleared() { + super.onCleared() + repository.clear() } } diff --git a/profile_hts/src/test/java/no/nordicsemi/android/hts/ExampleUnitTest.kt b/profile_hts/src/test/java/no/nordicsemi/android/hts/ExampleUnitTest.kt index 67ea2c81..5468079d 100644 --- a/profile_hts/src/test/java/no/nordicsemi/android/hts/ExampleUnitTest.kt +++ b/profile_hts/src/test/java/no/nordicsemi/android/hts/ExampleUnitTest.kt @@ -1,9 +1,8 @@ package no.nordicsemi.android.hts +import org.junit.Assert.assertEquals import org.junit.Test -import org.junit.Assert.* - /** * Example local unit test, which will execute on the development machine (host). * diff --git a/profile_prx/src/androidTest/java/no/nordicsemi/android/prx/ExampleInstrumentedTest.kt b/profile_prx/src/androidTest/java/no/nordicsemi/android/prx/ExampleInstrumentedTest.kt index 5e8752f6..d446cd60 100644 --- a/profile_prx/src/androidTest/java/no/nordicsemi/android/prx/ExampleInstrumentedTest.kt +++ b/profile_prx/src/androidTest/java/no/nordicsemi/android/prx/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package no.nordicsemi.android.prx -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXCommand.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXCommand.kt index 2a7d78c0..92000346 100644 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXCommand.kt +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXCommand.kt @@ -5,3 +5,5 @@ internal sealed class PRXCommand internal object EnableAlarm : PRXCommand() internal object DisableAlarm : PRXCommand() + +internal object Disconnect : PRXCommand() diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXRepository.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXRepository.kt index bb4b0367..8e28da2a 100644 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXRepository.kt +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXRepository.kt @@ -5,6 +5,8 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow 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 +19,9 @@ internal class PRXRepository @Inject constructor() { private val _command = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) val command = _command.asSharedFlow() + private val _status = MutableStateFlow(BleManagerStatus.CONNECTING) + val status = _status.asStateFlow() + fun setBatteryLevel(batteryLevel: Int) { _data.tryEmit(_data.value.copy(batteryLevel = batteryLevel)) } @@ -34,7 +39,12 @@ internal class PRXRepository @Inject constructor() { _command.tryEmit(command) } - fun clear(){ + fun setNewStatus(status: BleManagerStatus) { + _status.value = status + } + + fun clear() { + _status.value = BleManagerStatus.CONNECTING _data.tryEmit(PRXData()) } } diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/service/PRXAlertLevelParser.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/service/PRXAlertLevelParser.kt deleted file mode 100644 index 43084db8..00000000 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/service/PRXAlertLevelParser.kt +++ /dev/null @@ -1,48 +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.prx.service - -import android.bluetooth.BluetoothGattCharacteristic -import no.nordicsemi.android.ble.data.Data - -internal object PRXAlertLevelParser { - - fun parse(characteristic: BluetoothGattCharacteristic?): String { - return parse(Data.from(characteristic!!)) - } - - /** - * Parses the alert level. - * - * @param data - * @return alert level in human readable format - */ - fun parse(data: Data): String { - val value = data.getIntValue(Data.FORMAT_UINT8, 0)!! - return when (value) { - 0 -> "No Alert" - 1 -> "Mild Alert" - 2 -> "High Alert" - else -> "Reserved value ($value)" - } - } -} diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/service/PRXManager.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/service/PRXManager.kt index 33dbdfbe..f9c52a10 100644 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/service/PRXManager.kt +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/service/PRXManager.kt @@ -30,9 +30,7 @@ import android.util.Log import no.nordicsemi.android.ble.callback.FailCallback import no.nordicsemi.android.ble.common.callback.alert.AlertLevelDataCallback import no.nordicsemi.android.ble.common.data.alert.AlertLevelData -import no.nordicsemi.android.ble.data.Data import no.nordicsemi.android.ble.error.GattError -import no.nordicsemi.android.log.LogContract import no.nordicsemi.android.prx.data.PRXRepository import no.nordicsemi.android.service.BatteryManager import java.util.* @@ -161,12 +159,6 @@ internal class PRXManager( if (on) "Setting alarm to HIGH..." else "Disabling alarm..." ) } - .with { _: BluetoothDevice, data: Data -> - log( - LogContract.Log.Level.APPLICATION, - "\"" + PRXAlertLevelParser.parse(data) + "\" sent" - ) - } .done { device: BluetoothDevice? -> isAlertEnabled = on dataHolder.setRemoteAlarmLevel(on) 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 2d71f6ef..e2bdb80f 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 @@ -1,11 +1,11 @@ package no.nordicsemi.android.prx.service -import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import no.nordicsemi.android.prx.data.AlarmLevel import no.nordicsemi.android.prx.data.DisableAlarm +import no.nordicsemi.android.prx.data.Disconnect import no.nordicsemi.android.prx.data.EnableAlarm import no.nordicsemi.android.prx.data.PRXRepository import no.nordicsemi.android.service.ForegroundBleService @@ -16,7 +16,7 @@ import javax.inject.Inject internal class PRXService : ForegroundBleService() { @Inject - lateinit var dataHolder: PRXRepository + lateinit var repository: PRXRepository @Inject lateinit var alarmHandler: AlarmHandler @@ -24,7 +24,7 @@ internal class PRXService : ForegroundBleService() { private var serverManager: ProximityServerManager = ProximityServerManager(this) override val manager: PRXManager by lazy { - PRXManager(this, dataHolder).apply { + PRXManager(this, repository).apply { useServer(serverManager) } } @@ -34,14 +34,19 @@ internal class PRXService : ForegroundBleService() { serverManager.open() - dataHolder.command.onEach { + status.onEach { + repository.setNewStatus(it) + }.launchIn(scope) + + repository.command.onEach { when (it) { DisableAlarm -> manager.writeImmediateAlert(false) EnableAlarm -> manager.writeImmediateAlert(true) + Disconnect -> stopSelf() }.exhaustive }.launchIn(scope) - dataHolder.data.onEach { + repository.data.onEach { if (it.localAlarmLevel != AlarmLevel.NONE) { alarmHandler.playAlarm() } else { diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXScreen.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXScreen.kt index b9081b61..34d4fb0c 100644 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXScreen.kt +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXScreen.kt @@ -8,55 +8,42 @@ import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import no.nordicsemi.android.prx.R -import no.nordicsemi.android.prx.data.PRXData import no.nordicsemi.android.prx.service.PRXService import no.nordicsemi.android.prx.viewmodel.PRXViewModel 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 PRXScreen(finishAction: () -> Unit) { val viewModel: PRXViewModel = hiltViewModel() val state = viewModel.state.collectAsState().value - val isActive = viewModel.isActive.collectAsState().value val context = LocalContext.current - LaunchedEffect(isActive) { - if (!isActive) { - finishAction() - } - if (context.isServiceRunning(PRXService::class.java.name)) { - val intent = Intent(context, PRXService::class.java) - context.stopService(intent) - } - } - - LaunchedEffect("start-service") { - if (!context.isServiceRunning(PRXService::class.java.name)) { + LaunchedEffect(state.isActive) { + if (state.isActive) { val intent = Intent(context, PRXService::class.java) context.startService(intent) + } else { + finishAction() } } - PRXView(state) { viewModel.onEvent(it) } + PRXView(state.viewState) { viewModel.onEvent(it) } } @Composable -private fun PRXView(state: PRXData, onEvent: (PRXScreenViewEvent) -> Unit) { +private fun PRXView(state: PRXViewState, onEvent: (PRXScreenViewEvent) -> Unit) { Column(horizontalAlignment = Alignment.CenterHorizontally) { BackIconAppBar(stringResource(id = R.string.prx_title)) { onEvent(DisconnectEvent) } - ContentView(state) { onEvent(it) } + when (state) { + is DisplayDataState -> ContentView(state.data) { onEvent(it) } + LoadingState -> DeviceConnectingView() + }.exhaustive } } - -@Preview -@Composable -private fun PRXViewPreview() { - PRXView(PRXData()) { } -} diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXState.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXState.kt new file mode 100644 index 00000000..6445baa8 --- /dev/null +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXState.kt @@ -0,0 +1,14 @@ +package no.nordicsemi.android.prx.view + +import no.nordicsemi.android.prx.data.PRXData + +internal data class PRXState( + val viewState: PRXViewState, + val isActive: Boolean = true +) + +internal sealed class PRXViewState + +internal object LoadingState : PRXViewState() + +internal data class DisplayDataState(val data: PRXData) : PRXViewState() diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/viewmodel/PRXViewModel.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/viewmodel/PRXViewModel.kt index 8c394cc2..9b24debc 100644 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/viewmodel/PRXViewModel.kt +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/viewmodel/PRXViewModel.kt @@ -1,34 +1,54 @@ package no.nordicsemi.android.prx.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.prx.data.DisableAlarm +import no.nordicsemi.android.prx.data.Disconnect import no.nordicsemi.android.prx.data.EnableAlarm import no.nordicsemi.android.prx.data.PRXRepository import no.nordicsemi.android.prx.view.DisconnectEvent +import no.nordicsemi.android.prx.view.DisplayDataState +import no.nordicsemi.android.prx.view.LoadingState import no.nordicsemi.android.prx.view.PRXScreenViewEvent +import no.nordicsemi.android.prx.view.PRXState import no.nordicsemi.android.prx.view.TurnOffAlert import no.nordicsemi.android.prx.view.TurnOnAlert -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 PRXViewModel @Inject constructor( - private val dataHolder: PRXRepository -) : CloseableViewModel() { + private val repository: PRXRepository +) : ViewModel() { - val state = dataHolder.data + val state = repository.data.combine(repository.status) { data, status -> + when (status) { + BleManagerStatus.CONNECTING -> PRXState(LoadingState) + BleManagerStatus.OK -> PRXState(DisplayDataState(data)) + BleManagerStatus.DISCONNECTED -> PRXState(DisplayDataState(data), false) + } + }.stateIn(viewModelScope, SharingStarted.Lazily, PRXState(LoadingState)) fun onEvent(event: PRXScreenViewEvent) { when (event) { DisconnectEvent -> onDisconnectButtonClick() - TurnOffAlert -> dataHolder.invokeCommand(DisableAlarm) - TurnOnAlert -> dataHolder.invokeCommand(EnableAlarm) + TurnOffAlert -> repository.invokeCommand(DisableAlarm) + TurnOnAlert -> repository.invokeCommand(EnableAlarm) }.exhaustive } private fun onDisconnectButtonClick() { - finish() - dataHolder.clear() + repository.invokeCommand(Disconnect) + repository.clear() + } + + override fun onCleared() { + super.onCleared() + repository.clear() } } diff --git a/profile_prx/src/test/java/no/nordicsemi/android/prx/ExampleUnitTest.kt b/profile_prx/src/test/java/no/nordicsemi/android/prx/ExampleUnitTest.kt index 774842bf..03ecdc2a 100644 --- a/profile_prx/src/test/java/no/nordicsemi/android/prx/ExampleUnitTest.kt +++ b/profile_prx/src/test/java/no/nordicsemi/android/prx/ExampleUnitTest.kt @@ -1,9 +1,8 @@ package no.nordicsemi.android.prx +import org.junit.Assert.assertEquals import org.junit.Test -import org.junit.Assert.* - /** * Example local unit test, which will execute on the development machine (host). * diff --git a/profile_rscs/src/androidTest/java/no/nordicsemi/android/rscs/ExampleInstrumentedTest.kt b/profile_rscs/src/androidTest/java/no/nordicsemi/android/rscs/ExampleInstrumentedTest.kt index 33bfc46c..169f65bb 100644 --- a/profile_rscs/src/androidTest/java/no/nordicsemi/android/rscs/ExampleInstrumentedTest.kt +++ b/profile_rscs/src/androidTest/java/no/nordicsemi/android/rscs/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package no.nordicsemi.android.rscs -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/data/RSCSRepository.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/data/RSCSRepository.kt index 9a16bbe5..1e147ce1 100644 --- a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/data/RSCSRepository.kt +++ b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/data/RSCSRepository.kt @@ -1,7 +1,12 @@ package no.nordicsemi.android.rscs.data +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import no.nordicsemi.android.service.BleManagerStatus import javax.inject.Inject import javax.inject.Singleton @@ -9,7 +14,13 @@ import javax.inject.Singleton internal class RSCSRepository @Inject constructor() { private val _data = MutableStateFlow(RSCSData()) - val data: StateFlow = _data + val data: StateFlow = _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 setNewData( running: Boolean, @@ -27,11 +38,20 @@ internal class RSCSRepository @Inject constructor() { )) } + fun setNewStatus(status: BleManagerStatus) { + _status.value = status + } + fun setBatteryLevel(batteryLevel: Int) { _data.tryEmit(_data.value.copy(batteryLevel = batteryLevel)) } + fun sendDisconnectCommand() { + _command.tryEmit(DisconnectCommand) + } + fun clear() { + _status.value = BleManagerStatus.CONNECTING _data.tryEmit(RSCSData()) } } diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/data/RSCSServiceCommand.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/data/RSCSServiceCommand.kt new file mode 100644 index 00000000..87a8636e --- /dev/null +++ b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/data/RSCSServiceCommand.kt @@ -0,0 +1,3 @@ +package no.nordicsemi.android.rscs.data + +internal object DisconnectCommand diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCSService.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCSService.kt index 77e50ef8..800a493b 100644 --- a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCSService.kt +++ b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCSService.kt @@ -1,6 +1,8 @@ package no.nordicsemi.android.rscs.service import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import no.nordicsemi.android.rscs.data.RSCSRepository import no.nordicsemi.android.service.ForegroundBleService import javax.inject.Inject @@ -9,7 +11,19 @@ import javax.inject.Inject internal class RSCSService : ForegroundBleService() { @Inject - lateinit var dataHolder: RSCSRepository + lateinit var repository: RSCSRepository - override val manager: RSCSManager by lazy { RSCSManager(this, dataHolder) } + override val manager: RSCSManager by lazy { RSCSManager(this, repository) } + + override fun onCreate() { + super.onCreate() + + status.onEach { + repository.setNewStatus(it) + }.launchIn(scope) + + repository.command.onEach { + stopSelf() + }.launchIn(scope) + } } diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCSScreen.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCSScreen.kt index 67ff8b11..1c95baa5 100644 --- a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCSScreen.kt +++ b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCSScreen.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.rscs.R -import no.nordicsemi.android.rscs.data.RSCSData import no.nordicsemi.android.rscs.service.RSCSService import no.nordicsemi.android.rscs.viewmodel.RSCSViewModel 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 RSCSScreen(finishAction: () -> Unit) { val viewModel: RSCSViewModel = 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(RSCSService::class.java.name)) { - val intent = Intent(context, RSCSService::class.java) - context.stopService(intent) - } - } - - LaunchedEffect("start-service") { - if (!context.isServiceRunning(RSCSService::class.java.name)) { + LaunchedEffect(state.isActive) { + if (state.isActive) { val intent = Intent(context, RSCSService::class.java) context.startService(intent) + } else { + finishAction() } } - RSCSView(state) { viewModel.onEvent(it) } + RSCSView(state.viewState) { viewModel.onEvent(it) } } @Composable -private fun RSCSView(state: RSCSData, onEvent: (RSCScreenViewEvent) -> Unit) { +private fun RSCSView(state: RSCSViewState, onEvent: (RSCScreenViewEvent) -> Unit) { Column { BackIconAppBar(stringResource(id = R.string.rscs_title)) { onEvent(DisconnectEvent) } - RSCSContentView(state) { onEvent(it) } + when (state) { + is DisplayDataState -> RSCSContentView(state.data) { onEvent(it) } + LoadingState -> DeviceConnectingView() + }.exhaustive } } diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCSState.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCSState.kt new file mode 100644 index 00000000..c989d8a3 --- /dev/null +++ b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCSState.kt @@ -0,0 +1,14 @@ +package no.nordicsemi.android.rscs.view + +import no.nordicsemi.android.rscs.data.RSCSData + +internal data class RSCSState( + val viewState: RSCSViewState, + val isActive: Boolean = true +) + +internal sealed class RSCSViewState + +internal object LoadingState : RSCSViewState() + +internal data class DisplayDataState(val data: RSCSData) : RSCSViewState() diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/viewmodel/RSCSViewModel.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/viewmodel/RSCSViewModel.kt index cced1c3b..e35911b8 100644 --- a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/viewmodel/RSCSViewModel.kt +++ b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/viewmodel/RSCSViewModel.kt @@ -1,19 +1,33 @@ package no.nordicsemi.android.rscs.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.rscs.data.RSCSRepository import no.nordicsemi.android.rscs.view.DisconnectEvent +import no.nordicsemi.android.rscs.view.DisplayDataState +import no.nordicsemi.android.rscs.view.LoadingState +import no.nordicsemi.android.rscs.view.RSCSState import no.nordicsemi.android.rscs.view.RSCScreenViewEvent -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 RSCSViewModel @Inject constructor( - private val dataHolder: RSCSRepository -) : CloseableViewModel() { + private val repository: RSCSRepository +) : ViewModel() { - val state = dataHolder.data + val state = repository.data.combine(repository.status) { data, status -> + when (status) { + BleManagerStatus.CONNECTING -> RSCSState(LoadingState) + BleManagerStatus.OK -> RSCSState(DisplayDataState(data)) + BleManagerStatus.DISCONNECTED -> RSCSState(DisplayDataState(data), false) + } + }.stateIn(viewModelScope, SharingStarted.Lazily, RSCSState(LoadingState)) fun onEvent(event: RSCScreenViewEvent) { when (event) { @@ -22,7 +36,12 @@ internal class RSCSViewModel @Inject constructor( } private fun onDisconnectButtonClick() { - finish() - dataHolder.clear() + repository.sendDisconnectCommand() + repository.clear() + } + + override fun onCleared() { + super.onCleared() + repository.clear() } } diff --git a/profile_rscs/src/test/java/no/nordicsemi/android/rscs/ExampleUnitTest.kt b/profile_rscs/src/test/java/no/nordicsemi/android/rscs/ExampleUnitTest.kt index a5629c50..bf6cceb1 100644 --- a/profile_rscs/src/test/java/no/nordicsemi/android/rscs/ExampleUnitTest.kt +++ b/profile_rscs/src/test/java/no/nordicsemi/android/rscs/ExampleUnitTest.kt @@ -1,9 +1,8 @@ package no.nordicsemi.android.rscs +import org.junit.Assert.assertEquals import org.junit.Test -import org.junit.Assert.* - /** * Example local unit test, which will execute on the development machine (host). * 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 82c0faaf..6713af34 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 @@ -47,4 +47,8 @@ internal class UARTRepository @Inject constructor() { fun setNewStatus(status: BleManagerStatus) { _status.value = status } + + fun clear() { + _status.value = BleManagerStatus.CONNECTING + } } diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTAddMacroDialog.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTAddMacroDialog.kt index a9c93833..0aba9fd1 100644 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTAddMacroDialog.kt +++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTAddMacroDialog.kt @@ -2,7 +2,6 @@ package no.nordicsemi.android.uart.view import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTViews.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTViews.kt index dca55b42..ffe1e17f 100644 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTViews.kt +++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTViews.kt @@ -1,7 +1,11 @@ package no.nordicsemi.android.uart.view import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.PlayArrow 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 d2ba6ed4..1b3b10cd 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 @@ -42,4 +42,9 @@ internal class UARTViewModel @Inject constructor( is OnRunMacro -> repository.sendNewCommand(SendTextCommand(event.macro.command)) }.exhaustive } + + override fun onCleared() { + super.onCleared() + repository.clear() + } }