diff --git a/app/build.gradle b/app/build.gradle index 5f9029ee..8654dc87 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -50,11 +50,11 @@ android { dependencies { //Hilt requires to implement every module in the main app module //https://github.com/google/dagger/issues/2123 - implementation project(":feature_csc") - implementation project(":feature_hrs") - implementation project(":feature_hts") - implementation project(":feature_gls") - implementation project(':feature_scanner') + implementation project(':profile_csc') + implementation project(':profile_hrs') + implementation project(':profile_hts') + implementation project(':profile_gls') + implementation project(':profile_scanner') implementation project(":lib_theme") implementation project(":lib_utils") diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/FeatureButton.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/FeatureButton.kt index 6aab9719..fd3eee3e 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/FeatureButton.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/FeatureButton.kt @@ -4,16 +4,16 @@ import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row 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.material.Button -import androidx.compose.material.ButtonDefaults import androidx.compose.material.Text 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.graphics.Color @@ -25,10 +25,13 @@ import no.nordicsemi.android.theme.NordicColors @Composable fun FeatureButton(@DrawableRes iconId: Int, @StringRes nameId: Int, onClick: () -> Unit) { - Button( - modifier = Modifier.fillMaxWidth(), - onClick = { onClick() }, - colors = ButtonDefaults.buttonColors(backgroundColor = NordicColors.NordicGray4.value()), + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onClick() } + .background(NordicColors.ItemHighlight.value()) + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically ) { Image( painter = painterResource(iconId), @@ -41,14 +44,10 @@ fun FeatureButton(@DrawableRes iconId: Int, @StringRes nameId: Int, onClick: () ) Row( modifier = Modifier - .padding(16.dp) .fillMaxWidth(), horizontalArrangement = Arrangement.Center ) { - Text( - text = stringResource(id = nameId), - modifier = Modifier.padding(16.dp), - ) + Text(text = stringResource(id = nameId)) } } } 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 aca4b7b3..27fe1b12 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt @@ -1,10 +1,11 @@ package no.nordicsemi.android.nrftoolbox +import android.app.Activity import androidx.activity.OnBackPressedCallback import androidx.activity.compose.LocalOnBackPressedDispatcherOwner import androidx.compose.foundation.layout.Column -import androidx.compose.material.Text -import androidx.compose.material.TopAppBar +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -12,14 +13,17 @@ import androidx.compose.runtime.SideEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import no.nordicsemi.android.csc.view.CscScreen +import no.nordicsemi.android.csc.view.CSCScreen import no.nordicsemi.android.gls.view.GLSScreen import no.nordicsemi.android.hrs.view.HRSScreen import no.nordicsemi.android.hts.view.HTSScreen @@ -28,6 +32,7 @@ import no.nordicsemi.android.scanner.view.BluetoothNotEnabledScreen import no.nordicsemi.android.scanner.view.RequestPermissionScreen import no.nordicsemi.android.scanner.view.ScanDeviceScreen import no.nordicsemi.android.scanner.view.ScanDeviceScreenResult +import no.nordicsemi.android.theme.view.CloseIconAppBar import no.nordicsemi.android.utils.exhaustive @Composable @@ -42,12 +47,12 @@ internal fun HomeScreen() { NavHost(navController = navController, startDestination = NavDestination.HOME.id) { composable(NavDestination.HOME.id) { HomeView { viewModel.navigate(it) } } - composable(NavDestination.CSC.id) { CscScreen { viewModel.navigateUp() } } + composable(NavDestination.CSC.id) { CSCScreen { viewModel.navigateUp() } } composable(NavDestination.HRS.id) { HRSScreen { viewModel.navigateUp() } } composable(NavDestination.HTS.id) { HTSScreen { viewModel.navigateUp() } } composable(NavDestination.GLS.id) { GLSScreen { viewModel.navigateUp() } } composable(NavDestination.REQUEST_PERMISSION.id) { RequestPermissionScreen(continueAction) } - composable(NavDestination.BLUETOOTH_NOT_AVAILABLE.id) { BluetoothNotAvailableScreen() } + composable(NavDestination.BLUETOOTH_NOT_AVAILABLE.id) { BluetoothNotAvailableScreen{ viewModel.finish() } } composable(NavDestination.BLUETOOTH_NOT_ENABLED.id) { BluetoothNotEnabledScreen(continueAction) } @@ -69,11 +74,18 @@ internal fun HomeScreen() { @Composable fun HomeView(callback: (NavDestination) -> Unit) { Column { - TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) }) + val context = LocalContext.current + CloseIconAppBar(stringResource(id = R.string.app_name)) { + (context as? Activity)?.finish() + } FeatureButton(R.drawable.ic_csc, R.string.csc_module) { callback(NavDestination.CSC) } + Spacer(modifier = Modifier.height(1.dp)) FeatureButton(R.drawable.ic_hrs, R.string.hrs_module) { callback(NavDestination.HRS) } + Spacer(modifier = Modifier.height(1.dp)) FeatureButton(R.drawable.ic_gls, R.string.gls_module) { callback(NavDestination.GLS) } + Spacer(modifier = Modifier.height(1.dp)) + FeatureButton(R.drawable.ic_hts, R.string.hts_module) { callback(NavDestination.HTS) } } } @@ -102,7 +114,6 @@ private fun BackHandler(enabled: Boolean = true, onBack: () -> Unit) { } } - @Preview(showBackground = true) @Composable fun DefaultPreview() { diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/MainActivity.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/MainActivity.kt index de07cd82..026d2ae7 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/MainActivity.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/MainActivity.kt @@ -1,15 +1,15 @@ package no.nordicsemi.android.nrftoolbox import android.os.Bundle -import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import dagger.hilt.android.AndroidEntryPoint import no.nordicsemi.android.theme.TestTheme @AndroidEntryPoint -class MainActivity : ComponentActivity() { +class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt index c7090164..ce115ffb 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt @@ -4,5 +4,4 @@ import android.app.Application import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp -class NrfToolboxApplication : Application() { -} \ No newline at end of file +class NrfToolboxApplication : Application() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f8f8f56c..4188a864 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,4 +2,5 @@ CSC HRS GLS + HTS \ No newline at end of file diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/service/CSCDataReadBroadcast.kt b/feature_csc/src/main/java/no/nordicsemi/android/csc/service/CSCDataReadBroadcast.kt deleted file mode 100644 index d4c1e1ff..00000000 --- a/feature_csc/src/main/java/no/nordicsemi/android/csc/service/CSCDataReadBroadcast.kt +++ /dev/null @@ -1,24 +0,0 @@ -package no.nordicsemi.android.csc.service - -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow -import no.nordicsemi.android.csc.data.CSCData -import no.nordicsemi.android.service.BluetoothDataReadBroadcast -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -internal class CSCDataReadBroadcast @Inject constructor() : BluetoothDataReadBroadcast() { - - private val _wheelSize = MutableSharedFlow( - replay = 1, - extraBufferCapacity = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - val wheelSize: SharedFlow = _wheelSize - - fun setWheelSize(size: Int) { - _wheelSize.tryEmit(size) - } -} diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/service/CSCManagerCallbacks.kt b/feature_csc/src/main/java/no/nordicsemi/android/csc/service/CSCManagerCallbacks.kt deleted file mode 100644 index b5596083..00000000 --- a/feature_csc/src/main/java/no/nordicsemi/android/csc/service/CSCManagerCallbacks.kt +++ /dev/null @@ -1,27 +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.csc.service - -import no.nordicsemi.android.ble.common.profile.csc.CyclingSpeedAndCadenceCallback -import no.nordicsemi.android.service.BatteryManagerCallbacks - -internal interface CSCManagerCallbacks : BatteryManagerCallbacks, CyclingSpeedAndCadenceCallback diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/service/CSCService.kt b/feature_csc/src/main/java/no/nordicsemi/android/csc/service/CSCService.kt deleted file mode 100644 index 2857cfcc..00000000 --- a/feature_csc/src/main/java/no/nordicsemi/android/csc/service/CSCService.kt +++ /dev/null @@ -1,59 +0,0 @@ -package no.nordicsemi.android.csc.service - -import android.bluetooth.BluetoothDevice -import androidx.lifecycle.lifecycleScope -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import no.nordicsemi.android.csc.data.CSCData -import no.nordicsemi.android.service.ForegroundBleService -import no.nordicsemi.android.service.LoggableBleManager -import javax.inject.Inject - -@AndroidEntryPoint -internal class CSCService : ForegroundBleService(), CSCManagerCallbacks { - - private var data = CSCData() - - @Inject - lateinit var localBroadcast: CSCDataReadBroadcast - - override val manager: CSCManager by lazy { - CSCManager(this).apply { - setGattCallbacks(this@CSCService) - } - } - - override fun initializeManager(): LoggableBleManager { - return manager - } - - override fun onCreate() { - super.onCreate() - - localBroadcast.wheelSize.onEach { - manager.setWheelSize(it) - }.launchIn(lifecycleScope) - } - - override fun onDistanceChanged( - device: BluetoothDevice, - totalDistance: Float, - distance: Float, - speed: Float - ) { - localBroadcast.offer(data.copy(speed = speed, distance = distance, totalDistance = totalDistance)) - } - - override fun onCrankDataChanged( - device: BluetoothDevice, - crankCadence: Float, - gearRatio: Float - ) { - localBroadcast.offer(data.copy(cadence = crankCadence.toInt(), gearRatio = gearRatio)) - } - - override fun onBatteryLevelChanged(device: BluetoothDevice, batteryLevel: Int) { - localBroadcast.offer(data.copy(batteryLevel = batteryLevel)) - } -} \ No newline at end of file diff --git a/feature_gls/src/main/AndroidManifest.xml b/feature_gls/src/main/AndroidManifest.xml deleted file mode 100644 index 37b4a974..00000000 --- a/feature_gls/src/main/AndroidManifest.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/feature_gls/src/main/java/no/nordicsemi/android/gls/data/GLSData.kt b/feature_gls/src/main/java/no/nordicsemi/android/gls/data/GLSData.kt deleted file mode 100644 index 7b429a39..00000000 --- a/feature_gls/src/main/java/no/nordicsemi/android/gls/data/GLSData.kt +++ /dev/null @@ -1,12 +0,0 @@ -package no.nordicsemi.android.gls.data - -internal data class GLSData( - val record: List = emptyList(), - val batteryLevel: Int = 0, - val requestStatus: RequestStatus = RequestStatus.IDLE, - val isDeviceBonded: Boolean = false -) - -internal enum class RequestStatus { - IDLE, PENDING, SUCCESS, ABORTED, FAILED, NOT_SUPPORTED -} diff --git a/feature_gls/src/main/java/no/nordicsemi/android/gls/viewmodel/GLSScreenViewEvent.kt b/feature_gls/src/main/java/no/nordicsemi/android/gls/viewmodel/GLSScreenViewEvent.kt deleted file mode 100644 index e1ef521a..00000000 --- a/feature_gls/src/main/java/no/nordicsemi/android/gls/viewmodel/GLSScreenViewEvent.kt +++ /dev/null @@ -1,5 +0,0 @@ -package no.nordicsemi.android.gls.viewmodel - -sealed class GLSScreenViewEvent - -object DisconnectEvent : GLSScreenViewEvent() diff --git a/feature_gls/src/main/java/no/nordicsemi/android/gls/viewmodel/GLSViewModel.kt b/feature_gls/src/main/java/no/nordicsemi/android/gls/viewmodel/GLSViewModel.kt deleted file mode 100644 index 11acfcb7..00000000 --- a/feature_gls/src/main/java/no/nordicsemi/android/gls/viewmodel/GLSViewModel.kt +++ /dev/null @@ -1,24 +0,0 @@ -package no.nordicsemi.android.gls.viewmodel - -import androidx.lifecycle.ViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import no.nordicsemi.android.gls.repository.GLSManager -import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder -import javax.inject.Inject - -@HiltViewModel -internal class GLSViewModel @Inject constructor( - private val glsManager: GLSManager, - private val deviceHolder: SelectedBluetoothDeviceHolder -) : ViewModel() { - - val state = glsManager.data - - fun bondDevice() { - if (deviceHolder.isDeviceBonded()) { - deviceHolder.bondDevice() - } else { - //start work - } - } -} diff --git a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSDataBroadcast.kt b/feature_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSDataBroadcast.kt deleted file mode 100644 index ac426907..00000000 --- a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSDataBroadcast.kt +++ /dev/null @@ -1,9 +0,0 @@ -package no.nordicsemi.android.hrs.service - -import no.nordicsemi.android.hrs.data.HRSData -import no.nordicsemi.android.service.BluetoothDataReadBroadcast -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -internal class HRSDataBroadcast @Inject constructor() : BluetoothDataReadBroadcast() diff --git a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSManagerCallbacks.kt b/feature_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSManagerCallbacks.kt deleted file mode 100644 index dbb1cb6a..00000000 --- a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSManagerCallbacks.kt +++ /dev/null @@ -1,29 +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.common.profile.hr.BodySensorLocationCallback -import no.nordicsemi.android.ble.common.profile.hr.HeartRateMeasurementCallback -import no.nordicsemi.android.service.BatteryManagerCallbacks - -interface HRSManagerCallbacks - : BatteryManagerCallbacks, BodySensorLocationCallback, HeartRateMeasurementCallback \ No newline at end of file diff --git a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSService.kt b/feature_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSService.kt deleted file mode 100644 index cc07781d..00000000 --- a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSService.kt +++ /dev/null @@ -1,53 +0,0 @@ -package no.nordicsemi.android.hrs.service - -import android.bluetooth.BluetoothDevice -import dagger.hilt.android.AndroidEntryPoint -import no.nordicsemi.android.ble.BleManagerCallbacks -import no.nordicsemi.android.hrs.data.HRSData -import no.nordicsemi.android.service.ForegroundBleService -import no.nordicsemi.android.service.LoggableBleManager -import javax.inject.Inject - -@AndroidEntryPoint -internal class HRSService : ForegroundBleService(), HRSManagerCallbacks { - - private var data = HRSData() - private val points = mutableListOf() - - @Inject - lateinit var localBroadcast: HRSDataBroadcast - - override val manager: HRSManager by lazy { - HRSManager(this).apply { - setGattCallbacks(this@HRSService) - } - } - - override fun initializeManager(): LoggableBleManager { - return manager - } - - override fun onBatteryLevelChanged(device: BluetoothDevice, batteryLevel: Int) { - sendNewData(data.copy(batteryLevel = batteryLevel)) - } - - override fun onBodySensorLocationReceived(device: BluetoothDevice, sensorLocation: Int) { - sendNewData(data.copy(sensorLocation = sensorLocation)) - } - - override fun onHeartRateMeasurementReceived( - device: BluetoothDevice, - heartRate: Int, - contactDetected: Boolean?, - energyExpanded: Int?, - rrIntervals: MutableList? - ) { - points.add(heartRate) - sendNewData(data.copy(heartRates = points)) - } - - private fun sendNewData(newData: HRSData) { - data = newData - localBroadcast.offer(newData) - } -} diff --git a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/viewmodel/HRSViewModel.kt b/feature_hrs/src/main/java/no/nordicsemi/android/hrs/viewmodel/HRSViewModel.kt deleted file mode 100644 index 3437e7cd..00000000 --- a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/viewmodel/HRSViewModel.kt +++ /dev/null @@ -1,47 +0,0 @@ -package no.nordicsemi.android.hrs.viewmodel - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.withContext -import no.nordicsemi.android.hrs.data.HRSData -import no.nordicsemi.android.hrs.service.HRSDataBroadcast -import no.nordicsemi.android.hrs.view.DisconnectEvent -import no.nordicsemi.android.hrs.view.HRSScreenViewEvent -import javax.inject.Inject - -@HiltViewModel -internal class HRSViewModel @Inject constructor( - private val localBroadcast: HRSDataBroadcast -) : ViewModel() { - - val state = MutableStateFlow(HRSViewState()) - - init { - localBroadcast.events.onEach { - withContext(Dispatchers.Main) { consumeEvent(it) } - }.launchIn(viewModelScope) - } - - private fun consumeEvent(event: HRSData) { - state.value = state.value.copy( - points = event.heartRates, - batteryLevel = event.batteryLevel, - sensorLocation = event.sensorLocation - ) - } - - fun onEvent(event: HRSScreenViewEvent) { - (event as? DisconnectEvent)?.let { - onDisconnectButtonClick() - } - } - - private fun onDisconnectButtonClick() { - state.tryEmit(state.value.copy(isScreenActive = false)) - } -} diff --git a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/viewmodel/HRSViewState.kt b/feature_hrs/src/main/java/no/nordicsemi/android/hrs/viewmodel/HRSViewState.kt deleted file mode 100644 index 75ef9d94..00000000 --- a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/viewmodel/HRSViewState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package no.nordicsemi.android.hrs.viewmodel - -data class HRSViewState( - val points: List = listOf(1, 2, 3), - val batteryLevel: Int = 0, - val sensorLocation: Int = 0, - val isScreenActive: Boolean = true -) diff --git a/feature_hrs/src/main/res/drawable/fade_red.xml b/feature_hrs/src/main/res/drawable/fade_red.xml deleted file mode 100644 index 54ac10ba..00000000 --- a/feature_hrs/src/main/res/drawable/fade_red.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/feature_hts/src/main/java/no/nordicsemi/android/hts/data/HTSData.kt b/feature_hts/src/main/java/no/nordicsemi/android/hts/data/HTSData.kt deleted file mode 100644 index bd7a5c19..00000000 --- a/feature_hts/src/main/java/no/nordicsemi/android/hts/data/HTSData.kt +++ /dev/null @@ -1,22 +0,0 @@ -package no.nordicsemi.android.hts.data - -internal data class HTSData( - val heartRates: List = emptyList(), - val temperature: Temperature = Temperature.CELSIUS, - val batteryLevel: Int = 0, - val sensorLocation: Int = 0, - val isScreenActive: Boolean = true -) { - - fun displayTemperature() { - val value = when (temperature) { - Temperature.CELSIUS -> TODO() - Temperature.FAHRENHEIT -> TODO() - Temperature.KELVIN -> TODO() - } - } -} - -internal enum class Temperature { - CELSIUS, FAHRENHEIT, KELVIN -} diff --git a/feature_hts/src/main/java/no/nordicsemi/android/hts/service/HTSDataBroadcast.kt b/feature_hts/src/main/java/no/nordicsemi/android/hts/service/HTSDataBroadcast.kt deleted file mode 100644 index feaafcbd..00000000 --- a/feature_hts/src/main/java/no/nordicsemi/android/hts/service/HTSDataBroadcast.kt +++ /dev/null @@ -1,9 +0,0 @@ -package no.nordicsemi.android.hts.service - -import no.nordicsemi.android.hts.data.HTSData -import no.nordicsemi.android.service.BluetoothDataReadBroadcast -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -internal class HTSDataBroadcast @Inject constructor() : BluetoothDataReadBroadcast() diff --git a/feature_hts/src/main/java/no/nordicsemi/android/hts/service/HTSManagerCallbacks.kt b/feature_hts/src/main/java/no/nordicsemi/android/hts/service/HTSManagerCallbacks.kt deleted file mode 100644 index 821cf28c..00000000 --- a/feature_hts/src/main/java/no/nordicsemi/android/hts/service/HTSManagerCallbacks.kt +++ /dev/null @@ -1,31 +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.service - -import no.nordicsemi.android.ble.common.profile.ht.TemperatureMeasurementCallback -import no.nordicsemi.android.service.BatteryManagerCallbacks - -/** - * Interface [HTSManagerCallbacks] must be implemented by [HTActivity] in order - * to receive callbacks from [HTSManager]. - */ -interface HTSManagerCallbacks : BatteryManagerCallbacks, TemperatureMeasurementCallback diff --git a/feature_hts/src/main/java/no/nordicsemi/android/hts/service/HTSService.kt b/feature_hts/src/main/java/no/nordicsemi/android/hts/service/HTSService.kt deleted file mode 100644 index 8fd638a4..00000000 --- a/feature_hts/src/main/java/no/nordicsemi/android/hts/service/HTSService.kt +++ /dev/null @@ -1,49 +0,0 @@ -package no.nordicsemi.android.hts.service - -import android.bluetooth.BluetoothDevice -import dagger.hilt.android.AndroidEntryPoint -import no.nordicsemi.android.ble.BleManagerCallbacks -import no.nordicsemi.android.hts.data.HTSData -import no.nordicsemi.android.service.ForegroundBleService -import no.nordicsemi.android.service.LoggableBleManager -import java.util.* -import javax.inject.Inject - -@AndroidEntryPoint -internal class HTSService : ForegroundBleService(), HTSManagerCallbacks { - - private var data = HTSData() - private val points = mutableListOf() - - @Inject - lateinit var localBroadcast: HTSDataBroadcast - - override val manager: HTSManager by lazy { - HTSManager(this).apply { - setGattCallbacks(this@HTSService) - } - } - - override fun initializeManager(): LoggableBleManager { - return manager - } - - override fun onBatteryLevelChanged(device: BluetoothDevice, batteryLevel: Int) { - sendNewData(data.copy(batteryLevel = batteryLevel)) - } - - override fun onTemperatureMeasurementReceived( - device: BluetoothDevice, - temperature: Float, - unit: Int, - calendar: Calendar?, - type: Int? - ) { - TODO("Not yet implemented") - } - - private fun sendNewData(newData: HTSData) { - data = newData - localBroadcast.offer(newData) - } -} diff --git a/feature_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreenViewEvent.kt b/feature_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreenViewEvent.kt deleted file mode 100644 index 8c826645..00000000 --- a/feature_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreenViewEvent.kt +++ /dev/null @@ -1,5 +0,0 @@ -package no.nordicsemi.android.hts.view - -sealed class HTSScreenViewEvent - -object DisconnectEvent : HTSScreenViewEvent() diff --git a/feature_hts/src/main/java/no/nordicsemi/android/hts/viewmodel/HRSViewModel.kt b/feature_hts/src/main/java/no/nordicsemi/android/hts/viewmodel/HRSViewModel.kt deleted file mode 100644 index 918e938d..00000000 --- a/feature_hts/src/main/java/no/nordicsemi/android/hts/viewmodel/HRSViewModel.kt +++ /dev/null @@ -1,47 +0,0 @@ -package no.nordicsemi.android.hts.viewmodel - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.withContext -import no.nordicsemi.android.hts.data.HTSData -import no.nordicsemi.android.hts.service.HTSDataBroadcast -import no.nordicsemi.android.hts.view.DisconnectEvent -import no.nordicsemi.android.hts.view.HTSScreenViewEvent -import javax.inject.Inject - -@HiltViewModel -internal class HTSViewModel @Inject constructor( - private val localBroadcast: HTSDataBroadcast -) : ViewModel() { - - val state = MutableStateFlow(HTSData()) - - init { - localBroadcast.events.onEach { - withContext(Dispatchers.Main) { consumeEvent(it) } - }.launchIn(viewModelScope) - } - - private fun consumeEvent(event: HTSData) { - state.value = state.value.copy( - - batteryLevel = event.batteryLevel, - sensorLocation = event.sensorLocation - ) - } - - fun onEvent(event: HTSScreenViewEvent) { - (event as? DisconnectEvent)?.let { - onDisconnectButtonClick() - } - } - - private fun onDisconnectButtonClick() { - state.tryEmit(state.value.copy(isScreenActive = false)) - } -} 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 fb67f14e..e93cecd1 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 @@ -6,6 +6,7 @@ import android.bluetooth.BluetoothGattCharacteristic import android.content.Context import android.util.Log import androidx.annotation.IntRange +import no.nordicsemi.android.ble.BleManager import no.nordicsemi.android.ble.callback.DataReceivedCallback import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelDataCallback import no.nordicsemi.android.ble.data.Data @@ -18,17 +19,10 @@ import java.util.* * @param The profile callbacks type. * @see BleManager */ -abstract class BatteryManager(context: Context) : LoggableBleManager(context) { +abstract class BatteryManager(context: Context) : BleManager(context) { private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null - /** - * Returns the last received Battery Level value. - * The value is set to null when the device disconnects. - * @return Battery Level value, in percent. - */ - /** Last received Battery Level value. */ - var batteryLevel: Int? = null - private set + private val batteryLevelDataCallback: DataReceivedCallback = object : BatteryLevelDataCallback() { override fun onBatteryLevelChanged( @@ -36,8 +30,7 @@ abstract class BatteryManager(context: Context) : @IntRange(from = 0, to = 100) batteryLevel: Int ) { log(LogContract.Log.Level.APPLICATION, "Battery Level received: $batteryLevel%") - this@BatteryManager.batteryLevel = batteryLevel - mCallbacks?.onBatteryLevelChanged(device, batteryLevel) + onBatteryLevelChanged(batteryLevel) } override fun onInvalidDataReceived(device: BluetoothDevice, data: Data) { @@ -45,15 +38,14 @@ abstract class BatteryManager(context: Context) : } } + protected abstract fun onBatteryLevelChanged(batteryLevel: Int) + fun readBatteryLevelCharacteristic() { if (isConnected) { readCharacteristic(batteryLevelCharacteristic) .with(batteryLevelDataCallback) .fail { device: BluetoothDevice?, status: Int -> - log( - Log.WARN, - "Battery Level characteristic not found" - ) + log(Log.WARN, "Battery Level characteristic not found") } .enqueue() } @@ -66,32 +58,10 @@ abstract class BatteryManager(context: Context) : .with(batteryLevelDataCallback) enableNotifications(batteryLevelCharacteristic) .done { device: BluetoothDevice? -> - log( - Log.INFO, - "Battery Level notifications enabled" - ) + log(Log.INFO, "Battery Level notifications enabled") } .fail { device: BluetoothDevice?, status: Int -> - log( - Log.WARN, - "Battery Level characteristic not found" - ) - } - .enqueue() - } - } - - /** - * Disables Battery Level notifications on the Server. - */ - fun disableBatteryLevelCharacteristicNotifications() { - if (isConnected) { - disableNotifications(batteryLevelCharacteristic) - .done { device: BluetoothDevice? -> - log( - Log.INFO, - "Battery Level notifications disabled" - ) + log(Log.WARN, "Battery Level characteristic not found") } .enqueue() } @@ -106,16 +76,14 @@ abstract class BatteryManager(context: Context) : override fun isOptionalServiceSupported(gatt: BluetoothGatt): Boolean { val service = gatt.getService(BATTERY_SERVICE_UUID) if (service != null) { - batteryLevelCharacteristic = service.getCharacteristic( - BATTERY_LEVEL_CHARACTERISTIC_UUID - ) + batteryLevelCharacteristic = service.getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID) } return batteryLevelCharacteristic != null } override fun onDeviceDisconnected() { batteryLevelCharacteristic = null - batteryLevel = null + onBatteryLevelChanged(0) } } diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/BatteryManagerCallbacks.kt b/lib_service/src/main/java/no/nordicsemi/android/service/BatteryManagerCallbacks.kt deleted file mode 100644 index feb51fcd..00000000 --- a/lib_service/src/main/java/no/nordicsemi/android/service/BatteryManagerCallbacks.kt +++ /dev/null @@ -1,6 +0,0 @@ -package no.nordicsemi.android.service - -import no.nordicsemi.android.ble.BleManagerCallbacks -import no.nordicsemi.android.ble.common.profile.battery.BatteryLevelCallback - -interface BatteryManagerCallbacks : BleManagerCallbacks, BatteryLevelCallback 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 6f5440d8..a7aa15d5 100644 --- a/lib_service/src/main/java/no/nordicsemi/android/service/BleProfileService.kt +++ b/lib_service/src/main/java/no/nordicsemi/android/service/BleProfileService.kt @@ -21,33 +21,21 @@ */ package no.nordicsemi.android.service -import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice -import android.bluetooth.BluetoothGatt -import android.content.BroadcastReceiver -import android.content.Context import android.content.Intent -import android.content.IntentFilter -import android.net.Uri -import android.os.Binder import android.os.Handler -import android.os.IBinder -import android.util.Log import android.widget.Toast -import androidx.annotation.StringRes import androidx.lifecycle.LifecycleService -import androidx.localbroadcastmanager.content.LocalBroadcastManager import dagger.hilt.android.AndroidEntryPoint -import no.nordicsemi.android.ble.BleManagerCallbacks -import no.nordicsemi.android.ble.utils.ILogger +import no.nordicsemi.android.ble.BleManager import no.nordicsemi.android.log.ILogSession import no.nordicsemi.android.log.Logger import javax.inject.Inject @AndroidEntryPoint -abstract class BleProfileService : LifecycleService(), BleManagerCallbacks { +abstract class BleProfileService : LifecycleService() { - private var bleManager: LoggableBleManager? = null + protected abstract val manager: BleManager @Inject lateinit var bluetoothDeviceHolder: SelectedBluetoothDeviceHolder @@ -56,9 +44,8 @@ abstract class BleProfileService : LifecycleService(), BleManagerCallbacks { * Returns a handler that is created in onCreate(). * The handler may be used to postpone execution of some operations or to run them in UI thread. */ - protected var handler: Handler? = null - private set - protected var bound = false + private var handler: Handler? = null + private var activityIsChangingConfiguration = false /** @@ -66,256 +53,45 @@ abstract class BleProfileService : LifecycleService(), BleManagerCallbacks { * * @return bluetooth device */ - protected val bluetoothDevice: BluetoothDevice by lazy { + private val bluetoothDevice: BluetoothDevice by lazy { bluetoothDeviceHolder.device ?: throw IllegalArgumentException( - "No device address at EXTRA_DEVICE_ADDRESS key" + "No device associated with the application." ) } - /** - * Returns the device name - * - * @return the device name - */ - protected var deviceName: String? = null - private set - /** * Returns the log session that can be used to append log entries. The method returns `null` if the nRF Logger app was not installed. It is safe to use logger when * [.onServiceStarted] has been called. * * @return the log session */ - protected var logSession: ILogSession? = null + private var logSession: ILogSession? = null private set - private val bluetoothStateBroadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF) - val logger: ILogger = binder - val stateString = - "[Broadcast] Action received: " + BluetoothAdapter.ACTION_STATE_CHANGED + ", state changed to " + state2String( - state - ) - logger.log(Log.DEBUG, stateString) - when (state) { - BluetoothAdapter.STATE_ON -> onBluetoothEnabled() - BluetoothAdapter.STATE_TURNING_OFF, BluetoothAdapter.STATE_OFF -> onBluetoothDisabled() - } - } - - private fun state2String(state: Int): String { - return when (state) { - BluetoothAdapter.STATE_TURNING_ON -> "TURNING ON" - BluetoothAdapter.STATE_ON -> "ON" - BluetoothAdapter.STATE_TURNING_OFF -> "TURNING OFF" - BluetoothAdapter.STATE_OFF -> "OFF" - else -> "UNKNOWN ($state)" - } - } - } - - inner class LocalBinder : Binder(), ILogger { - /** - * Disconnects from the sensor. - */ - fun disconnect() { - val state = bleManager!!.connectionState - if (state == BluetoothGatt.STATE_DISCONNECTED || state == BluetoothGatt.STATE_DISCONNECTING) { - bleManager!!.close() - onDeviceDisconnected(bluetoothDevice!!) - return - } - bleManager!!.disconnect().enqueue() - } - - /** - * Sets whether the bound activity if changing configuration or not. - * If `false`, we will turn off battery level notifications in onUnbind(..) method below. - * - * @param changing true if the bound activity is finishing - */ - fun setActivityIsChangingConfiguration(changing: Boolean) { - activityIsChangingConfiguration = changing - } - - /** - * Returns the device address - * - * @return device address - */ - val deviceAddress: String - get() = bluetoothDevice!!.address - - /** - * Returns the device name - * - * @return the device name - */ - fun getDeviceName(): String? { - return deviceName - } - - /** - * Returns the Bluetooth device - * - * @return the Bluetooth device - */ - fun getBluetoothDevice(): BluetoothDevice? { - return bluetoothDevice - } - - /** - * Returns `true` if the device is connected to the sensor. - * - * @return `true` if device is connected to the sensor, `false` otherwise - */ - val isConnected: Boolean - get() = bleManager!!.isConnected - - /** - * Returns the connection state of given device. - * - * @return the connection state, as in [BleManager.getConnectionState]. - */ - val connectionState: Int - get() = bleManager!!.connectionState - - /** - * Returns the log session that can be used to append log entries. - * The log session is created when the service is being created. - * The method returns `null` if the nRF Logger app was not installed. - * - * @return the log session - */ - fun getLogSession(): ILogSession? { - return logSession - } - - override fun log(level: Int, message: String) { - Logger.log(logSession, level, message) - } - - override fun log(level: Int, @StringRes messageRes: Int, vararg params: Any) { - Logger.log(logSession, level, messageRes, *params) - } - }// default implementation returns the basic binder. You can overwrite the LocalBinder with your own, wider implementation - - /** - * Returns the binder implementation. This must return class implementing the additional manager interface that may be used in the bound activity. - * - * @return the service binder - */ - protected val binder: LocalBinder - protected get() =// default implementation returns the basic binder. You can overwrite the LocalBinder with your own, wider implementation - LocalBinder() - - override fun onBind(intent: Intent): IBinder? { - super.onBind(intent) - bound = true - return binder - } - - override fun onRebind(intent: Intent) { - bound = true - if (!activityIsChangingConfiguration) onRebind() - } - - /** - * Called when the activity has rebound to the service after being recreated. - * This method is not called when the activity was killed to be recreated when the phone orientation changed - * if prior to being killed called [LocalBinder.setActivityIsChangingConfiguration] with parameter true. - */ - protected open fun onRebind() { - // empty default implementation - } - - override fun onUnbind(intent: Intent): Boolean { - bound = false - if (!activityIsChangingConfiguration) onUnbind() - - // We want the onRebind method be called if anything else binds to it again - return true - } - - /** - * Called when the activity has unbound from the service before being finished. - * This method is not called when the activity is killed to be recreated when the phone orientation changed. - */ - protected open fun onUnbind() { - // empty default implementation - } override fun onCreate() { super.onCreate() handler = Handler() - - // Initialize the manager - bleManager = initializeManager() - - // Register broadcast receivers - registerReceiver( - bluetoothStateBroadcastReceiver, - IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED) - ) - - // Service has now been created - onServiceCreated() - - // Call onBluetoothEnabled if Bluetooth enabled - val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() - if (bluetoothAdapter.isEnabled) { - onBluetoothEnabled() - } } - /** - * Called when the service has been created, before the [.onBluetoothEnabled] is called. - */ - protected fun onServiceCreated() { - // empty default implementation - } - - /** - * Initializes the Ble Manager responsible for connecting to a single device. - * - * @return a new BleManager object - */ - protected abstract fun initializeManager(): LoggableBleManager - /** * This method returns whether autoConnect option should be used. * * @return true to use autoConnect feature, false (default) otherwise. */ - protected fun shouldAutoConnect(): Boolean { + private fun shouldAutoConnect(): Boolean { return false } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) - val logUri = intent?.getParcelableExtra(EXTRA_LOG_URI) - logSession = Logger.openSession(applicationContext, logUri) - deviceName = intent?.getStringExtra(EXTRA_DEVICE_NAME) - Logger.i(logSession, "Service started") - val adapter = BluetoothAdapter.getDefaultAdapter() - bleManager!!.setLogger(logSession) - onServiceStarted() - bleManager!!.connect(bluetoothDevice) + manager.connect(bluetoothDevice) .useAutoConnect(shouldAutoConnect()) .retry(3, 100) .enqueue() return START_REDELIVER_INTENT } - /** - * Called when the service has been started. The device name and address are set. - * The BLE Manager will try to connect to the device after this method finishes. - */ - protected fun onServiceStarted() { - // empty default implementation - } - override fun onTaskRemoved(rootIntent: Intent) { super.onTaskRemoved(rootIntent) // This method is called when user removed the app from Recents. @@ -326,58 +102,15 @@ abstract class BleProfileService : LifecycleService(), BleManagerCallbacks { override fun onDestroy() { super.onDestroy() - // Unregister broadcast receivers - unregisterReceiver(bluetoothStateBroadcastReceiver) // shutdown the manager - bleManager!!.close() + manager.close() Logger.i(logSession, "Service destroyed") - bleManager = null bluetoothDeviceHolder.forgetDevice() - deviceName = null logSession = null handler = null } - /** - * Method called when Bluetooth Adapter has been disabled. - */ - protected fun onBluetoothDisabled() { - // empty default implementation - } - - /** - * This method is called when Bluetooth Adapter has been enabled and - * after the service was created if Bluetooth Adapter was enabled at that moment. - * This method could initialize all Bluetooth related features, for example open the GATT server. - */ - protected fun onBluetoothEnabled() { - // empty default implementation - } - - override fun onDeviceConnecting(device: BluetoothDevice) { - val broadcast = Intent(BROADCAST_CONNECTION_STATE) - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice) - broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTING) - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) - } - - override fun onDeviceConnected(device: BluetoothDevice) { - val broadcast = Intent(BROADCAST_CONNECTION_STATE) - broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTED) - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice) - broadcast.putExtra(EXTRA_DEVICE_NAME, deviceName) - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) - } - - override fun onDeviceDisconnecting(device: BluetoothDevice) { - // Notify user about changing the state to DISCONNECTING - val broadcast = Intent(BROADCAST_CONNECTION_STATE) - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice) - broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTING) - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) - } - /** * This method should return false if the service needs to do some asynchronous work after if has disconnected from the device. * In that case the [.stopService] method must be called when done. @@ -388,102 +121,19 @@ abstract class BleProfileService : LifecycleService(), BleManagerCallbacks { return true } - override fun onDeviceDisconnected(device: BluetoothDevice) { - // Note 1: Do not use the device argument here unless you change calling onDeviceDisconnected from the binder above - - // Note 2: if BleManager#shouldAutoConnect() for this device returned true, this callback will be - // invoked ONLY when user requested disconnection (using Disconnect button). If the device - // disconnects due to a link loss, the onLinkLossOccurred(BluetoothDevice) method will be called instead. - val broadcast = Intent(BROADCAST_CONNECTION_STATE) - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice) - broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTED) - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) - if (stopWhenDisconnected()) stopService() - } - - protected fun stopService() { + private fun stopService() { // user requested disconnection. We must stop the service Logger.v(logSession, "Stopping service...") stopSelf() } - override fun onLinkLossOccurred(device: BluetoothDevice) { - val broadcast = Intent(BROADCAST_CONNECTION_STATE) - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice) - broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_LINK_LOSS) - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) - } - - override fun onServicesDiscovered(device: BluetoothDevice, optionalServicesFound: Boolean) { - val broadcast = Intent(BROADCAST_SERVICES_DISCOVERED) - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice) - broadcast.putExtra(EXTRA_SERVICE_PRIMARY, true) - broadcast.putExtra(EXTRA_SERVICE_SECONDARY, optionalServicesFound) - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) - } - - override fun onDeviceReady(device: BluetoothDevice) { - val broadcast = Intent(BROADCAST_DEVICE_READY) - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice) - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) - } - - override fun onDeviceNotSupported(device: BluetoothDevice) { - val broadcast = Intent(BROADCAST_SERVICES_DISCOVERED) - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice) - broadcast.putExtra(EXTRA_SERVICE_PRIMARY, false) - broadcast.putExtra(EXTRA_SERVICE_SECONDARY, false) - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) - - // no need for disconnecting, it will be disconnected by the manager automatically - } - - override fun onBatteryValueReceived(device: BluetoothDevice, value: Int) { - val broadcast = Intent(BROADCAST_BATTERY_LEVEL) - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice) - broadcast.putExtra(EXTRA_BATTERY_LEVEL, value) - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) - } - - override fun onBondingRequired(device: BluetoothDevice) { - showToast(R.string.csc_bonding) - val broadcast = Intent(BROADCAST_BOND_STATE) - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice) - broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING) - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) - } - - override fun onBonded(device: BluetoothDevice) { - showToast(R.string.csc_bonded) - val broadcast = Intent(BROADCAST_BOND_STATE) - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice) - broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED) - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) - } - - override fun onBondingFailed(device: BluetoothDevice) { - showToast(R.string.csc_bonding_failed) - val broadcast = Intent(BROADCAST_BOND_STATE) - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice) - broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE) - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) - } - - override fun onError(device: BluetoothDevice, message: String, errorCode: Int) { - val broadcast = Intent(BROADCAST_ERROR) - broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice) - broadcast.putExtra(EXTRA_ERROR_MESSAGE, message) - broadcast.putExtra(EXTRA_ERROR_CODE, errorCode) - LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast) - } - /** * Shows a message as a Toast notification. This method is thread safe, you can call it from any thread * * @param messageResId an resource id of the message to be shown */ protected fun showToast(messageResId: Int) { - handler!!.post { + handler?.post { Toast.makeText(this@BleProfileService, messageResId, Toast.LENGTH_SHORT).show() } } @@ -494,7 +144,7 @@ abstract class BleProfileService : LifecycleService(), BleManagerCallbacks { * @param message a message to be shown */ protected fun showToast(message: String?) { - handler!!.post { + handler?.post { Toast.makeText(this@BleProfileService, message, Toast.LENGTH_SHORT).show() } } @@ -505,7 +155,7 @@ abstract class BleProfileService : LifecycleService(), BleManagerCallbacks { * @return device address */ protected val deviceAddress: String - protected get() = bluetoothDevice!!.address + get() = bluetoothDevice.address /** * Returns `true` if the device is connected to the sensor. @@ -513,41 +163,5 @@ abstract class BleProfileService : LifecycleService(), BleManagerCallbacks { * @return `true` if device is connected to the sensor, `false` otherwise */ protected val isConnected: Boolean - protected get() = bleManager != null && bleManager!!.isConnected - - companion object { - private const val TAG = "BleProfileService" - const val BROADCAST_CONNECTION_STATE = - "no.nordicsemi.android.nrftoolbox.BROADCAST_CONNECTION_STATE" - const val BROADCAST_SERVICES_DISCOVERED = - "no.nordicsemi.android.nrftoolbox.BROADCAST_SERVICES_DISCOVERED" - const val BROADCAST_DEVICE_READY = "no.nordicsemi.android.nrftoolbox.DEVICE_READY" - const val BROADCAST_BOND_STATE = "no.nordicsemi.android.nrftoolbox.BROADCAST_BOND_STATE" - - @Deprecated("") - val BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL" - const val BROADCAST_ERROR = "no.nordicsemi.android.nrftoolbox.BROADCAST_ERROR" - - /** - * The key for the device name that is returned in [.BROADCAST_CONNECTION_STATE] with state [.STATE_CONNECTED]. - */ - const val EXTRA_DEVICE_NAME = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE_NAME" - const val EXTRA_DEVICE = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE" - const val EXTRA_LOG_URI = "no.nordicsemi.android.nrftoolbox.EXTRA_LOG_URI" - const val EXTRA_CONNECTION_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_CONNECTION_STATE" - const val EXTRA_BOND_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_BOND_STATE" - const val EXTRA_SERVICE_PRIMARY = "no.nordicsemi.android.nrftoolbox.EXTRA_SERVICE_PRIMARY" - const val EXTRA_SERVICE_SECONDARY = - "no.nordicsemi.android.nrftoolbox.EXTRA_SERVICE_SECONDARY" - - @Deprecated("") - val EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL" - const val EXTRA_ERROR_MESSAGE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_MESSAGE" - const val EXTRA_ERROR_CODE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_CODE" - const val STATE_LINK_LOSS = -1 - const val STATE_DISCONNECTED = 0 - const val STATE_CONNECTED = 1 - const val STATE_CONNECTING = 2 - const val STATE_DISCONNECTING = 3 - } -} \ No newline at end of file + get() = manager.isConnected +} diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/BluetoothDataReadBroadcast.kt b/lib_service/src/main/java/no/nordicsemi/android/service/BluetoothDataReadBroadcast.kt deleted file mode 100644 index 3ea5baef..00000000 --- a/lib_service/src/main/java/no/nordicsemi/android/service/BluetoothDataReadBroadcast.kt +++ /dev/null @@ -1,19 +0,0 @@ -package no.nordicsemi.android.service - -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow - -abstract class BluetoothDataReadBroadcast { - - private val _event = MutableSharedFlow( - replay = 1, - extraBufferCapacity = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - val events: SharedFlow = _event - - fun offer(newEvent: T) { - _event.tryEmit(newEvent) - } -} diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/ForegroundBleService.kt b/lib_service/src/main/java/no/nordicsemi/android/service/ForegroundBleService.kt index 5422ace3..6f4de89b 100644 --- a/lib_service/src/main/java/no/nordicsemi/android/service/ForegroundBleService.kt +++ b/lib_service/src/main/java/no/nordicsemi/android/service/ForegroundBleService.kt @@ -31,9 +31,7 @@ import androidx.core.app.NotificationCompat private const val CHANNEL_ID = "FOREGROUND_BLE_SERVICE" -abstract class ForegroundBleService> : BleProfileService() { - - protected abstract val manager: T +abstract class ForegroundBleService : BleProfileService() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val result = super.onStartCommand(intent, flags, startId) @@ -48,22 +46,6 @@ abstract class ForegroundBleService the callbacks class. - */ -abstract class LoggableBleManager(context: Context) : LegacyBleManager(context) { - private var logSession: ILogSession? = null - - /** - * Sets the log session to log into. - * - * @param session nRF Logger log session to log inti, or null, if nRF Logger is not installed. - */ - fun setLogger(session: ILogSession?) { - logSession = session - } - - override fun log(priority: Int, message: String) { - Logger.log(logSession, LogContract.Log.Level.fromPriority(priority), message) - Log.println(priority, "BleManager", message) - } -} \ No newline at end of file diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/SelectedBluetoothDeviceHolder.kt b/lib_service/src/main/java/no/nordicsemi/android/service/SelectedBluetoothDeviceHolder.kt index 645ae68e..ddefcae2 100644 --- a/lib_service/src/main/java/no/nordicsemi/android/service/SelectedBluetoothDeviceHolder.kt +++ b/lib_service/src/main/java/no/nordicsemi/android/service/SelectedBluetoothDeviceHolder.kt @@ -16,7 +16,7 @@ class SelectedBluetoothDeviceHolder constructor( return deviceManager.associations.firstOrNull()?.let { bluetoothAdapter?.getRemoteDevice(it) } } - fun isDeviceBonded(): Boolean { + fun isBondingRequired(): Boolean { return device?.bondState == BluetoothDevice.BOND_NONE } fun bondDevice() { diff --git a/lib_theme/src/main/AndroidManifest.xml b/lib_theme/src/main/AndroidManifest.xml index a251fc74..423e376a 100644 --- a/lib_theme/src/main/AndroidManifest.xml +++ b/lib_theme/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + \ No newline at end of file diff --git a/lib_theme/src/main/java/no/nordicsemi/android/theme/Color.kt b/lib_theme/src/main/java/no/nordicsemi/android/theme/Color.kt index 82bbd901..d1cb0537 100644 --- a/lib_theme/src/main/java/no/nordicsemi/android/theme/Color.kt +++ b/lib_theme/src/main/java/no/nordicsemi/android/theme/Color.kt @@ -28,16 +28,17 @@ object NordicColors { val TableViewBackground = NeutralColor(Color(0xFFF2F2F6)) val TableViewSeparator = NeutralColor(Color(0xFFD2D2D6)) - val Primary = ThemedColor(Color(0xFF00A9CE), Color(0xFF212121)) - val PrimaryVariant = ThemedColor(Color(0xFF008CD2), Color.Black) - val Secondary = ThemedColor(Color(0xFF00A9CE), Color(0xFF008CD2)) - val SecondaryVariant = ThemedColor(Color(0xFF008CD2), Color(0xFF008CD2)) + val Primary = ThemedColor(Color(0xFF00A9CE), Color(0xFF00A9CE)) + val PrimaryVariant = ThemedColor(Color(0xFF008CD2), Color(0xFF00A9CE)) + val Secondary = ThemedColor(Color(0xFF00A9CE), Color(0xFF00A9CE)) + val SecondaryVariant = ThemedColor(Color(0xFF008CD2), Color(0xFF00A9CE)) val OnPrimary = ThemedColor(Color.White, Color.White) val OnSecondary = ThemedColor(Color.White, Color.White) val OnBackground = ThemedColor(Color.Black, Color.White) val OnSurface = ThemedColor(Color.Black, Color.White) - val Background = ThemedColor(Color(0xFFDADADA), Color.Black) - val Surface = ThemedColor(Color(0xFFDADADA), Color.Black) + val ItemHighlight = ThemedColor(Color.White, Color(0xFF1E1E1E)) + val Background = ThemedColor(Color(0xFFF5F5F5), Color(0xFF121212)) + val Surface = ThemedColor(Color(0xFFF5F5F5), Color(0xFF121212)) } sealed class NordicColor { diff --git a/lib_theme/src/main/java/no/nordicsemi/android/theme/Theme.kt b/lib_theme/src/main/java/no/nordicsemi/android/theme/Theme.kt index f8fa424e..7de953b0 100644 --- a/lib_theme/src/main/java/no/nordicsemi/android/theme/Theme.kt +++ b/lib_theme/src/main/java/no/nordicsemi/android/theme/Theme.kt @@ -7,7 +7,7 @@ import androidx.compose.material.lightColors import androidx.compose.runtime.Composable @Composable -fun TestTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) { +fun TestTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { val darkColorPalette = darkColors( primary = NordicColors.Primary.value(), diff --git a/lib_theme/src/main/java/no/nordicsemi/android/theme/view/BatteryLevelView.kt b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/BatteryLevelView.kt index 0a1fbd52..7b2603ee 100644 --- a/lib_theme/src/main/java/no/nordicsemi/android/theme/view/BatteryLevelView.kt +++ b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/BatteryLevelView.kt @@ -1,28 +1,15 @@ package no.nordicsemi.android.theme.view -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Card import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import no.nordicsemi.android.theme.NordicColors import no.nordicsemi.android.theme.R @Composable fun BatteryLevelView(batteryLevel: Int) { - Card( - backgroundColor = NordicColors.NordicGray4.value(), - shape = RoundedCornerShape(10.dp), - elevation = 0.dp - ) { - Box(modifier = Modifier.padding(16.dp)) { - KeyValueField( - stringResource(id = R.string.field_battery), - "$batteryLevel%" - ) - } + ScreenSection { + KeyValueField( + stringResource(id = R.string.field_battery), + "$batteryLevel%" + ) } } \ No newline at end of file diff --git a/lib_theme/src/main/java/no/nordicsemi/android/theme/view/KeyValueField.kt b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/KeyValueField.kt index 08e6a58a..4083c1de 100644 --- a/lib_theme/src/main/java/no/nordicsemi/android/theme/view/KeyValueField.kt +++ b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/KeyValueField.kt @@ -9,7 +9,7 @@ import androidx.compose.ui.Modifier import no.nordicsemi.android.theme.NordicColors @Composable - fun KeyValueField(key: String, value: String) { +fun KeyValueField(key: String, value: String) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween diff --git a/lib_theme/src/main/java/no/nordicsemi/android/theme/view/SensorRecordCard.kt b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/SensorRecordCard.kt index c5c02880..88c5bcc4 100644 --- a/lib_theme/src/main/java/no/nordicsemi/android/theme/view/SensorRecordCard.kt +++ b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/SensorRecordCard.kt @@ -1,18 +1,23 @@ package no.nordicsemi.android.theme.view +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Card import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import no.nordicsemi.android.theme.NordicColors @Composable -fun SensorRecordCard(content: @Composable () -> Unit) { +fun ScreenSection(content: @Composable () -> Unit) { Card( - backgroundColor = NordicColors.NordicGray4.value(), - shape = RoundedCornerShape(10.dp), + backgroundColor = NordicColors.ItemHighlight.value(), + shape = RoundedCornerShape(4.dp), elevation = 0.dp ) { - content() + Box(modifier = Modifier.padding(16.dp)) { + content() + } } } diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/SpeedUnitRadioGroup.kt b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/SpeedUnitRadioGroup.kt similarity index 59% rename from feature_csc/src/main/java/no/nordicsemi/android/csc/view/SpeedUnitRadioGroup.kt rename to lib_theme/src/main/java/no/nordicsemi/android/theme/view/SpeedUnitRadioGroup.kt index 964fe107..c43d94e0 100644 --- a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/SpeedUnitRadioGroup.kt +++ b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/SpeedUnitRadioGroup.kt @@ -1,4 +1,4 @@ -package no.nordicsemi.android.csc.view +package no.nordicsemi.android.theme.view import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row @@ -12,10 +12,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @Composable -internal fun SpeedUnitRadioGroup( - currentItem: RadioGroupItem, - items: List, - onEvent: (RadioGroupItem) -> Unit +fun SpeedUnitRadioGroup( + currentItem: T, + items: List>, + onEvent: (RadioGroupItem) -> Unit ) { Row( modifier = Modifier.fillMaxWidth(), @@ -28,14 +28,14 @@ internal fun SpeedUnitRadioGroup( } @Composable -internal fun SpeedUnitRadioButton( - selectedItem: RadioGroupItem, - displayedItem: RadioGroupItem, - onEvent: (RadioGroupItem) -> Unit +internal fun SpeedUnitRadioButton( + selectedItem: T, + displayedItem: RadioGroupItem, + onEvent: (RadioGroupItem) -> Unit ) { Row { RadioButton( - selected = (selectedItem == displayedItem), + selected = (selectedItem == displayedItem.unit), onClick = { onEvent(displayedItem) } ) Spacer(modifier = Modifier.width(4.dp)) @@ -43,12 +43,4 @@ internal fun SpeedUnitRadioButton( } } -internal fun createSpeedUnitLabel(unit: SpeedUnit): String { - return when (unit) { - SpeedUnit.M_S -> "m/s" - SpeedUnit.KM_H -> "km/h" - SpeedUnit.MPH -> "mph" - } -} - -data class RadioGroupItem(val label: String) +data class RadioGroupItem(val unit: T, val label: String) diff --git a/lib_theme/src/main/java/no/nordicsemi/android/theme/view/TopAppBar.kt b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/TopAppBar.kt new file mode 100644 index 00000000..e9d32b85 --- /dev/null +++ b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/TopAppBar.kt @@ -0,0 +1,42 @@ +package no.nordicsemi.android.theme.view + +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Close +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import no.nordicsemi.android.theme.R + +@Composable +fun CloseIconAppBar(text: String, onClick: () -> Unit) { + TopAppBar( + title = { Text(text) }, + navigationIcon = { + IconButton(onClick = { onClick() }) { + Icon( + Icons.Default.Close, + contentDescription = stringResource(id = R.string.close_app), + ) + } + } + ) +} + +@Composable +fun BackIconAppBar(text: String, onClick: () -> Unit) { + TopAppBar( + title = { Text(text) }, + navigationIcon = { + IconButton(onClick = { onClick() }) { + Icon( + Icons.Default.ArrowBack, + contentDescription = stringResource(id = R.string.back_screen), + ) + } + } + ) +} diff --git a/lib_theme/src/main/java/no/nordicsemi/android/theme/viewmodel/CloseableViewModel.kt b/lib_theme/src/main/java/no/nordicsemi/android/theme/viewmodel/CloseableViewModel.kt new file mode 100644 index 00000000..2b8af830 --- /dev/null +++ b/lib_theme/src/main/java/no/nordicsemi/android/theme/viewmodel/CloseableViewModel.kt @@ -0,0 +1,13 @@ +package no.nordicsemi.android.theme.viewmodel + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow + +abstract class CloseableViewModel : ViewModel() { + + var isActive = MutableStateFlow(true) + + protected fun finish() { + isActive.tryEmit(false) + } +} diff --git a/lib_theme/src/main/res/values/colors.xml b/lib_theme/src/main/res/values/colors.xml index cdc236f2..c3b04f40 100644 --- a/lib_theme/src/main/res/values/colors.xml +++ b/lib_theme/src/main/res/values/colors.xml @@ -6,6 +6,6 @@ #FF0077c8 #FF004c97 #FFFFFFFF - #FFDADADA + #FFF5F5F5 #FF0090B0 diff --git a/lib_theme/src/main/res/values/strings.xml b/lib_theme/src/main/res/values/strings.xml index 522d4b3d..93e85944 100644 --- a/lib_theme/src/main/res/values/strings.xml +++ b/lib_theme/src/main/res/values/strings.xml @@ -2,6 +2,9 @@ nRF Toolbox - Disconnect + Close the application. + Close the current screen. + + DISCONNECT Battery \ No newline at end of file diff --git a/lib_utils/src/main/AndroidManifest.xml b/lib_utils/src/main/AndroidManifest.xml index 82b96384..26b65fb5 100644 --- a/lib_utils/src/main/AndroidManifest.xml +++ b/lib_utils/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + \ No newline at end of file diff --git a/feature_csc/build.gradle b/profile_csc/build.gradle similarity index 100% rename from feature_csc/build.gradle rename to profile_csc/build.gradle diff --git a/feature_csc/src/androidTest/java/no/nordicsemi/android/csc/ExampleInstrumentedTest.kt b/profile_csc/src/androidTest/java/no/nordicsemi/android/csc/ExampleInstrumentedTest.kt similarity index 100% rename from feature_csc/src/androidTest/java/no/nordicsemi/android/csc/ExampleInstrumentedTest.kt rename to profile_csc/src/androidTest/java/no/nordicsemi/android/csc/ExampleInstrumentedTest.kt diff --git a/feature_csc/src/main/AndroidManifest.xml b/profile_csc/src/main/AndroidManifest.xml similarity index 100% rename from feature_csc/src/main/AndroidManifest.xml rename to profile_csc/src/main/AndroidManifest.xml diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/data/CSCData.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCData.kt similarity index 74% rename from feature_csc/src/main/java/no/nordicsemi/android/csc/data/CSCData.kt rename to profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCData.kt index ccc3727e..3aeb1bd4 100644 --- a/feature_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,7 +1,9 @@ package no.nordicsemi.android.csc.data +import androidx.compose.runtime.Composable import no.nordicsemi.android.csc.view.CSCSettings import no.nordicsemi.android.csc.view.SpeedUnit +import no.nordicsemi.android.theme.view.RadioGroupItem import java.util.* internal data class CSCData( @@ -9,15 +11,21 @@ internal data class CSCData( val scanDevices: Boolean = false, val selectedSpeedUnit: SpeedUnit = SpeedUnit.M_S, val speed: Float = 0f, - val cadence: Int = 0, + val cadence: Float = 0f, val distance: Float = 0f, val totalDistance: Float = 0f, val gearRatio: Float = 0f, val batteryLevel: Int = 0, - val wheelSize: String = CSCSettings.DefaultWheelSize.NAME, - val isScreenActive: Boolean = true + val wheelSize: Int = CSCSettings.DefaultWheelSize.VALUE, + val wheelSizeDisplay: String = CSCSettings.DefaultWheelSize.NAME ) { + @Composable + fun drawItself() { + + } + + private val speedWithUnit = when (selectedSpeedUnit) { SpeedUnit.M_S -> speed SpeedUnit.KM_H -> speed * 3.6f @@ -33,7 +41,7 @@ internal data class CSCData( } fun displayCadence(): String { - return String.format("%d RPM", cadence) + return String.format("%.0f RPM", cadence) } fun displayDistance(): String { @@ -56,7 +64,11 @@ internal data class CSCData( return String.format(Locale.US, "%.1f", gearRatio) } - fun items(): List<> { - + fun items(): List> { + return listOf( + RadioGroupItem(SpeedUnit.M_S,"m/s"), + RadioGroupItem(SpeedUnit.KM_H, "km/h"), + RadioGroupItem(SpeedUnit.MPH, "mph") + ) } } diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCDataHolder.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCDataHolder.kt new file mode 100644 index 00000000..810c113f --- /dev/null +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCDataHolder.kt @@ -0,0 +1,46 @@ +package no.nordicsemi.android.csc.data + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import no.nordicsemi.android.csc.view.SpeedUnit +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class CSCDataHolder @Inject constructor() { + + private val _data = MutableStateFlow(CSCData()) + val data: StateFlow = _data + + fun setWheelSize(wheelSize: Int, wheelSizeDisplay: String) { + _data.tryEmit(_data.value.copy( + wheelSize = wheelSize, + wheelSizeDisplay = wheelSizeDisplay, + showDialog = false + )) + } + + fun setSpeedUnit(selectedSpeedUnit: SpeedUnit) { + _data.tryEmit(_data.value.copy(selectedSpeedUnit = selectedSpeedUnit)) + } + + 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 setBatteryLevel(batteryLevel: Int) { + _data.tryEmit(_data.value.copy(batteryLevel = batteryLevel)) + } + + fun clear() { + _data.tryEmit(CSCData()) + } +} diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/service/CSCManager.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/service/CSCManager.kt similarity index 92% rename from feature_csc/src/main/java/no/nordicsemi/android/csc/service/CSCManager.kt rename to profile_csc/src/main/java/no/nordicsemi/android/csc/service/CSCManager.kt index 2e90c1cf..a9e17354 100644 --- a/feature_csc/src/main/java/no/nordicsemi/android/csc/service/CSCManager.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/service/CSCManager.kt @@ -29,6 +29,7 @@ import android.util.Log 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.CSCDataHolder import no.nordicsemi.android.csc.service.CSCMeasurementParser.parse import no.nordicsemi.android.csc.view.CSCSettings import no.nordicsemi.android.log.LogContract @@ -41,11 +42,15 @@ private val CYCLING_SPEED_AND_CADENCE_SERVICE_UUID = UUID.fromString("00001816-0 /** 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) : BatteryManager(context) { +internal class CSCManager(context: Context, private val dataHolder: CSCDataHolder) : BatteryManager(context) { private var cscMeasurementCharacteristic: BluetoothGattCharacteristic? = null private var wheelSize = CSCSettings.DefaultWheelSize.VALUE + override fun onBatteryLevelChanged(batteryLevel: Int) { + dataHolder.setBatteryLevel(batteryLevel) + } + override fun getGattCallback(): BatteryManagerGattCallback { return CSCManagerGattCallback() } @@ -82,7 +87,7 @@ internal class CSCManager(context: Context) : BatteryManager Unit) { @@ -25,9 +25,10 @@ internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) { } Column( - modifier = Modifier.padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { + Spacer(modifier = Modifier.height(16.dp)) + SettingsSection(state, onEvent) Spacer(modifier = Modifier.height(16.dp)) @@ -47,16 +48,17 @@ internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) { @Composable private fun SettingsSection(state: CSCData, onEvent: (CSCViewEvent) -> Unit) { - SensorRecordCard { + ScreenSection { Column( - modifier = Modifier.padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { WheelSizeView(state, onEvent) Spacer(modifier = Modifier.height(16.dp)) - SpeedUnitRadioGroup(state.selectedSpeedUnit) { onEvent(it) } + SpeedUnitRadioGroup(state.selectedSpeedUnit, state.items()) { + onEvent(OnSelectedSpeedUnitSelected(it.unit)) + } } } } diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/CscScreen.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCScreen.kt similarity index 74% rename from feature_csc/src/main/java/no/nordicsemi/android/csc/view/CscScreen.kt rename to profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCScreen.kt index 558d50ac..b0255f1e 100644 --- a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/CscScreen.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCScreen.kt @@ -2,8 +2,6 @@ package no.nordicsemi.android.csc.view import android.content.Intent import androidx.compose.foundation.layout.Column -import androidx.compose.material.Text -import androidx.compose.material.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -11,19 +9,21 @@ 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.service.CSCService import no.nordicsemi.android.csc.data.CSCData -import no.nordicsemi.android.csc.viewmodel.CscViewModel +import no.nordicsemi.android.csc.service.CSCService +import no.nordicsemi.android.csc.viewmodel.CSCViewModel +import no.nordicsemi.android.theme.view.BackIconAppBar import no.nordicsemi.android.utils.isServiceRunning @Composable -fun CscScreen(finishAction: () -> Unit) { - val viewModel: CscViewModel = hiltViewModel() +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(state.isScreenActive) { - if (!state.isScreenActive) { + LaunchedEffect(isScreenActive) { + if (!isScreenActive) { finishAction() } if (context.isServiceRunning(CSCService::class.java.name)) { @@ -45,7 +45,9 @@ fun CscScreen(finishAction: () -> Unit) { @Composable private fun CSCView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) { Column { - TopAppBar(title = { Text(text = stringResource(id = R.string.csc_title)) }) + BackIconAppBar(stringResource(id = R.string.csc_title)) { + onEvent(OnDisconnectButtonClick) + } CSCContentView(state) { onEvent(it) } } diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/CSCSettings.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCSettings.kt similarity index 82% rename from feature_csc/src/main/java/no/nordicsemi/android/csc/view/CSCSettings.kt rename to profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCSettings.kt index e38cdc12..08ace118 100644 --- a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/CSCSettings.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCSettings.kt @@ -1,6 +1,6 @@ package no.nordicsemi.android.csc.view -object CSCSettings { +internal object CSCSettings { object DefaultWheelSize { const val NAME = "60-622" diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/CSCViewEvent.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCViewEvent.kt similarity index 100% rename from feature_csc/src/main/java/no/nordicsemi/android/csc/view/CSCViewEvent.kt rename to profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCViewEvent.kt diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/SelectWheelSizeDialog.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/SelectWheelSizeDialog.kt similarity index 100% rename from feature_csc/src/main/java/no/nordicsemi/android/csc/view/SelectWheelSizeDialog.kt rename to profile_csc/src/main/java/no/nordicsemi/android/csc/view/SelectWheelSizeDialog.kt diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/SensorsReadingView.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/SensorsReadingView.kt similarity index 89% rename from feature_csc/src/main/java/no/nordicsemi/android/csc/view/SensorsReadingView.kt rename to profile_csc/src/main/java/no/nordicsemi/android/csc/view/SensorsReadingView.kt index f37a793a..a61d02ee 100644 --- a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/SensorsReadingView.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/SensorsReadingView.kt @@ -3,7 +3,6 @@ package no.nordicsemi.android.csc.view import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -13,12 +12,12 @@ import no.nordicsemi.android.csc.R import no.nordicsemi.android.csc.data.CSCData import no.nordicsemi.android.theme.view.BatteryLevelView import no.nordicsemi.android.theme.view.KeyValueField -import no.nordicsemi.android.theme.view.SensorRecordCard +import no.nordicsemi.android.theme.view.ScreenSection @Composable internal fun SensorsReadingView(state: CSCData) { - SensorRecordCard { - Column(modifier = Modifier.padding(16.dp)) { + ScreenSection { + Column { KeyValueField(stringResource(id = R.string.scs_field_speed), state.displaySpeed()) Spacer(modifier = Modifier.height(4.dp)) KeyValueField(stringResource(id = R.string.scs_field_cadence), state.displayCadence()) diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/SpeedUnit.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/SpeedUnit.kt similarity index 100% rename from feature_csc/src/main/java/no/nordicsemi/android/csc/view/SpeedUnit.kt rename to profile_csc/src/main/java/no/nordicsemi/android/csc/view/SpeedUnit.kt diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/WheelSizeView.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/WheelSizeView.kt similarity index 96% rename from feature_csc/src/main/java/no/nordicsemi/android/csc/view/WheelSizeView.kt rename to profile_csc/src/main/java/no/nordicsemi/android/csc/view/WheelSizeView.kt index caf7ec7c..5314513b 100644 --- a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/WheelSizeView.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/WheelSizeView.kt @@ -18,7 +18,7 @@ import no.nordicsemi.android.csc.data.CSCData internal fun WheelSizeView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) { OutlinedTextField( modifier = Modifier.fillMaxWidth(), - value = state.wheelSize, + value = state.wheelSizeDisplay, onValueChange = { }, enabled = false, label = { Text(text = stringResource(id = R.string.scs_field_wheel_size)) }, diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/viewmodel/CscViewModel.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/viewmodel/CSCViewModel.kt similarity index 50% rename from feature_csc/src/main/java/no/nordicsemi/android/csc/viewmodel/CscViewModel.kt rename to profile_csc/src/main/java/no/nordicsemi/android/csc/viewmodel/CSCViewModel.kt index e6efa2a5..bb9278e7 100644 --- a/feature_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,35 +1,22 @@ package no.nordicsemi.android.csc.viewmodel -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.withContext -import no.nordicsemi.android.csc.data.CSCData -import no.nordicsemi.android.csc.service.CSCDataReadBroadcast +import no.nordicsemi.android.csc.data.CSCDataHolder import no.nordicsemi.android.csc.view.CSCViewEvent 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.theme.viewmodel.CloseableViewModel import no.nordicsemi.android.utils.exhaustive import javax.inject.Inject @HiltViewModel -internal class CscViewModel @Inject constructor( - private val localBroadcast: CSCDataReadBroadcast -) : ViewModel() { +internal class CSCViewModel @Inject constructor( + private val dataHolder: CSCDataHolder +) : CloseableViewModel() { - val state = MutableStateFlow(CSCData()) - - init { - localBroadcast.events.onEach { - withContext(Dispatchers.Main) { state.value = it } - }.launchIn(viewModelScope) - } + val state = dataHolder.data fun onEvent(event: CSCViewEvent) { when (event) { @@ -41,22 +28,19 @@ internal class CscViewModel @Inject constructor( } private fun onSelectedSpeedUnit(event: OnSelectedSpeedUnitSelected) { - state.tryEmit(state.value.copy(selectedSpeedUnit = event.selectedSpeedUnit)) + dataHolder.setSpeedUnit(event.selectedSpeedUnit) } private fun onShowDialogEvent() { - state.tryEmit(state.value.copy(showDialog = true)) + dataHolder.setDisplayWheelSizeDialog() } private fun onWheelSizeChanged(event: OnWheelSizeSelected) { - localBroadcast.setWheelSize(event.wheelSize) - state.tryEmit(state.value.copy( - showDialog = false, - wheelSize = event.wheelSizeDisplayInfo - )) + dataHolder.setWheelSize(event.wheelSize, event.wheelSizeDisplayInfo) } private fun onDisconnectButtonClick() { - state.tryEmit(state.value.copy(isScreenActive = false)) + finish() + dataHolder.clear() } } diff --git a/feature_csc/src/main/res/values/strings.xml b/profile_csc/src/main/res/values/strings.xml similarity index 100% rename from feature_csc/src/main/res/values/strings.xml rename to profile_csc/src/main/res/values/strings.xml diff --git a/feature_csc/src/test/java/no/nordicsemi/android/csc/ExampleUnitTest.kt b/profile_csc/src/test/java/no/nordicsemi/android/csc/ExampleUnitTest.kt similarity index 100% rename from feature_csc/src/test/java/no/nordicsemi/android/csc/ExampleUnitTest.kt rename to profile_csc/src/test/java/no/nordicsemi/android/csc/ExampleUnitTest.kt diff --git a/feature_gls/build.gradle b/profile_gls/build.gradle similarity index 100% rename from feature_gls/build.gradle rename to profile_gls/build.gradle diff --git a/feature_gls/src/androidTest/java/no/nordicsemi/android/gls/ExampleInstrumentedTest.kt b/profile_gls/src/androidTest/java/no/nordicsemi/android/gls/ExampleInstrumentedTest.kt similarity index 100% rename from feature_gls/src/androidTest/java/no/nordicsemi/android/gls/ExampleInstrumentedTest.kt rename to profile_gls/src/androidTest/java/no/nordicsemi/android/gls/ExampleInstrumentedTest.kt diff --git a/profile_gls/src/main/AndroidManifest.xml b/profile_gls/src/main/AndroidManifest.xml new file mode 100644 index 00000000..eb9eb3d1 --- /dev/null +++ b/profile_gls/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSData.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSData.kt new file mode 100644 index 00000000..14dbbbc6 --- /dev/null +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSData.kt @@ -0,0 +1,27 @@ +package no.nordicsemi.android.gls.data + +import no.nordicsemi.android.theme.view.RadioGroupItem + +internal data class GLSData( + val records: List = emptyList(), + val batteryLevel: Int = 0, + val requestStatus: RequestStatus = RequestStatus.IDLE, + val isDeviceBonded: Boolean = false, + val selectedMode: WorkingMode = WorkingMode.ALL +) { + fun modeItems(): List> { + return listOf( + RadioGroupItem(WorkingMode.ALL, "All"), + RadioGroupItem(WorkingMode.FIRST, "First"), + RadioGroupItem(WorkingMode.LAST, "Last") + ) + } +} + +internal enum class WorkingMode { + ALL, LAST, FIRST +} + +internal enum class RequestStatus { + IDLE, PENDING, SUCCESS, ABORTED, FAILED, NOT_SUPPORTED +} diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSDataHolder.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSDataHolder.kt new file mode 100644 index 00000000..a0c489c0 --- /dev/null +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSDataHolder.kt @@ -0,0 +1,49 @@ +package no.nordicsemi.android.gls.data + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class GLSDataHolder @Inject constructor() { + + private val _data = MutableStateFlow(GLSData()) + val data: StateFlow = _data + + fun addNewRecord(record: GLSRecord) { + val newRecords = _data.value.records.toMutableList().apply { + add(record) + } + _data.tryEmit(_data.value.copy(records = newRecords)) + } + + fun addNewContext(context: MeasurementContext) { + _data.value.records.find { context.sequenceNumber == it.sequenceNumber }?.let { + it.context = context + } + _data.tryEmit(_data.value) + } + + fun setRequestStatus(requestStatus: RequestStatus) { + _data.tryEmit(_data.value.copy(requestStatus = requestStatus)) + } + + fun records() = _data.value.records + + fun clearRecords() { + _data.tryEmit(_data.value.copy(records = emptyList())) + } + + fun setNewWorkingMode(workingMode: WorkingMode) { + _data.tryEmit(_data.value.copy(selectedMode = workingMode)) + } + + fun setNewBatteryLevel(batteryLevel: Int) { + _data.tryEmit(_data.value.copy(batteryLevel = batteryLevel)) + } + + fun clear() { + _data.tryEmit(GLSData()) + } +} diff --git a/feature_gls/src/main/java/no/nordicsemi/android/gls/data/GLSRecord.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSRecord.kt similarity index 97% rename from feature_gls/src/main/java/no/nordicsemi/android/gls/data/GLSRecord.kt rename to profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSRecord.kt index 452d511e..c4ed474e 100644 --- a/feature_gls/src/main/java/no/nordicsemi/android/gls/data/GLSRecord.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSRecord.kt @@ -33,7 +33,7 @@ internal data class GLSRecord( /** The glucose concentration. 0 if not present */ val glucoseConcentration: Float = 0f, - /** Concentration unit. One of the following: [GLSRecord.UNIT_kgpl], [GLSRecord.UNIT_molpl] */ + /** Concentration unit. One of the following: [ConcentrationUnit.UNIT_KGPL], [ConcentrationUnit.UNIT_MOLPL] */ val unit: ConcentrationUnit = ConcentrationUnit.UNIT_KGPL, /** The type of the record. 0 if not present */ @@ -49,6 +49,8 @@ internal data class GLSRecord( ) internal data class MeasurementContext( + /** Record sequence number */ + val sequenceNumber: Int = 0, val carbohydrateId: CarbohydrateId = CarbohydrateId.NOT_PRESENT, diff --git a/feature_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSManager.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSManager.kt similarity index 86% rename from feature_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSManager.kt rename to profile_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSManager.kt index 2eb43ad7..2adb1c1e 100644 --- a/feature_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSManager.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSManager.kt @@ -28,7 +28,6 @@ import android.bluetooth.BluetoothGattCharacteristic import android.content.Context import android.util.Log import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.flow.MutableStateFlow import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointDataCallback import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementContextDataCallback import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementDataCallback @@ -44,7 +43,7 @@ import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContex import no.nordicsemi.android.ble.data.Data import no.nordicsemi.android.gls.data.CarbohydrateId import no.nordicsemi.android.gls.data.ConcentrationUnit -import no.nordicsemi.android.gls.data.GLSData +import no.nordicsemi.android.gls.data.GLSDataHolder import no.nordicsemi.android.gls.data.GLSRecord import no.nordicsemi.android.gls.data.HealthStatus import no.nordicsemi.android.gls.data.MeasurementContext @@ -55,7 +54,6 @@ 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 no.nordicsemi.android.service.BatteryManagerCallbacks import java.util.* import javax.inject.Inject import javax.inject.Singleton @@ -78,16 +76,18 @@ private val RACP_CHARACTERISTIC = UUID.fromString("00002A52-0000-1000-8000-00805 @Singleton internal class GLSManager @Inject constructor( - @ApplicationContext context: Context -) : BatteryManager(context) { - - val data = MutableStateFlow(GLSData()) - private val records = hashMapOf() + @ApplicationContext context: Context, + private val dataHolder: GLSDataHolder +) : BatteryManager(context) { private var glucoseMeasurementCharacteristic: BluetoothGattCharacteristic? = null private var glucoseMeasurementContextCharacteristic: BluetoothGattCharacteristic? = null private var recordAccessControlPointCharacteristic: BluetoothGattCharacteristic? = null + override fun onBatteryLevelChanged(batteryLevel: Int) { + dataHolder.setNewBatteryLevel(batteryLevel) + } + override fun getGattCallback(): BatteryManagerGattCallback { return GlucoseManagerGattCallback() } @@ -121,10 +121,14 @@ internal class GLSManager @Inject constructor( .with(object : GlucoseMeasurementDataCallback() { override fun onGlucoseMeasurementReceived( - device: BluetoothDevice, sequenceNumber: Int, - time: Calendar, glucoseConcentration: Float?, - unit: Int?, type: Int?, - sampleLocation: Int?, status: GlucoseStatus?, + device: BluetoothDevice, + sequenceNumber: Int, + time: Calendar, + glucoseConcentration: Float?, + unit: Int?, + type: Int?, + sampleLocation: Int?, + status: GlucoseStatus?, contextInformationFollows: Boolean ) { val record = GLSRecord( @@ -138,27 +142,29 @@ internal class GLSManager @Inject constructor( status = status?.value ?: 0 ) - records[record.sequenceNumber] = record - if (!contextInformationFollows) { - data.tryEmit(data.value.copy(record = records.values.toList())) - } + dataHolder.addNewRecord(record) } }) setNotificationCallback(glucoseMeasurementContextCharacteristic) .with(object : GlucoseMeasurementContextDataCallback() { override fun onGlucoseMeasurementContextReceived( - device: BluetoothDevice, sequenceNumber: Int, - carbohydrate: Carbohydrate?, carbohydrateAmount: Float?, - meal: Meal?, tester: Tester?, - health: Health?, exerciseDuration: Int?, - exerciseIntensity: Int?, medication: Medication?, - medicationAmount: Float?, medicationUnit: Int?, + device: BluetoothDevice, + sequenceNumber: Int, + carbohydrate: Carbohydrate?, + carbohydrateAmount: Float?, + meal: Meal?, + tester: Tester?, + health: Health?, + exerciseDuration: Int?, + exerciseIntensity: Int?, + medication: Medication?, + medicationAmount: Float?, + medicationUnit: Int?, HbA1c: Float? ) { - val record = records[sequenceNumber] ?: return - val context = MeasurementContext( + sequenceNumber = sequenceNumber, carbohydrateId = carbohydrate?.value?.let { CarbohydrateId.create(it) } ?: CarbohydrateId.NOT_PRESENT, carbohydrateUnits = carbohydrateAmount ?: 0f, @@ -177,9 +183,8 @@ internal class GLSManager @Inject constructor( ?: MedicationUnit.UNIT_KG, HbA1c = HbA1c ?: 0f ) - record.context = context - data.tryEmit(data.value) + dataHolder.addNewContext(context) } }) setIndicationCallback(recordAccessControlPointCharacteristic) @@ -194,14 +199,14 @@ internal class GLSManager @Inject constructor( RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED else -> RequestStatus.SUCCESS } - data.tryEmit(data.value.copy(requestStatus = status)) + dataHolder.setRequestStatus(status) } override fun onRecordAccessOperationCompletedWithNoRecordsFound( device: BluetoothDevice, @RACPOpCode requestCode: Int ) { - data.tryEmit(data.value.copy(requestStatus = RequestStatus.SUCCESS)) + dataHolder.setRequestStatus(RequestStatus.SUCCESS) } override fun onNumberOfRecordsReceived( @@ -211,8 +216,8 @@ internal class GLSManager @Inject constructor( //TODO("Probably not needed") // mCallbacks!!.onNumberOfRecordsRequested(device, numberOfRecords) if (numberOfRecords > 0) { - if (records.size > 0) { - val sequenceNumber = records.keys.last() + 1 + if (dataHolder.records().isNotEmpty()) { + val sequenceNumber = dataHolder.records().last().sequenceNumber + 1 //TODO check if correct writeCharacteristic( recordAccessControlPointCharacteristic, RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo( @@ -228,7 +233,7 @@ internal class GLSManager @Inject constructor( .enqueue() } } else { - data.tryEmit(data.value.copy(requestStatus = RequestStatus.SUCCESS)) + dataHolder.setRequestStatus(RequestStatus.SUCCESS) } } @@ -239,9 +244,9 @@ internal class GLSManager @Inject constructor( ) { log(Log.WARN, "Record Access operation failed (error $errorCode)") if (errorCode == RACP_ERROR_OP_CODE_NOT_SUPPORTED) { - data.tryEmit(data.value.copy(requestStatus = RequestStatus.NOT_SUPPORTED)) + dataHolder.setRequestStatus(RequestStatus.NOT_SUPPORTED) } else { - data.tryEmit(data.value.copy(requestStatus = RequestStatus.FAILED)) + dataHolder.setRequestStatus(RequestStatus.FAILED) } } }) @@ -271,9 +276,7 @@ internal class GLSManager @Inject constructor( return glucoseMeasurementCharacteristic != null && recordAccessControlPointCharacteristic != null } - override fun onServicesInvalidated() { - TODO("Not yet implemented") - } + override fun onServicesInvalidated() { } override fun isOptionalServiceSupported(gatt: BluetoothGatt): Boolean { super.isOptionalServiceSupported(gatt) @@ -290,11 +293,11 @@ internal class GLSManager @Inject constructor( /** * Clears the records list locally. */ - fun clear() { - records.clear() + private fun clear() { + dataHolder.clearRecords() val target = bluetoothDevice if (target != null) { - data.tryEmit(data.value.copy(requestStatus = RequestStatus.SUCCESS)) + dataHolder.setRequestStatus(RequestStatus.SUCCESS) } } @@ -303,11 +306,11 @@ internal class GLSManager @Inject constructor( * be returned to Glucose Measurement characteristic as a notification followed by Record Access * Control Point indication with status code Success or other in case of error. */ - fun lastRecord(): Unit { + fun requestLastRecord() { if (recordAccessControlPointCharacteristic == null) return val target = bluetoothDevice ?: return clear() - data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING)) + dataHolder.setRequestStatus(RequestStatus.PENDING) writeCharacteristic( recordAccessControlPointCharacteristic, RecordAccessControlPointData.reportLastStoredRecord() @@ -326,11 +329,11 @@ internal class GLSManager @Inject constructor( * returned to Glucose Measurement characteristic as a notification followed by Record Access * Control Point indication with status code Success or other in case of error. */ - fun requestFirstRecord(): Unit { + fun requestFirstRecord() { if (recordAccessControlPointCharacteristic == null) return val target = bluetoothDevice ?: return clear() - data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING)) + dataHolder.setRequestStatus(RequestStatus.PENDING) writeCharacteristic( recordAccessControlPointCharacteristic, RecordAccessControlPointData.reportFirstStoredRecord() @@ -350,11 +353,11 @@ internal class GLSManager @Inject constructor( * will be returned to Glucose Measurement characteristic as a notification followed by * Record Access Control Point indication with status code Success or other in case of error. */ - fun requestAllRecords(): Unit { + fun requestAllRecords() { if (recordAccessControlPointCharacteristic == null) return val target = bluetoothDevice ?: return clear() - data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING)) + dataHolder.setRequestStatus(RequestStatus.PENDING) writeCharacteristic( recordAccessControlPointCharacteristic, RecordAccessControlPointData.reportNumberOfAllStoredRecords() @@ -382,13 +385,13 @@ internal class GLSManager @Inject constructor( fun refreshRecords() { if (recordAccessControlPointCharacteristic == null) return val target = bluetoothDevice ?: return - if (records.size == 0) { + if (dataHolder.records().isEmpty()) { requestAllRecords() } else { - data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING)) + dataHolder.setRequestStatus(RequestStatus.PENDING) // obtain the last sequence number - val sequenceNumber = records.keys.last() + 1 + val sequenceNumber = dataHolder.records().last().sequenceNumber + 1 //TODO check if correct writeCharacteristic( recordAccessControlPointCharacteristic, RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber) @@ -432,7 +435,7 @@ internal class GLSManager @Inject constructor( if (recordAccessControlPointCharacteristic == null) return val target = bluetoothDevice ?: return clear() - data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING)) + dataHolder.setRequestStatus(RequestStatus.PENDING) writeCharacteristic( recordAccessControlPointCharacteristic, RecordAccessControlPointData.deleteAllStoredRecords() @@ -445,7 +448,7 @@ internal class GLSManager @Inject constructor( } .enqueue() - val elements = listOf(1, 2, 3) + val elements = listOf(1, 2, 3) val result = elements.all { it > 3 } } } diff --git a/feature_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSRecordAccessControlPointParser.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSRecordAccessControlPointParser.kt similarity index 99% rename from feature_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSRecordAccessControlPointParser.kt rename to profile_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSRecordAccessControlPointParser.kt index 41ac8c0c..ca6f393f 100644 --- a/feature_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSRecordAccessControlPointParser.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/repository/GLSRecordAccessControlPointParser.kt @@ -23,7 +23,7 @@ package no.nordicsemi.android.gls.repository import no.nordicsemi.android.ble.data.Data -object GLSRecordAccessControlPointParser { +object GLSRecordAccessControlPointParser { private const val OP_CODE_REPORT_STORED_RECORDS = 1 private const val OP_CODE_DELETE_STORED_RECORDS = 2 diff --git a/feature_gls/src/main/java/no/nordicsemi/android/gls/view/GLSContentView.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSContentView.kt similarity index 59% rename from feature_gls/src/main/java/no/nordicsemi/android/gls/view/GLSContentView.kt rename to profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSContentView.kt index 16646538..41e9863f 100644 --- a/feature_gls/src/main/java/no/nordicsemi/android/gls/view/GLSContentView.kt +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSContentView.kt @@ -3,8 +3,8 @@ package no.nordicsemi.android.gls.view import androidx.compose.foundation.layout.Column 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.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.MaterialTheme @@ -18,18 +18,27 @@ 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.OnWorkingModeSelected import no.nordicsemi.android.theme.view.BatteryLevelView +import no.nordicsemi.android.theme.view.ScreenSection +import no.nordicsemi.android.theme.view.SpeedUnitRadioGroup @Composable internal fun GLSContentView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) { Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), + modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally ) { Spacer(modifier = Modifier.height(16.dp)) + SettingsView(state, onEvent) + + Spacer(modifier = Modifier.height(16.dp)) + + RecordsView(state) + + Spacer(modifier = Modifier.height(16.dp)) + BatteryLevelView(state.batteryLevel) Spacer(modifier = Modifier.height(16.dp)) @@ -43,3 +52,22 @@ internal fun GLSContentView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Uni } } +@Composable +private fun SettingsView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) { + ScreenSection { + SpeedUnitRadioGroup(state.selectedMode, state.modeItems()) { + onEvent(OnWorkingModeSelected(it.unit)) + } + } +} + +@Composable +private fun RecordsView(state: GLSData) { + ScreenSection { + Column(modifier = Modifier.fillMaxWidth()) { + state.records.forEach { + Text(text = String.format("Glocose concentration: ", it.glucoseConcentration)) + } + } + } +} diff --git a/feature_gls/src/main/java/no/nordicsemi/android/gls/view/GLSScreen.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSScreen.kt similarity index 64% rename from feature_gls/src/main/java/no/nordicsemi/android/gls/view/GLSScreen.kt rename to profile_gls/src/main/java/no/nordicsemi/android/gls/view/GLSScreen.kt index 17f4a35b..c047ddb3 100644 --- a/feature_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,8 +1,6 @@ package no.nordicsemi.android.gls.view import androidx.compose.foundation.layout.Column -import androidx.compose.material.Text -import androidx.compose.material.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -10,23 +8,38 @@ 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 @Composable fun GLSScreen(finishAction: () -> Unit) { val viewModel: GLSViewModel = hiltViewModel() val state = viewModel.state.collectAsState().value + val isScreenActive = viewModel.isActive.collectAsState().value LaunchedEffect(state.isDeviceBonded) { -// viewModel.bondDevice() + viewModel.bondDevice() + } + + LaunchedEffect(isScreenActive) { + if (!isScreenActive) { + finishAction() + } + } + + GLSView(state) { + viewModel.onEvent(it) } } @Composable private fun GLSView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) { Column { - TopAppBar(title = { Text(text = stringResource(id = R.string.gls_title)) }) + BackIconAppBar(stringResource(id = R.string.gls_title)) { + onEvent(DisconnectEvent) + } GLSContentView(state, onEvent) } diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/viewmodel/GLSScreenViewEvent.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/viewmodel/GLSScreenViewEvent.kt new file mode 100644 index 00000000..68449ace --- /dev/null +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/viewmodel/GLSScreenViewEvent.kt @@ -0,0 +1,9 @@ +package no.nordicsemi.android.gls.viewmodel + +import no.nordicsemi.android.gls.data.WorkingMode + +internal sealed class GLSScreenViewEvent + +internal data class OnWorkingModeSelected(val workingMode: WorkingMode) : GLSScreenViewEvent() + +internal object DisconnectEvent : GLSScreenViewEvent() 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 new file mode 100644 index 00000000..e075f08c --- /dev/null +++ b/profile_gls/src/main/java/no/nordicsemi/android/gls/viewmodel/GLSViewModel.kt @@ -0,0 +1,68 @@ +package no.nordicsemi.android.gls.viewmodel + +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import no.nordicsemi.android.gls.data.GLSDataHolder +import no.nordicsemi.android.gls.data.WorkingMode +import no.nordicsemi.android.gls.repository.GLSManager +import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder +import no.nordicsemi.android.theme.viewmodel.CloseableViewModel +import no.nordicsemi.android.utils.exhaustive +import javax.inject.Inject + +@HiltViewModel +internal class GLSViewModel @Inject constructor( + private val glsManager: GLSManager, + private val deviceHolder: SelectedBluetoothDeviceHolder, + private val dataHolder: GLSDataHolder +) : CloseableViewModel() { + + val state = dataHolder.data + private var lastSelectedMode = state.value.selectedMode + + init { + dataHolder.data.onEach { + if (lastSelectedMode == it.selectedMode) { + return@onEach + } + lastSelectedMode = it.selectedMode + when (it.selectedMode) { + WorkingMode.ALL -> glsManager.requestAllRecords() + WorkingMode.LAST -> glsManager.requestLastRecord() + WorkingMode.FIRST -> glsManager.requestFirstRecord() + }.exhaustive + }.launchIn(GlobalScope) + } + + fun onEvent(event: GLSScreenViewEvent) { + when (event) { + DisconnectEvent -> disconnect() + is OnWorkingModeSelected -> dataHolder.setNewWorkingMode(event.workingMode) + }.exhaustive + } + + fun bondDevice() { + if (deviceHolder.isBondingRequired()) { + deviceHolder.bondDevice() + } else { + connectDevice() + } + } + + private fun connectDevice() { + deviceHolder.device?.let { + glsManager.connect(it) + .useAutoConnect(false) + .retry(3, 100) + .enqueue() + } + } + + private fun disconnect() { + finish() + deviceHolder.forgetDevice() + dataHolder.clear() + } +} diff --git a/feature_gls/src/main/res/values/strings.xml b/profile_gls/src/main/res/values/strings.xml similarity index 100% rename from feature_gls/src/main/res/values/strings.xml rename to profile_gls/src/main/res/values/strings.xml diff --git a/feature_gls/src/test/java/no/nordicsemi/android/gls/ExampleUnitTest.kt b/profile_gls/src/test/java/no/nordicsemi/android/gls/ExampleUnitTest.kt similarity index 100% rename from feature_gls/src/test/java/no/nordicsemi/android/gls/ExampleUnitTest.kt rename to profile_gls/src/test/java/no/nordicsemi/android/gls/ExampleUnitTest.kt diff --git a/feature_hrs/build.gradle b/profile_hrs/build.gradle similarity index 100% rename from feature_hrs/build.gradle rename to profile_hrs/build.gradle diff --git a/feature_hrs/src/androidTest/java/no/nordicsemi/android/hrs/ExampleInstrumentedTest.kt b/profile_hrs/src/androidTest/java/no/nordicsemi/android/hrs/ExampleInstrumentedTest.kt similarity index 100% rename from feature_hrs/src/androidTest/java/no/nordicsemi/android/hrs/ExampleInstrumentedTest.kt rename to profile_hrs/src/androidTest/java/no/nordicsemi/android/hrs/ExampleInstrumentedTest.kt diff --git a/feature_hrs/src/main/AndroidManifest.xml b/profile_hrs/src/main/AndroidManifest.xml similarity index 100% rename from feature_hrs/src/main/AndroidManifest.xml rename to profile_hrs/src/main/AndroidManifest.xml diff --git a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSData.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSData.kt similarity index 81% rename from feature_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSData.kt rename to profile_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSData.kt index 0335f907..5ca9c7b2 100644 --- a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSData.kt +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSData.kt @@ -3,5 +3,5 @@ package no.nordicsemi.android.hrs.data internal data class HRSData( val heartRates: List = emptyList(), val batteryLevel: Int = 0, - val sensorLocation: Int = 0 + val sensorLocation: Int = 0, ) diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSDataHolder.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSDataHolder.kt new file mode 100644 index 00000000..24dcb982 --- /dev/null +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSDataHolder.kt @@ -0,0 +1,32 @@ +package no.nordicsemi.android.hrs.data + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class HRSDataHolder @Inject constructor() { + + private val _data = MutableStateFlow(HRSData()) + val data: StateFlow = _data + + fun addNewHeartRate(heartRate: Int) { + val result = _data.value.heartRates.toMutableList().apply { + add(heartRate) + } + _data.tryEmit(_data.value.copy(heartRates = result)) + } + + fun setSensorLocation(sensorLocation: Int) { + _data.tryEmit(_data.value.copy(sensorLocation = sensorLocation)) + } + + fun setBatteryLevel(batteryLevel: Int) { + _data.tryEmit(_data.value.copy(batteryLevel = batteryLevel)) + } + + fun clear() { + _data.tryEmit(HRSData()) + } +} diff --git a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/service/BodySensorLocationParser.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/BodySensorLocationParser.kt similarity index 95% rename from feature_hrs/src/main/java/no/nordicsemi/android/hrs/service/BodySensorLocationParser.kt rename to profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/BodySensorLocationParser.kt index 10a68c76..5b65517c 100644 --- a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/service/BodySensorLocationParser.kt +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/BodySensorLocationParser.kt @@ -25,8 +25,7 @@ import no.nordicsemi.android.ble.data.Data internal object BodySensorLocationParser { fun parse(data: Data): String { - val value = data.getIntValue(Data.FORMAT_UINT8, 0)!! - return when (value) { + return when (data.getIntValue(Data.FORMAT_UINT8, 0)!!) { 6 -> "Foot" 5 -> "Ear Lobe" 4 -> "Hand" diff --git a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSManager.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSManager.kt similarity index 63% rename from feature_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSManager.kt rename to profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSManager.kt index 76e74287..590ac0d5 100644 --- a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSManager.kt +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSManager.kt @@ -31,21 +31,63 @@ import no.nordicsemi.android.ble.common.callback.hr.BodySensorLocationDataCallba 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.HRSDataHolder import no.nordicsemi.android.log.LogContract import no.nordicsemi.android.service.BatteryManager import java.util.* +private val HR_SERVICE_UUID = UUID.fromString("0000180D-0000-1000-8000-00805f9b34fb") +private val BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID = UUID.fromString("00002A38-0000-1000-8000-00805f9b34fb") +private val HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A37-0000-1000-8000-00805f9b34fb") + /** * HRSManager class performs BluetoothGatt operations for connection, service discovery, * enabling notification and reading characteristics. * All operations required to connect to device with BLE Heart Rate Service and reading * heart rate values are performed here. */ -class HRSManager(context: Context) : BatteryManager(context) { +internal class HRSManager(context: Context, private val dataHolder: HRSDataHolder) : BatteryManager(context) { private var heartRateCharacteristic: BluetoothGattCharacteristic? = null private var bodySensorLocationCharacteristic: BluetoothGattCharacteristic? = null + 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 + ) { + dataHolder.setSensorLocation(sensorLocation) + } + } + + 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, + contactDetected: Boolean?, + @IntRange(from = 0) energyExpanded: Int?, + rrIntervals: List? + ) { + dataHolder.addNewHeartRate(heartRate) + } + } + + override fun onBatteryLevelChanged(batteryLevel: Int) { + dataHolder.setBatteryLevel(batteryLevel) + } + override fun getGattCallback(): BatteryManagerGattCallback { return HeartRateManagerCallback() } @@ -58,56 +100,14 @@ class HRSManager(context: Context) : BatteryManager(context override fun initialize() { super.initialize() readCharacteristic(bodySensorLocationCharacteristic) - .with(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 - ) { - mCallbacks?.onBodySensorLocationReceived(device, sensorLocation) - } - - }) + .with(bodySensorLocationDataCallback) .fail { device: BluetoothDevice?, status: Int -> log(Log.WARN, "Body Sensor Location characteristic not found") } .enqueue() setNotificationCallback(heartRateCharacteristic) - .with(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, - contactDetected: Boolean?, - @IntRange(from = 0) energyExpanded: Int?, - rrIntervals: List? - ) { - mCallbacks?.onHeartRateMeasurementReceived( - device, - heartRate, - contactDetected, - energyExpanded, - rrIntervals - ) - } - }) + .with(heartRateMeasurementDataCallback) enableNotifications(heartRateCharacteristic).enqueue() } @@ -140,23 +140,4 @@ class HRSManager(context: Context) : BatteryManager(context override fun onServicesInvalidated() {} } - - companion object { - - val HR_SERVICE_UUID = UUID.fromString("0000180D-0000-1000-8000-00805f9b34fb") - private val BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID = UUID.fromString("00002A38-0000-1000-8000-00805f9b34fb") - private val HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A37-0000-1000-8000-00805f9b34fb") - private var managerInstance: HRSManager? = null - - /** - * Singleton implementation of HRSManager class. - */ - @Synchronized - fun getInstance(context: Context): HRSManager? { - if (managerInstance == null) { - managerInstance = HRSManager(context) - } - return managerInstance - } - } -} \ No newline at end of file +} 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 new file mode 100644 index 00000000..11189213 --- /dev/null +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSService.kt @@ -0,0 +1,15 @@ +package no.nordicsemi.android.hrs.service + +import dagger.hilt.android.AndroidEntryPoint +import no.nordicsemi.android.hrs.data.HRSDataHolder +import no.nordicsemi.android.service.ForegroundBleService +import javax.inject.Inject + +@AndroidEntryPoint +internal class HRSService : ForegroundBleService() { + + @Inject + lateinit var dataHolder: HRSDataHolder + + override val manager: HRSManager by lazy { HRSManager(this, dataHolder) } +} diff --git a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/service/HeartRateMeasurementParser.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HeartRateMeasurementParser.kt similarity index 100% rename from feature_hrs/src/main/java/no/nordicsemi/android/hrs/service/HeartRateMeasurementParser.kt rename to profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HeartRateMeasurementParser.kt diff --git a/feature_hts/src/main/java/no/nordicsemi/android/hts/view/HTSContentView.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSContentView.kt similarity index 75% rename from feature_hts/src/main/java/no/nordicsemi/android/hts/view/HTSContentView.kt rename to profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSContentView.kt index 74e9befc..72cf830f 100644 --- a/feature_hts/src/main/java/no/nordicsemi/android/hts/view/HTSContentView.kt +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSContentView.kt @@ -1,9 +1,8 @@ -package no.nordicsemi.android.hts.view +package no.nordicsemi.android.hrs.view import androidx.compose.foundation.layout.Box 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.material.Button @@ -16,22 +15,21 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import no.nordicsemi.android.hts.R -import no.nordicsemi.android.hts.data.HTSData +import no.nordicsemi.android.hrs.R +import no.nordicsemi.android.hrs.data.HRSData import no.nordicsemi.android.theme.view.BatteryLevelView -import no.nordicsemi.android.theme.view.SensorRecordCard +import no.nordicsemi.android.theme.view.ScreenSection @Composable -internal fun HTSContentView(state: HTSData, onEvent: (HTSScreenViewEvent) -> Unit) { +internal fun HRSContentView(state: HRSData, onEvent: (HRSScreenViewEvent) -> Unit) { Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - SensorRecordCard { - Box(modifier = Modifier.padding(16.dp)) { + Spacer(modifier = Modifier.height(16.dp)) + ScreenSection { + Box(modifier = Modifier.padding(16.dp)) { + LineChartView(state) } } @@ -53,5 +51,5 @@ internal fun HTSContentView(state: HTSData, onEvent: (HTSScreenViewEvent) -> Uni @Preview @Composable private fun Preview() { - HTSContentView(state = HTSData()) { } + HRSContentView(state = HRSData()) { } } diff --git a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreen.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreen.kt similarity index 77% rename from feature_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreen.kt rename to profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreen.kt index 6cfea08e..241f21f3 100644 --- a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreen.kt +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreen.kt @@ -2,8 +2,6 @@ package no.nordicsemi.android.hrs.view import android.content.Intent import androidx.compose.foundation.layout.Column -import androidx.compose.material.Text -import androidx.compose.material.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -11,19 +9,21 @@ 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.hrs.viewmodel.HRSViewState +import no.nordicsemi.android.theme.view.BackIconAppBar import no.nordicsemi.android.utils.isServiceRunning @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(state.isScreenActive) { - if (!state.isScreenActive) { + LaunchedEffect(isActive) { + if (!isActive) { finishAction() } if (context.isServiceRunning(HRSService::class.java.name)) { @@ -43,9 +43,11 @@ fun HRSScreen(finishAction: () -> Unit) { } @Composable -private fun HRSView(state: HRSViewState, onEvent: (HRSScreenViewEvent) -> Unit) { +private fun HRSView(state: HRSData, onEvent: (HRSScreenViewEvent) -> Unit) { Column { - TopAppBar(title = { Text(text = stringResource(id = R.string.hrs_title)) }) + BackIconAppBar(stringResource(id = R.string.hrs_title)) { + onEvent(DisconnectEvent) + } HRSContentView(state) { onEvent(it) } } diff --git a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreenViewEvent.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreenViewEvent.kt similarity index 100% rename from feature_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreenViewEvent.kt rename to profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreenViewEvent.kt diff --git a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSContentView.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/LineChartView.kt similarity index 50% rename from feature_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSContentView.kt rename to profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/LineChartView.kt index 1ca046aa..4fa231eb 100644 --- a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSContentView.kt +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/LineChartView.kt @@ -3,101 +3,72 @@ package no.nordicsemi.android.hrs.view import android.content.Context import android.graphics.Color import android.graphics.DashPathEffect -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Card -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.content.ContextCompat import com.github.mikephil.charting.charts.LineChart +import com.github.mikephil.charting.components.XAxis import com.github.mikephil.charting.data.Entry import com.github.mikephil.charting.data.LineData import com.github.mikephil.charting.data.LineDataSet -import com.github.mikephil.charting.formatter.IFillFormatter import com.github.mikephil.charting.interfaces.datasets.ILineDataSet -import com.github.mikephil.charting.utils.Utils -import no.nordicsemi.android.hrs.R -import no.nordicsemi.android.hrs.viewmodel.HRSViewState -import no.nordicsemi.android.theme.NordicColors -import no.nordicsemi.android.theme.view.BatteryLevelView +import no.nordicsemi.android.hrs.data.HRSData import java.util.* -@Composable -internal fun HRSContentView(state: HRSViewState, onEvent: (HRSScreenViewEvent) -> Unit) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Card( - backgroundColor = NordicColors.NordicGray4.value(), - shape = RoundedCornerShape(10.dp), - elevation = 0.dp - ) { - Box(modifier = Modifier.padding(16.dp)) { - LineChartView(state) - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - BatteryLevelView(state.batteryLevel) - - Spacer(modifier = Modifier.height(16.dp)) - - Button( - colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.secondary), - onClick = { onEvent(DisconnectEvent) } - ) { - Text(text = stringResource(id = R.string.disconnect)) - } - } -} +private const val X_AXIS_ELEMENTS_COUNT = 40f @Composable -internal fun LineChartView(state: HRSViewState) { +internal fun LineChartView(state: HRSData) { + val items = state.heartRates.takeLast(X_AXIS_ELEMENTS_COUNT.toInt()).reversed() + val isSystemInDarkTheme = isSystemInDarkTheme() AndroidView( modifier = Modifier .fillMaxWidth() .height(300.dp), - factory = { createLineChartView(it, state) }, - update = { updateData(state.points, it) } + factory = { createLineChartView(isSystemInDarkTheme, it, items) }, + update = { updateData(items, it) } ) } -internal fun createLineChartView(context: Context, state: HRSViewState): LineChart { +internal fun createLineChartView(isDarkTheme: Boolean, context: Context, points: List): LineChart { return LineChart(context).apply { - setBackgroundColor(Color.WHITE) - description.isEnabled = false - setTouchEnabled(true) + legend.isEnabled = false + + setTouchEnabled(false) -// setOnChartValueSelectedListener(this) setDrawGridBackground(false) isDragEnabled = true - setScaleEnabled(true) - setPinchZoom(true) + setScaleEnabled(false) + setPinchZoom(false) + + if (isDarkTheme) { + setBackgroundColor(Color.TRANSPARENT) + xAxis.gridColor = Color.WHITE + xAxis.textColor = Color.WHITE + axisLeft.gridColor = Color.WHITE + axisLeft.textColor = Color.WHITE + } else { + setBackgroundColor(Color.WHITE) + xAxis.gridColor = Color.BLACK + xAxis.textColor = Color.BLACK + axisLeft.gridColor = Color.BLACK + axisLeft.textColor = Color.BLACK + } xAxis.apply { xAxis.enableGridDashedLine(10f, 10f, 0f) + + axisMinimum = -X_AXIS_ELEMENTS_COUNT + axisMaximum = 0f + setAvoidFirstLastClipping(true) + position = XAxis.XAxisPosition.BOTTOM } axisLeft.apply { enableGridDashedLine(10f, 10f, 0f) @@ -107,11 +78,9 @@ internal fun createLineChartView(context: Context, state: HRSViewState): LineCha } axisRight.isEnabled = false - //--- - - val entries = state.points.mapIndexed { i, v -> - Entry(i.toFloat(), v.toFloat()) - } + val entries = points.mapIndexed { i, v -> + Entry(-i.toFloat(), v.toFloat()) + }.reversed() // create a dataset and give it a type if (data != null && data.dataSetCount > 0) { @@ -124,6 +93,7 @@ internal fun createLineChartView(context: Context, state: HRSViewState): LineCha val set1 = LineDataSet(entries, "DataSet 1") set1.setDrawIcons(false) + set1.setDrawValues(false) // draw dashed line @@ -133,8 +103,13 @@ internal fun createLineChartView(context: Context, state: HRSViewState): LineCha // black lines and points // black lines and points - set1.color = Color.BLACK - set1.setCircleColor(Color.BLACK) + if (isDarkTheme) { + set1.color = Color.WHITE + set1.setCircleColor(Color.WHITE) + } else { + set1.color = Color.BLACK + set1.setCircleColor(Color.BLACK) + } // line thickness and point size @@ -164,31 +139,9 @@ internal fun createLineChartView(context: Context, state: HRSViewState): LineCha // draw selection line as dashed set1.enableDashedHighlightLine(10f, 5f, 0f) - // set the filled area - - // set the filled area - set1.setDrawFilled(true) - set1.fillFormatter = IFillFormatter { _, _ -> - axisLeft.axisMinimum - } - - // set color of filled area - - // set color of filled area - if (Utils.getSDKInt() >= 18) { - // drawables only supported on api level 18 and above - val drawable = ContextCompat.getDrawable(context, R.drawable.fade_red) - set1.fillDrawable = drawable - } else { - set1.fillColor = Color.BLACK - } - val dataSets = ArrayList() dataSets.add(set1) // add the data sets - - // create a data object with the data sets - // create a data object with the data sets val data = LineData(dataSets) @@ -202,8 +155,8 @@ internal fun createLineChartView(context: Context, state: HRSViewState): LineCha private fun updateData(points: List, chart: LineChart) { val entries = points.mapIndexed { i, v -> - Entry(i.toFloat(), v.toFloat()) - } + Entry(-i.toFloat(), v.toFloat()) + }.reversed() with(chart) { if (data != null && data.dataSetCount > 0) { @@ -216,9 +169,3 @@ private fun updateData(points: List, chart: LineChart) { } } } - -@Preview -@Composable -private fun Preview() { - HRSContentView(state = 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 new file mode 100644 index 00000000..7251d4eb --- /dev/null +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/viewmodel/HRSViewModel.kt @@ -0,0 +1,27 @@ +package no.nordicsemi.android.hrs.viewmodel + +import dagger.hilt.android.lifecycle.HiltViewModel +import no.nordicsemi.android.hrs.data.HRSDataHolder +import no.nordicsemi.android.hrs.view.DisconnectEvent +import no.nordicsemi.android.hrs.view.HRSScreenViewEvent +import no.nordicsemi.android.theme.viewmodel.CloseableViewModel +import javax.inject.Inject + +@HiltViewModel +internal class HRSViewModel @Inject constructor( + private val dataHolder: HRSDataHolder +) : CloseableViewModel() { + + val state = dataHolder.data + + fun onEvent(event: HRSScreenViewEvent) { + (event as? DisconnectEvent)?.let { + onDisconnectButtonClick() + } + } + + private fun onDisconnectButtonClick() { + finish() + dataHolder.clear() + } +} diff --git a/feature_hrs/src/main/res/values/strings.xml b/profile_hrs/src/main/res/values/strings.xml similarity index 100% rename from feature_hrs/src/main/res/values/strings.xml rename to profile_hrs/src/main/res/values/strings.xml diff --git a/feature_hrs/src/test/java/no/nordicsemi/android/hrs/ExampleUnitTest.kt b/profile_hrs/src/test/java/no/nordicsemi/android/hrs/ExampleUnitTest.kt similarity index 100% rename from feature_hrs/src/test/java/no/nordicsemi/android/hrs/ExampleUnitTest.kt rename to profile_hrs/src/test/java/no/nordicsemi/android/hrs/ExampleUnitTest.kt diff --git a/feature_hts/build.gradle b/profile_hts/build.gradle similarity index 100% rename from feature_hts/build.gradle rename to profile_hts/build.gradle diff --git a/feature_hts/src/androidTest/java/no/nordicsemi/android/hts/ExampleInstrumentedTest.kt b/profile_hts/src/androidTest/java/no/nordicsemi/android/hts/ExampleInstrumentedTest.kt similarity index 100% rename from feature_hts/src/androidTest/java/no/nordicsemi/android/hts/ExampleInstrumentedTest.kt rename to profile_hts/src/androidTest/java/no/nordicsemi/android/hts/ExampleInstrumentedTest.kt diff --git a/feature_hts/src/main/AndroidManifest.xml b/profile_hts/src/main/AndroidManifest.xml similarity index 100% rename from feature_hts/src/main/AndroidManifest.xml rename to profile_hts/src/main/AndroidManifest.xml diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSData.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSData.kt new file mode 100644 index 00000000..0b0473f8 --- /dev/null +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSData.kt @@ -0,0 +1,30 @@ +package no.nordicsemi.android.hts.data + +import no.nordicsemi.android.theme.view.RadioGroupItem + +internal data class HTSData( + val temperatureValue: Float = 0f, + val temperatureUnit: TemperatureUnit = TemperatureUnit.CELSIUS, + val batteryLevel: Int = 0, +) { + + fun displayTemperature(): String { + return when (temperatureUnit) { + TemperatureUnit.CELSIUS -> String.format("%.1f °C", temperatureValue) + TemperatureUnit.FAHRENHEIT -> String.format("%.1f °F", temperatureValue * 1.8f + 32f) + TemperatureUnit.KELVIN -> String.format("%.1f °K", temperatureValue + 273.15f) + } + } + + fun temperatureSettingsItems(): List> { + return listOf( + RadioGroupItem(TemperatureUnit.CELSIUS,"°C"), + RadioGroupItem(TemperatureUnit.FAHRENHEIT, "°F"), + RadioGroupItem(TemperatureUnit.KELVIN, "°K") + ) + } +} + +internal enum class TemperatureUnit { + CELSIUS, FAHRENHEIT, KELVIN +} diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSDataHolder.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSDataHolder.kt new file mode 100644 index 00000000..630a46a7 --- /dev/null +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSDataHolder.kt @@ -0,0 +1,29 @@ +package no.nordicsemi.android.hts.data + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class HTSDataHolder @Inject constructor() { + + private val _data = MutableStateFlow(HTSData()) + val data: StateFlow = _data + + fun setNewTemperature(temperature: Float) { + _data.tryEmit(_data.value.copy(temperatureValue = temperature)) + } + + fun setBatteryLevel(batteryLevel: Int) { + _data.tryEmit(_data.value.copy(batteryLevel = batteryLevel)) + } + + fun setTemperatureUnit(unit: TemperatureUnit) { + _data.tryEmit(_data.value.copy(temperatureUnit = unit)) + } + + fun clear() { + _data.tryEmit(HTSData()) + } +} diff --git a/feature_hts/src/main/java/no/nordicsemi/android/hts/service/DateTimeParser.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/service/DateTimeParser.kt similarity index 100% rename from feature_hts/src/main/java/no/nordicsemi/android/hts/service/DateTimeParser.kt rename to profile_hts/src/main/java/no/nordicsemi/android/hts/service/DateTimeParser.kt diff --git a/feature_hts/src/main/java/no/nordicsemi/android/hts/service/HTSManager.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/service/HTSManager.kt similarity index 75% rename from feature_hts/src/main/java/no/nordicsemi/android/hts/service/HTSManager.kt rename to profile_hts/src/main/java/no/nordicsemi/android/hts/service/HTSManager.kt index ba78d138..e54b14bb 100644 --- a/feature_hts/src/main/java/no/nordicsemi/android/hts/service/HTSManager.kt +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/service/HTSManager.kt @@ -29,6 +29,7 @@ import no.nordicsemi.android.ble.common.callback.ht.TemperatureMeasurementDataCa 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.HTSDataHolder import no.nordicsemi.android.log.LogContract import no.nordicsemi.android.service.BatteryManager import java.util.* @@ -41,10 +42,34 @@ private val HT_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A1C-0000- * enabling indication and reading characteristics. All operations required to connect to device * with BLE HT Service and reading health thermometer values are performed here. */ -class HTSManager internal constructor(context: Context) : BatteryManager(context) { +class HTSManager internal constructor(context: Context, private val dataHolder: HTSDataHolder) : BatteryManager(context) { private var htCharacteristic: BluetoothGattCharacteristic? = null + private val temperatureMeasurementDataCallback = object : TemperatureMeasurementDataCallback() { + override fun onDataReceived(device: BluetoothDevice, data: Data) { + log( + LogContract.Log.Level.APPLICATION, + "\"" + TemperatureMeasurementParser.parse(data) + "\" received" + ) + super.onDataReceived(device, data) + } + + override fun onTemperatureMeasurementReceived( + device: BluetoothDevice, + temperature: Float, + @TemperatureUnit unit: Int, + calendar: Calendar?, + @TemperatureType type: Int? + ) { + dataHolder.setNewTemperature(temperature) + } + } + + override fun onBatteryLevelChanged(batteryLevel: Int) { + dataHolder.setBatteryLevel(batteryLevel) + } + override fun getGattCallback(): BatteryManagerGattCallback { return HTManagerGattCallback() } @@ -57,31 +82,7 @@ class HTSManager internal constructor(context: Context) : BatteryManager "Armpit" 2 -> "Body (general)" 3 -> "Ear (usually ear lobe)" 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 new file mode 100644 index 00000000..4337f403 --- /dev/null +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSContentView.kt @@ -0,0 +1,70 @@ +package no.nordicsemi.android.hts.view + +import androidx.compose.foundation.layout.Box +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.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.hts.R +import no.nordicsemi.android.hts.data.HTSData +import no.nordicsemi.android.theme.view.BatteryLevelView +import no.nordicsemi.android.theme.view.KeyValueField +import no.nordicsemi.android.theme.view.ScreenSection +import no.nordicsemi.android.theme.view.SpeedUnitRadioGroup + +@Composable +internal fun HTSContentView(state: HTSData, onEvent: (HTSScreenViewEvent) -> Unit) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(16.dp)) + + ScreenSection { + Box(modifier = Modifier.padding(16.dp)) { + SpeedUnitRadioGroup(state.temperatureUnit, state.temperatureSettingsItems()) { + onEvent(OnTemperatureUnitSelected(it.unit)) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + ScreenSection { + KeyValueField( + stringResource(id = R.string.hts_temperature), + state.displayTemperature() + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + BatteryLevelView(state.batteryLevel) + + Spacer(modifier = Modifier.height(16.dp)) + + Button( + colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.secondary), + onClick = { onEvent(DisconnectEvent) } + ) { + Text(text = stringResource(id = R.string.disconnect)) + } + } +} + +@Preview +@Composable +private fun Preview() { + HTSContentView(state = HTSData()) { } +} diff --git a/feature_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreen.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreen.kt similarity index 78% rename from feature_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreen.kt rename to profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreen.kt index 8f0074cc..f33f1074 100644 --- a/feature_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreen.kt +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreen.kt @@ -2,8 +2,6 @@ package no.nordicsemi.android.hts.view import android.content.Intent import androidx.compose.foundation.layout.Column -import androidx.compose.material.Text -import androidx.compose.material.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -14,16 +12,18 @@ import no.nordicsemi.android.hts.R import no.nordicsemi.android.hts.data.HTSData import no.nordicsemi.android.hts.service.HTSService import no.nordicsemi.android.hts.viewmodel.HTSViewModel +import no.nordicsemi.android.theme.view.BackIconAppBar import no.nordicsemi.android.utils.isServiceRunning @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(state.isScreenActive) { - if (!state.isScreenActive) { + LaunchedEffect(isActive) { + if (!isActive) { finishAction() } if (context.isServiceRunning(HTSService::class.java.name)) { @@ -39,13 +39,15 @@ fun HTSScreen(finishAction: () -> Unit) { } } - HRSView(state) { viewModel.onEvent(it) } + HTSView(state) { viewModel.onEvent(it) } } @Composable -private fun HRSView(state: HTSData, onEvent: (HTSScreenViewEvent) -> Unit) { +private fun HTSView(state: HTSData, onEvent: (HTSScreenViewEvent) -> Unit) { Column { - TopAppBar(title = { Text(text = stringResource(id = R.string.hts_title)) }) + BackIconAppBar(stringResource(id = R.string.hts_title)) { + onEvent(DisconnectEvent) + } HTSContentView(state) { onEvent(it) } } diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreenViewEvent.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreenViewEvent.kt new file mode 100644 index 00000000..886df9b6 --- /dev/null +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreenViewEvent.kt @@ -0,0 +1,9 @@ +package no.nordicsemi.android.hts.view + +import no.nordicsemi.android.hts.data.TemperatureUnit + +internal sealed class HTSScreenViewEvent + +internal data class OnTemperatureUnitSelected(val value: TemperatureUnit) : HTSScreenViewEvent() + +internal object DisconnectEvent : HTSScreenViewEvent() 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 new file mode 100644 index 00000000..5faa9dae --- /dev/null +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/viewmodel/HTSViewModel.kt @@ -0,0 +1,34 @@ +package no.nordicsemi.android.hts.viewmodel + +import dagger.hilt.android.lifecycle.HiltViewModel +import no.nordicsemi.android.hts.data.HTSDataHolder +import no.nordicsemi.android.hts.view.DisconnectEvent +import no.nordicsemi.android.hts.view.HTSScreenViewEvent +import no.nordicsemi.android.hts.view.OnTemperatureUnitSelected +import no.nordicsemi.android.theme.viewmodel.CloseableViewModel +import no.nordicsemi.android.utils.exhaustive +import javax.inject.Inject + +@HiltViewModel +internal class HTSViewModel @Inject constructor( + private val dataHolder: HTSDataHolder +) : CloseableViewModel() { + + val state = dataHolder.data + + fun onEvent(event: HTSScreenViewEvent) { + when (event) { + DisconnectEvent -> onDisconnectButtonClick() + is OnTemperatureUnitSelected -> onTemperatureUnitSelected(event) + }.exhaustive + } + + private fun onDisconnectButtonClick() { + finish() + dataHolder.clear() + } + + private fun onTemperatureUnitSelected(event: OnTemperatureUnitSelected) { + dataHolder.setTemperatureUnit(event.value) + } +} diff --git a/feature_hts/src/main/res/values/strings.xml b/profile_hts/src/main/res/values/strings.xml similarity index 81% rename from feature_hts/src/main/res/values/strings.xml rename to profile_hts/src/main/res/values/strings.xml index cf6cda54..5c8ad9a6 100644 --- a/feature_hts/src/main/res/values/strings.xml +++ b/profile_hts/src/main/res/values/strings.xml @@ -5,4 +5,6 @@ %.1f °C %.1f °F %.1f °K + + Temperature diff --git a/feature_hts/src/test/java/no/nordicsemi/android/hts/ExampleUnitTest.kt b/profile_hts/src/test/java/no/nordicsemi/android/hts/ExampleUnitTest.kt similarity index 100% rename from feature_hts/src/test/java/no/nordicsemi/android/hts/ExampleUnitTest.kt rename to profile_hts/src/test/java/no/nordicsemi/android/hts/ExampleUnitTest.kt diff --git a/feature_scanner/build.gradle b/profile_scanner/build.gradle similarity index 100% rename from feature_scanner/build.gradle rename to profile_scanner/build.gradle diff --git a/feature_scanner/src/androidTest/java/no/nordicsemi/android/scanner/ExampleInstrumentedTest.kt b/profile_scanner/src/androidTest/java/no/nordicsemi/android/scanner/ExampleInstrumentedTest.kt similarity index 100% rename from feature_scanner/src/androidTest/java/no/nordicsemi/android/scanner/ExampleInstrumentedTest.kt rename to profile_scanner/src/androidTest/java/no/nordicsemi/android/scanner/ExampleInstrumentedTest.kt diff --git a/feature_scanner/src/main/AndroidManifest.xml b/profile_scanner/src/main/AndroidManifest.xml similarity index 100% rename from feature_scanner/src/main/AndroidManifest.xml rename to profile_scanner/src/main/AndroidManifest.xml diff --git a/feature_scanner/src/main/java/no/nordicsemi/android/scanner/HiltModule.kt b/profile_scanner/src/main/java/no/nordicsemi/android/scanner/HiltModule.kt similarity index 87% rename from feature_scanner/src/main/java/no/nordicsemi/android/scanner/HiltModule.kt rename to profile_scanner/src/main/java/no/nordicsemi/android/scanner/HiltModule.kt index 02216cab..8bb0dd5f 100644 --- a/feature_scanner/src/main/java/no/nordicsemi/android/scanner/HiltModule.kt +++ b/profile_scanner/src/main/java/no/nordicsemi/android/scanner/HiltModule.kt @@ -25,8 +25,8 @@ internal object HiltModule { fun createSelectedBluetoothDeviceHolder( @ApplicationContext context: Context, bluetoothAdapter: BluetoothAdapter? - ): no.nordicsemi.android.service.SelectedBluetoothDeviceHolder { - return no.nordicsemi.android.service.SelectedBluetoothDeviceHolder( + ): SelectedBluetoothDeviceHolder { + return SelectedBluetoothDeviceHolder( context, bluetoothAdapter ) diff --git a/feature_scanner/src/main/java/no/nordicsemi/android/scanner/tools/NordicBleScanner.kt b/profile_scanner/src/main/java/no/nordicsemi/android/scanner/tools/NordicBleScanner.kt similarity index 100% rename from feature_scanner/src/main/java/no/nordicsemi/android/scanner/tools/NordicBleScanner.kt rename to profile_scanner/src/main/java/no/nordicsemi/android/scanner/tools/NordicBleScanner.kt diff --git a/feature_scanner/src/main/java/no/nordicsemi/android/scanner/tools/PermissionHelper.kt b/profile_scanner/src/main/java/no/nordicsemi/android/scanner/tools/PermissionHelper.kt similarity index 100% rename from feature_scanner/src/main/java/no/nordicsemi/android/scanner/tools/PermissionHelper.kt rename to profile_scanner/src/main/java/no/nordicsemi/android/scanner/tools/PermissionHelper.kt diff --git a/feature_scanner/src/main/java/no/nordicsemi/android/scanner/tools/ScannerStatus.kt b/profile_scanner/src/main/java/no/nordicsemi/android/scanner/tools/ScannerStatus.kt similarity index 100% rename from feature_scanner/src/main/java/no/nordicsemi/android/scanner/tools/ScannerStatus.kt rename to profile_scanner/src/main/java/no/nordicsemi/android/scanner/tools/ScannerStatus.kt diff --git a/feature_scanner/src/main/java/no/nordicsemi/android/scanner/view/BluetoothNotAvailableScreen.kt b/profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/BluetoothNotAvailableScreen.kt similarity index 87% rename from feature_scanner/src/main/java/no/nordicsemi/android/scanner/view/BluetoothNotAvailableScreen.kt rename to profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/BluetoothNotAvailableScreen.kt index 1756e495..b76dae26 100644 --- a/feature_scanner/src/main/java/no/nordicsemi/android/scanner/view/BluetoothNotAvailableScreen.kt +++ b/profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/BluetoothNotAvailableScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.material.Button import androidx.compose.material.ButtonDefaults import androidx.compose.material.MaterialTheme import androidx.compose.material.Text -import androidx.compose.material.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -23,11 +22,15 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import no.nordicsemi.android.scanner.R +import no.nordicsemi.android.theme.view.BackIconAppBar +import no.nordicsemi.android.theme.view.CloseIconAppBar @Composable -fun BluetoothNotAvailableScreen() { +fun BluetoothNotAvailableScreen(finish: () -> Unit) { Column { - TopAppBar(title = { Text(text = stringResource(id = R.string.scanner__request_permission)) }) + CloseIconAppBar(stringResource(id = R.string.scanner__request_permission)) { + finish() + } Column( modifier = Modifier .fillMaxWidth() @@ -49,7 +52,9 @@ fun BluetoothNotEnabledScreen(finish: () -> Unit) { }) Column { - TopAppBar(title = { Text(text = stringResource(id = R.string.scanner__request_permission)) }) + BackIconAppBar(stringResource(id = R.string.scanner__request_permission)) { + finish() + } Column( modifier = Modifier .fillMaxWidth() diff --git a/feature_scanner/src/main/java/no/nordicsemi/android/scanner/view/NotConnectedView.kt b/profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/NotConnectedView.kt similarity index 100% rename from feature_scanner/src/main/java/no/nordicsemi/android/scanner/view/NotConnectedView.kt rename to profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/NotConnectedView.kt diff --git a/feature_scanner/src/main/java/no/nordicsemi/android/scanner/view/RequestPermissionScreen.kt b/profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/RequestPermissionScreen.kt similarity index 96% rename from feature_scanner/src/main/java/no/nordicsemi/android/scanner/view/RequestPermissionScreen.kt rename to profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/RequestPermissionScreen.kt index 13552db1..3f31af98 100644 --- a/feature_scanner/src/main/java/no/nordicsemi/android/scanner/view/RequestPermissionScreen.kt +++ b/profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/RequestPermissionScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material.Button import androidx.compose.material.Text -import androidx.compose.material.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable @@ -31,6 +30,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionsRequired import com.google.accompanist.permissions.rememberMultiplePermissionsState import no.nordicsemi.android.scanner.R +import no.nordicsemi.android.theme.view.BackIconAppBar @OptIn(ExperimentalPermissionsApi::class) @Composable @@ -40,7 +40,9 @@ fun RequestPermissionScreen(finish: () -> Unit) { )) Column { - TopAppBar(title = { Text(text = stringResource(id = R.string.scanner__request_permission)) }) + BackIconAppBar(stringResource(id = R.string.scanner__request_permission)) { + finish() + } PermissionsRequired( multiplePermissionsState = permissionsState, diff --git a/feature_scanner/src/main/java/no/nordicsemi/android/scanner/view/ScanDeviceScreen.kt b/profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/ScanDeviceScreen.kt similarity index 100% rename from feature_scanner/src/main/java/no/nordicsemi/android/scanner/view/ScanDeviceScreen.kt rename to profile_scanner/src/main/java/no/nordicsemi/android/scanner/view/ScanDeviceScreen.kt diff --git a/feature_scanner/src/main/java/no/nordicsemi/android/scanner/viewmodel/BluetoothPermissionState.kt b/profile_scanner/src/main/java/no/nordicsemi/android/scanner/viewmodel/BluetoothPermissionState.kt similarity index 100% rename from feature_scanner/src/main/java/no/nordicsemi/android/scanner/viewmodel/BluetoothPermissionState.kt rename to profile_scanner/src/main/java/no/nordicsemi/android/scanner/viewmodel/BluetoothPermissionState.kt diff --git a/feature_scanner/src/main/res/values/strings.xml b/profile_scanner/src/main/res/values/strings.xml similarity index 100% rename from feature_scanner/src/main/res/values/strings.xml rename to profile_scanner/src/main/res/values/strings.xml diff --git a/feature_scanner/src/test/java/no/nordicsemi/android/scanner/ExampleUnitTest.kt b/profile_scanner/src/test/java/no/nordicsemi/android/scanner/ExampleUnitTest.kt similarity index 100% rename from feature_scanner/src/test/java/no/nordicsemi/android/scanner/ExampleUnitTest.kt rename to profile_scanner/src/test/java/no/nordicsemi/android/scanner/ExampleUnitTest.kt diff --git a/settings.gradle b/settings.gradle index b4b29012..2222c137 100644 --- a/settings.gradle +++ b/settings.gradle @@ -61,11 +61,11 @@ rootProject.name = "Android-nRF-Toolbox" include ':app' -include ':feature_csc' -include ':feature_gls' -include ':feature_hrs' -include ':feature_hts' -include ':feature_scanner' +include ':profile_csc' +include ':profile_gls' +include ':profile_hrs' +include ':profile_hts' +include ':profile_scanner' include ':lib_service' include ':lib_theme' @@ -74,3 +74,7 @@ include ':lib_utils' if (file('../Android-BLE-Library').exists()) { includeBuild('../Android-BLE-Library') } + +if (file('../Android-Scanner-Compat-Library').exists()) { + includeBuild('../Android-Scanner-Compat-Library') +}