diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCServicesData.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCServiceData.kt similarity index 91% rename from profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCServicesData.kt rename to profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCServiceData.kt index f1ebb66e..70149f3f 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCServicesData.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCServiceData.kt @@ -3,7 +3,7 @@ package no.nordicsemi.android.csc.data import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState import no.nordicsemi.android.kotlin.ble.profile.csc.CSCData -data class CSCServicesData( +data class CSCServiceData( val data: CSCData = CSCData(), val batteryLevel: Int? = null, val connectionState: GattConnectionState? = null diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCRepository.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCRepository.kt index 9f4103df..ad90c346 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCRepository.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCRepository.kt @@ -39,7 +39,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map import no.nordicsemi.android.common.core.simpleSharedFlow import no.nordicsemi.android.common.logger.NordicLogger -import no.nordicsemi.android.csc.data.CSCServicesData +import no.nordicsemi.android.csc.data.CSCServiceData import no.nordicsemi.android.kotlin.ble.core.ServerDevice import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState import no.nordicsemi.android.kotlin.ble.profile.csc.CSCData @@ -61,7 +61,7 @@ class CSCRepository @Inject constructor( private val _wheelSize = MutableStateFlow(WheelSizes.default) internal val wheelSize = _wheelSize.asStateFlow() - private val _data = MutableStateFlow(CSCServicesData()) + private val _data = MutableStateFlow(CSCServiceData()) internal val data = _data.asStateFlow() private val _stopEvent = simpleSharedFlow() diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCContentView.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCContentView.kt index e8133ab8..c2204ab8 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCContentView.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCContentView.kt @@ -49,7 +49,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import no.nordicsemi.android.common.theme.view.RadioButtonGroup import no.nordicsemi.android.csc.R -import no.nordicsemi.android.csc.data.CSCServicesData +import no.nordicsemi.android.csc.data.CSCServiceData import no.nordicsemi.android.kotlin.ble.profile.csc.CSCData import no.nordicsemi.android.kotlin.ble.profile.csc.WheelSize import no.nordicsemi.android.ui.view.ScreenSection @@ -58,7 +58,7 @@ import no.nordicsemi.android.ui.view.dialog.FlowCanceled import no.nordicsemi.android.ui.view.dialog.ItemSelectedResult @Composable -internal fun CSCContentView(state: CSCServicesData, speedUnit: SpeedUnit, onEvent: (CSCViewEvent) -> Unit) { +internal fun CSCContentView(state: CSCServiceData, speedUnit: SpeedUnit, onEvent: (CSCViewEvent) -> Unit) { val showDialog = rememberSaveable { mutableStateOf(false) } if (showDialog.value) { @@ -125,5 +125,5 @@ private fun SettingsSection( @Preview @Composable private fun ConnectedPreview() { - CSCContentView(CSCServicesData(), SpeedUnit.KM_H) { } + CSCContentView(CSCServiceData(), SpeedUnit.KM_H) { } } diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCState.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCState.kt index 9fbbe674..c5638323 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCState.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCState.kt @@ -31,7 +31,7 @@ package no.nordicsemi.android.csc.view -import no.nordicsemi.android.csc.data.CSCServicesData +import no.nordicsemi.android.csc.data.CSCServiceData internal data class CSCViewState( val speedUnit: SpeedUnit = SpeedUnit.M_S, @@ -41,6 +41,6 @@ internal data class CSCViewState( internal sealed class CSCMangerState -internal data class WorkingState(val result: CSCServicesData) : CSCMangerState() +internal data class WorkingState(val result: CSCServiceData) : CSCMangerState() internal object NoDeviceState : CSCMangerState() diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/SensorsReadingView.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/SensorsReadingView.kt index cfa0c373..10cbbd5c 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/SensorsReadingView.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/SensorsReadingView.kt @@ -40,14 +40,14 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import no.nordicsemi.android.csc.R -import no.nordicsemi.android.csc.data.CSCServicesData +import no.nordicsemi.android.csc.data.CSCServiceData import no.nordicsemi.android.ui.view.BatteryLevelView import no.nordicsemi.android.ui.view.KeyValueField import no.nordicsemi.android.ui.view.ScreenSection import no.nordicsemi.android.ui.view.SectionTitle @Composable -internal fun SensorsReadingView(state: CSCServicesData, speedUnit: SpeedUnit) { +internal fun SensorsReadingView(state: CSCServiceData, speedUnit: SpeedUnit) { val csc = state.data ScreenSection { SectionTitle(resId = R.drawable.ic_records, title = "Records") @@ -80,5 +80,5 @@ internal fun SensorsReadingView(state: CSCServicesData, speedUnit: SpeedUnit) { @Preview @Composable private fun Preview() { - SensorsReadingView(CSCServicesData(), SpeedUnit.KM_H) + SensorsReadingView(CSCServiceData(), SpeedUnit.KM_H) } diff --git a/profile_hrs/build.gradle.kts b/profile_hrs/build.gradle.kts index ea8177d8..470c8a69 100644 --- a/profile_hrs/build.gradle.kts +++ b/profile_hrs/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { implementation(libs.nordic.navigation) implementation(libs.nordic.uiscanner) implementation(libs.nordic.uilogger) + implementation(libs.nordic.core) implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.compose.material.iconsExtended) diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSManager.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSManager.kt deleted file mode 100644 index d02ab5cc..00000000 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSManager.kt +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright (c) 2022, 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.data - -import android.bluetooth.BluetoothGatt -import android.bluetooth.BluetoothGattCharacteristic -import android.content.Context -import android.util.Log -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import no.nordicsemi.android.ble.BleManager -import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelResponse -import no.nordicsemi.android.ble.common.callback.hr.BodySensorLocationResponse -import no.nordicsemi.android.ble.common.callback.hr.HeartRateMeasurementResponse -import no.nordicsemi.android.ble.ktx.asValidResponseFlow -import no.nordicsemi.android.ble.ktx.suspendForValidResponse -import no.nordicsemi.android.common.logger.NordicLogger -import no.nordicsemi.android.service.ConnectionObserverAdapter -import no.nordicsemi.android.utils.launchWithCatch -import java.util.* - -val HRS_SERVICE_UUID: 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 val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb") -private val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb") - -internal class HRSManager( - context: Context, - private val scope: CoroutineScope, - private val logger: NordicLogger -) : BleManager(context) { - - private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null - private var heartRateCharacteristic: BluetoothGattCharacteristic? = null - private var bodySensorLocationCharacteristic: BluetoothGattCharacteristic? = null - - private val data = MutableStateFlow(HRSData()) - val dataHolder = ConnectionObserverAdapter() - - init { - connectionObserver = dataHolder - - data.onEach { - dataHolder.setValue(it) - }.launchIn(scope) - } - - override fun log(priority: Int, message: String) { - logger.log(priority, message) - } - - override fun getMinLogPriority(): Int { - return Log.VERBOSE - } - - override fun getGattCallback(): BleManagerGattCallback { - return HeartRateManagerCallback() - } - - private inner class HeartRateManagerCallback : BleManagerGattCallback() { - override fun initialize() { - super.initialize() - - scope.launchWithCatch { - val readData = readCharacteristic(bodySensorLocationCharacteristic) - .suspendForValidResponse() - - data.value = data.value.copy(sensorLocation = readData.sensorLocation) - } - - setNotificationCallback(heartRateCharacteristic).asValidResponseFlow() - .onEach { - val result = data.value.heartRates.toMutableList().apply { - add(it.heartRate) - } - data.tryEmit(data.value.copy(heartRates = result)) - }.launchIn(scope) - enableNotifications(heartRateCharacteristic).enqueue() - - setNotificationCallback(batteryLevelCharacteristic).asValidResponseFlow().onEach { - data.value = data.value.copy(batteryLevel = it.batteryLevel) - }.launchIn(scope) - enableNotifications(batteryLevelCharacteristic).enqueue() - } - - override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean { - gatt.getService(HRS_SERVICE_UUID)?.run { - heartRateCharacteristic = getCharacteristic(HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID) - bodySensorLocationCharacteristic = getCharacteristic(BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID) - } - gatt.getService(BATTERY_SERVICE_UUID)?.run { - batteryLevelCharacteristic = getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID) - } - return heartRateCharacteristic != null - } - - override fun onServicesInvalidated() { - bodySensorLocationCharacteristic = null - heartRateCharacteristic = null - batteryLevelCharacteristic = null - } - } -} diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSData.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSServiceData.kt similarity index 82% rename from profile_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSData.kt rename to profile_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSServiceData.kt index a72fbc2e..8d326ecd 100644 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSData.kt +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSServiceData.kt @@ -31,8 +31,14 @@ package no.nordicsemi.android.hrs.data -internal data class HRSData( - val heartRates: List = emptyList(), +import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState +import no.nordicsemi.android.kotlin.ble.profile.hrs.HRSData + +internal data class HRSServiceData( + val data: List = emptyList(), + val bodySensorLocation: Int? = null, val batteryLevel: Int? = null, - val sensorLocation: Int = 0, -) + val connectionState: GattConnectionState? = null +) { + val heartRates = data.map { it.heartRate } +} diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSRepository.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSRepository.kt index 85d0928f..45e49803 100644 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSRepository.kt +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSRepository.kt @@ -33,22 +33,18 @@ package no.nordicsemi.android.hrs.service import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch +import no.nordicsemi.android.common.core.simpleSharedFlow import no.nordicsemi.android.common.logger.NordicLogger -import no.nordicsemi.android.common.logger.NordicLoggerFactory -import no.nordicsemi.android.hrs.data.HRSData -import no.nordicsemi.android.hrs.data.HRSManager +import no.nordicsemi.android.hrs.data.HRSServiceData import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.service.BleManagerResult -import no.nordicsemi.android.service.IdleResult +import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState +import no.nordicsemi.android.kotlin.ble.profile.hrs.HRSData +import no.nordicsemi.android.service.DisconnectAndStopEvent import no.nordicsemi.android.service.ServiceManager -import no.nordicsemi.android.ui.view.StringConst import javax.inject.Inject import javax.inject.Singleton @@ -56,57 +52,43 @@ import javax.inject.Singleton class HRSRepository @Inject constructor( @ApplicationContext private val context: Context, - private val serviceManager: ServiceManager, - private val loggerFactory: NordicLoggerFactory, - private val stringConst: StringConst + private val serviceManager: ServiceManager ) { - private var manager: HRSManager? = null private var logger: NordicLogger? = null - private val _data = MutableStateFlow>(IdleResult()) + private val _data = MutableStateFlow(HRSServiceData()) internal val data = _data.asStateFlow() - val isRunning = data.map { it.isRunning() } - val hasBeenDisconnected = data.map { it.hasBeenDisconnected() } + private val _stopEvent = simpleSharedFlow() + internal val stopEvent = _stopEvent.asSharedFlow() + + val isRunning = data.map { it.connectionState == GattConnectionState.STATE_CONNECTED } fun launch(device: ServerDevice) { serviceManager.startService(HRSService::class.java, device) } - fun start(device: ServerDevice, scope: CoroutineScope) { - val createdLogger = loggerFactory.create(stringConst.APP_NAME, "HRS", device.address).also { - logger = it - } - val manager = HRSManager(context, scope, createdLogger) - this.manager = manager + fun onConnectionStateChanged(connectionState: GattConnectionState?) { + _data.value = _data.value.copy(connectionState = connectionState) + } - manager.dataHolder.status.onEach { - _data.value = it - }.launchIn(scope) + fun onHRSDataChanged(data: HRSData) { + _data.value = _data.value.copy(data = _data.value.data + data) + } - scope.launch { - manager.start(device) - } + fun onBodySensorLocationChanged(bodySensorLocation: Int) { + _data.value = _data.value.copy(bodySensorLocation = bodySensorLocation) + } + + fun onBatteryLevelChanged(batteryLevel: Int) { + _data.value = _data.value.copy(batteryLevel = batteryLevel) } fun openLogger() { NordicLogger.launch(context, logger) } - private suspend fun HRSManager.start(device: ServerDevice) { -// try { -// connect(device.device) -// .useAutoConnect(false) -// .retry(3, 100) -// .suspend() -// } catch (e: Exception) { -// e.printStackTrace() -// } - } - fun release() { - manager?.disconnect()?.enqueue() - logger = null - manager = null + _stopEvent.tryEmit(DisconnectAndStopEvent()) } } diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSService.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSService.kt index a3d7675d..e267a7dc 100644 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSService.kt +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSService.kt @@ -31,33 +31,100 @@ package no.nordicsemi.android.hrs.service +import android.annotation.SuppressLint import android.content.Intent import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import no.nordicsemi.android.kotlin.ble.core.ServerDevice +import no.nordicsemi.android.kotlin.ble.core.client.callback.BleGattClient +import no.nordicsemi.android.kotlin.ble.core.client.service.BleGattServices +import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState +import no.nordicsemi.android.kotlin.ble.profile.battery.BatteryLevelParser +import no.nordicsemi.android.kotlin.ble.profile.hrs.BodySensorLocationParser +import no.nordicsemi.android.kotlin.ble.profile.hrs.HRSDataParser import no.nordicsemi.android.service.DEVICE_DATA import no.nordicsemi.android.service.NotificationService +import java.util.* import javax.inject.Inject +val HRS_SERVICE_UUID: 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 val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb") +private val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb") + +@SuppressLint("MissingPermission") @AndroidEntryPoint internal class HRSService : NotificationService() { @Inject lateinit var repository: HRSRepository + private lateinit var client: BleGattClient + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) val device = intent!!.getParcelableExtra(DEVICE_DATA)!! - repository.start(device, lifecycleScope) + startGattClient(device) - repository.hasBeenDisconnected.onEach { - if (it) stopSelf() - }.launchIn(lifecycleScope) + repository.stopEvent + .onEach { disconnect() } + .launchIn(lifecycleScope) return START_REDELIVER_INTENT } + + private fun startGattClient(blinkyDevice: ServerDevice) = lifecycleScope.launch { + client = blinkyDevice.connect(this@HRSService) + + client.connectionState + .onEach { repository.onConnectionStateChanged(it) } + .filterNotNull() + .onEach { stopIfDisconnected(it) } + .launchIn(lifecycleScope) + + client.services + .filterNotNull() + .onEach { configureGatt(it) } + .launchIn(lifecycleScope) + } + + private suspend fun configureGatt(services: BleGattServices) { + val htsService = services.findService(HRS_SERVICE_UUID)!! + val htsMeasurementCharacteristic = htsService.findCharacteristic(HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID)!! + val bodySensorLocationCharacteristic = htsService.findCharacteristic(BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID)!! + val batteryService = services.findService(BATTERY_SERVICE_UUID)!! + val batteryLevelCharacteristic = batteryService.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)!! + + val bodySensorLocation = bodySensorLocationCharacteristic.read() + BodySensorLocationParser.parse(bodySensorLocation)?.let { repository.onBodySensorLocationChanged(it) } + + batteryLevelCharacteristic.getNotifications() + .mapNotNull { BatteryLevelParser.parse(it) } + .onEach { repository.onBatteryLevelChanged(it) } + .launchIn(lifecycleScope) + + htsMeasurementCharacteristic.getNotifications() + .mapNotNull { HRSDataParser.parse(it) } + .onEach { repository.onHRSDataChanged(it) } + .launchIn(lifecycleScope) + } + + private fun stopIfDisconnected(connectionState: GattConnectionState) { + if (connectionState == GattConnectionState.STATE_DISCONNECTED) { + stopSelf() + } + } + + private fun disconnect() { + client.disconnect() + } } diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSContentView.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSContentView.kt index d7e96190..3b653850 100644 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSContentView.kt +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSContentView.kt @@ -46,13 +46,13 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import no.nordicsemi.android.hrs.R -import no.nordicsemi.android.hrs.data.HRSData +import no.nordicsemi.android.hrs.data.HRSServiceData import no.nordicsemi.android.ui.view.BatteryLevelView import no.nordicsemi.android.ui.view.ScreenSection import no.nordicsemi.android.ui.view.SectionTitle @Composable -internal fun HRSContentView(state: HRSData, zoomIn: Boolean, onEvent: (HRSScreenViewEvent) -> Unit) { +internal fun HRSContentView(state: HRSServiceData, zoomIn: Boolean, onEvent: (HRSScreenViewEvent) -> Unit) { Column( horizontalAlignment = Alignment.CenterHorizontally ) { @@ -102,5 +102,5 @@ private fun Menu(zoomIn: Boolean, onEvent: (HRSScreenViewEvent) -> Unit) { @Preview @Composable private fun Preview() { - HRSContentView(state = HRSData(), zoomIn = false) { } + HRSContentView(state = HRSServiceData(), zoomIn = false) { } } diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreen.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreen.kt index c615f37a..c37c2882 100644 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreen.kt +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreen.kt @@ -48,15 +48,7 @@ import no.nordicsemi.android.common.ui.scanner.view.DeviceDisconnectedView import no.nordicsemi.android.common.ui.scanner.view.Reason import no.nordicsemi.android.hrs.R import no.nordicsemi.android.hrs.viewmodel.HRSViewModel -import no.nordicsemi.android.service.ConnectedResult -import no.nordicsemi.android.service.ConnectingResult -import no.nordicsemi.android.service.DeviceHolder -import no.nordicsemi.android.service.DisconnectedResult -import no.nordicsemi.android.service.IdleResult -import no.nordicsemi.android.service.LinkLossResult -import no.nordicsemi.android.service.MissingServiceResult -import no.nordicsemi.android.service.SuccessResult -import no.nordicsemi.android.service.UnknownErrorResult +import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState import no.nordicsemi.android.ui.view.BackIconAppBar import no.nordicsemi.android.ui.view.LoggerIconAppBar import no.nordicsemi.android.ui.view.NavigateUpButton @@ -78,17 +70,14 @@ fun HRSScreen() { .padding(16.dp) .verticalScroll(rememberScrollState()) ) { - when (state) { + when (state.hrsManagerState) { NoDeviceState -> DeviceConnectingView() - is WorkingState -> when (state.result) { - is IdleResult, - is ConnectingResult -> DeviceConnectingView { NavigateUpButton(navigateUp) } - is ConnectedResult -> DeviceConnectingView { NavigateUpButton(navigateUp) } - is DisconnectedResult -> DeviceDisconnectedView(Reason.USER) { NavigateUpButton(navigateUp) } - is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS) { NavigateUpButton(navigateUp) } - is MissingServiceResult -> DeviceDisconnectedView(Reason.MISSING_SERVICE) { NavigateUpButton(navigateUp) } - is UnknownErrorResult -> DeviceDisconnectedView(Reason.UNKNOWN) { NavigateUpButton(navigateUp) } - is SuccessResult -> HRSContentView(state.result.data, state.zoomIn) { viewModel.onEvent(it) } + is WorkingState -> when (state.hrsManagerState.result.connectionState) { + null, + GattConnectionState.STATE_CONNECTING -> DeviceConnectingView { NavigateUpButton(navigateUp) } + GattConnectionState.STATE_DISCONNECTED, + GattConnectionState.STATE_DISCONNECTING -> DeviceDisconnectedView(Reason.UNKNOWN) { NavigateUpButton(navigateUp) } + GattConnectionState.STATE_CONNECTED -> HRSContentView(state.hrsManagerState.result, state.zoomIn) { viewModel.onEvent(it) } } } } @@ -97,15 +86,11 @@ fun HRSScreen() { @Composable private fun AppBar(state: HRSViewState, navigateUp: () -> Unit, viewModel: HRSViewModel) { - val toolbarName = (state as? WorkingState)?.let { - (it.result as? DeviceHolder)?.deviceName() - } - - if (toolbarName == null) { - BackIconAppBar(stringResource(id = R.string.hrs_title), navigateUp) - } else { - LoggerIconAppBar(toolbarName, navigateUp, { viewModel.onEvent(DisconnectEvent) }) { + if (state.deviceName?.isNotBlank() == true) { + LoggerIconAppBar(state.deviceName, navigateUp, { viewModel.onEvent(DisconnectEvent) }) { viewModel.onEvent(OpenLoggerEvent) } + } else { + BackIconAppBar(stringResource(id = R.string.hrs_title), navigateUp) } } diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSState.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSState.kt index b003daa7..d0d3e597 100644 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSState.kt +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSState.kt @@ -31,14 +31,16 @@ package no.nordicsemi.android.hrs.view -import no.nordicsemi.android.hrs.data.HRSData -import no.nordicsemi.android.service.BleManagerResult +import no.nordicsemi.android.hrs.data.HRSServiceData -internal sealed class HRSViewState - -internal data class WorkingState( - val result: BleManagerResult, +internal data class HRSViewState( val zoomIn: Boolean = false, -) : HRSViewState() + val hrsManagerState: HRSManagerState = NoDeviceState, + val deviceName: String? = null +) -internal object NoDeviceState : HRSViewState() +internal sealed class HRSManagerState + +internal data class WorkingState(val result: HRSServiceData) : HRSManagerState() + +internal object NoDeviceState : HRSManagerState() diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/LineChartView.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/LineChartView.kt index 9f0ad9c4..aa185af2 100644 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/LineChartView.kt +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/LineChartView.kt @@ -47,7 +47,7 @@ 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.interfaces.datasets.ILineDataSet -import no.nordicsemi.android.hrs.data.HRSData +import no.nordicsemi.android.hrs.data.HRSServiceData private const val X_AXIS_ELEMENTS_COUNT = 40f @@ -55,7 +55,7 @@ private const val AXIS_MIN = 0 private const val AXIS_MAX = 300 @Composable -internal fun LineChartView(state: HRSData, zoomIn: Boolean,) { +internal fun LineChartView(state: HRSServiceData, zoomIn: Boolean,) { val items = state.heartRates.takeLast(X_AXIS_ELEMENTS_COUNT.toInt()).reversed() val isSystemInDarkTheme = isSystemInDarkTheme() AndroidView( @@ -119,8 +119,8 @@ internal fun createLineChartView( val entries = points.mapIndexed { i, v -> Entry(-i.toFloat(), v.toFloat()) }.reversed() - // create a dataset and give it a type + // create a dataset and give it a type if (data != null && data.dataSetCount > 0) { val set1 = data!!.getDataSetByIndex(0) as LineDataSet set1.values = entries @@ -133,13 +133,9 @@ internal fun createLineChartView( set1.setDrawIcons(false) set1.setDrawValues(false) - // draw dashed line - // draw dashed line set1.enableDashedLine(10f, 5f, 0f) - // black lines and points - // black lines and points if (isDarkTheme) { set1.color = Color.WHITE @@ -149,31 +145,21 @@ internal fun createLineChartView( set1.setCircleColor(Color.BLACK) } - // line thickness and point size - // line thickness and point size set1.lineWidth = 1f set1.circleRadius = 3f - // draw points as solid circles - // draw points as solid circles set1.setDrawCircleHole(false) - // customize legend entry - // customize legend entry set1.formLineWidth = 1f set1.formLineDashEffect = DashPathEffect(floatArrayOf(10f, 5f), 0f) set1.formSize = 15f - // text size of values - // text size of values set1.valueTextSize = 9f - // draw selection line as dashed - // draw selection line as dashed set1.enableDashedHighlightLine(10f, 5f, 0f) @@ -183,8 +169,6 @@ internal fun createLineChartView( // create a data object with the data sets val data = LineData(dataSets) - // set data - // set data setData(data) } diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/viewmodel/HRSViewModel.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/viewmodel/HRSViewModel.kt index 0ebe4367..6e14805b 100644 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/viewmodel/HRSViewModel.kt +++ b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/viewmodel/HRSViewModel.kt @@ -46,18 +46,17 @@ import no.nordicsemi.android.analytics.Profile import no.nordicsemi.android.analytics.ProfileConnectedEvent import no.nordicsemi.android.common.navigation.NavigationResult import no.nordicsemi.android.common.navigation.Navigator -import no.nordicsemi.android.hrs.data.HRS_SERVICE_UUID import no.nordicsemi.android.hrs.service.HRSRepository +import no.nordicsemi.android.hrs.service.HRS_SERVICE_UUID import no.nordicsemi.android.hrs.view.DisconnectEvent import no.nordicsemi.android.hrs.view.HRSScreenViewEvent import no.nordicsemi.android.hrs.view.HRSViewState import no.nordicsemi.android.hrs.view.NavigateUpEvent -import no.nordicsemi.android.hrs.view.NoDeviceState import no.nordicsemi.android.hrs.view.OpenLoggerEvent import no.nordicsemi.android.hrs.view.SwitchZoomEvent import no.nordicsemi.android.hrs.view.WorkingState import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.service.ConnectedResult +import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId import javax.inject.Inject @@ -68,7 +67,7 @@ internal class HRSViewModel @Inject constructor( private val analytics: AppAnalytics ) : ViewModel() { - private val _state = MutableStateFlow(NoDeviceState) + private val _state = MutableStateFlow(HRSViewState()) val state = _state.asStateFlow() init { @@ -79,10 +78,9 @@ internal class HRSViewModel @Inject constructor( } repository.data.onEach { - val zoomIn = (_state.value as? WorkingState)?.zoomIn ?: false - _state.value = WorkingState(it, zoomIn) + _state.value = _state.value.copy(hrsManagerState = WorkingState(it)) - (it as? ConnectedResult)?.let { + if (it.connectionState == GattConnectionState.STATE_CONNECTED) { analytics.logEvent(ProfileConnectedEvent(Profile.HRS)) } }.launchIn(viewModelScope) @@ -113,9 +111,7 @@ internal class HRSViewModel @Inject constructor( } private fun onZoomButtonClicked() { - (_state.value as? WorkingState)?.let { - _state.value = it.copy(zoomIn = !it.zoomIn) - } + _state.value = _state.value.copy(zoomIn = !_state.value.zoomIn) } private fun disconnect() { diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSServicesData.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSServiceData.kt similarity index 98% rename from profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSServicesData.kt rename to profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSServiceData.kt index 05398976..ab54a19f 100644 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSServicesData.kt +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSServiceData.kt @@ -34,7 +34,7 @@ package no.nordicsemi.android.hts.data import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState import no.nordicsemi.android.kotlin.ble.profile.hts.HTSData -internal data class HTSServicesData( +internal data class HTSServiceData( val data: HTSData = HTSData(), val batteryLevel: Int? = null, val connectionState: GattConnectionState? = null diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSRepository.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSRepository.kt index 7b382b23..e3ee177d 100644 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSRepository.kt +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSRepository.kt @@ -39,7 +39,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map import no.nordicsemi.android.common.core.simpleSharedFlow import no.nordicsemi.android.common.logger.NordicLogger -import no.nordicsemi.android.hts.data.HTSServicesData +import no.nordicsemi.android.hts.data.HTSServiceData import no.nordicsemi.android.kotlin.ble.core.ServerDevice import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState import no.nordicsemi.android.kotlin.ble.profile.hts.HTSData @@ -56,7 +56,7 @@ class HTSRepository @Inject constructor( ) { private var logger: NordicLogger? = null - private val _data = MutableStateFlow(HTSServicesData()) + private val _data = MutableStateFlow(HTSServiceData()) internal val data = _data.asStateFlow() private val _stopEvent = simpleSharedFlow() diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSContentView.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSContentView.kt index 2b5abc5f..d46a4a0f 100644 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSContentView.kt +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSContentView.kt @@ -45,14 +45,14 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import no.nordicsemi.android.common.theme.view.RadioButtonGroup import no.nordicsemi.android.hts.R -import no.nordicsemi.android.hts.data.HTSServicesData +import no.nordicsemi.android.hts.data.HTSServiceData import no.nordicsemi.android.ui.view.BatteryLevelView import no.nordicsemi.android.ui.view.KeyValueField import no.nordicsemi.android.ui.view.ScreenSection import no.nordicsemi.android.ui.view.SectionTitle @Composable -internal fun HTSContentView(state: HTSServicesData, temperatureUnit: TemperatureUnit, onEvent: (HTSScreenViewEvent) -> Unit) { +internal fun HTSContentView(state: HTSServiceData, temperatureUnit: TemperatureUnit, onEvent: (HTSScreenViewEvent) -> Unit) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally @@ -99,5 +99,5 @@ internal fun HTSContentView(state: HTSServicesData, temperatureUnit: Temperature @Preview @Composable private fun Preview() { - HTSContentView(state = HTSServicesData(), TemperatureUnit.CELSIUS) { } + HTSContentView(state = HTSServiceData(), TemperatureUnit.CELSIUS) { } } diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSState.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSState.kt index d954ab67..ef122680 100644 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSState.kt +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSState.kt @@ -31,7 +31,7 @@ package no.nordicsemi.android.hts.view -import no.nordicsemi.android.hts.data.HTSServicesData +import no.nordicsemi.android.hts.data.HTSServiceData internal data class HTSViewState( val temperatureUnit: TemperatureUnit = TemperatureUnit.CELSIUS, @@ -41,6 +41,6 @@ internal data class HTSViewState( internal sealed class HTSManagerState -internal data class WorkingState(val result: HTSServicesData) : HTSManagerState() +internal data class WorkingState(val result: HTSServiceData) : HTSManagerState() internal object NoDeviceState : HTSManagerState()