diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/DisconnectAndStopEvent.kt b/lib_service/src/main/java/no/nordicsemi/android/service/DisconnectAndStopEvent.kt new file mode 100644 index 00000000..31bdb0ee --- /dev/null +++ b/lib_service/src/main/java/no/nordicsemi/android/service/DisconnectAndStopEvent.kt @@ -0,0 +1,3 @@ +package no.nordicsemi.android.service + +class DisconnectAndStopEvent diff --git a/profile_csc/build.gradle.kts b/profile_csc/build.gradle.kts index ac51882f..016ed175 100644 --- a/profile_csc/build.gradle.kts +++ b/profile_csc/build.gradle.kts @@ -55,6 +55,7 @@ dependencies { implementation(libs.nordic.theme) implementation(libs.nordic.navigation) implementation(libs.nordic.uiscanner) + implementation(libs.nordic.core) implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.compose.material.iconsExtended) 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/CSCServicesData.kt index cfc0f48f..f1ebb66e 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/CSCServicesData.kt @@ -6,5 +6,5 @@ import no.nordicsemi.android.kotlin.ble.profile.csc.CSCData data class CSCServicesData( val data: CSCData = CSCData(), val batteryLevel: Int? = null, - val connectionState: GattConnectionState = GattConnectionState.STATE_DISCONNECTED + 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 85f67a79..9f4103df 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 @@ -34,8 +34,10 @@ package no.nordicsemi.android.csc.repository import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow 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.kotlin.ble.core.ServerDevice @@ -43,6 +45,7 @@ import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState import no.nordicsemi.android.kotlin.ble.profile.csc.CSCData import no.nordicsemi.android.kotlin.ble.profile.csc.WheelSize import no.nordicsemi.android.kotlin.ble.profile.csc.WheelSizes +import no.nordicsemi.android.service.DisconnectAndStopEvent import no.nordicsemi.android.service.ServiceManager import javax.inject.Inject import javax.inject.Singleton @@ -61,6 +64,9 @@ class CSCRepository @Inject constructor( private val _data = MutableStateFlow(CSCServicesData()) internal val data = _data.asStateFlow() + private val _stopEvent = simpleSharedFlow() + internal val stopEvent = _stopEvent.asSharedFlow() + val isRunning = data.map { it.connectionState == GattConnectionState.STATE_CONNECTED } fun launch(device: ServerDevice) { @@ -71,7 +77,7 @@ class CSCRepository @Inject constructor( _wheelSize.value = wheelSize } - fun onConnectionStateChanged(connectionState: GattConnectionState) { + fun onConnectionStateChanged(connectionState: GattConnectionState?) { _data.value = _data.value.copy(connectionState = connectionState) } @@ -89,6 +95,6 @@ class CSCRepository @Inject constructor( fun release() { logger = null - serviceManager.stopService(CSCService::class.java) + _stopEvent.tryEmit(DisconnectAndStopEvent()) } } diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCService.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCService.kt index 119c5a27..89c4a506 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCService.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCService.kt @@ -41,7 +41,9 @@ 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.csc.CSCDataParser import no.nordicsemi.android.service.DEVICE_DATA @@ -62,6 +64,8 @@ internal class CSCService : NotificationService() { @Inject lateinit var repository: CSCRepository + private lateinit var client: BleGattClient + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) @@ -69,14 +73,20 @@ internal class CSCService : NotificationService() { startGattClient(device) + repository.stopEvent + .onEach { disconnect() } + .launchIn(lifecycleScope) + return START_REDELIVER_INTENT } private fun startGattClient(blinkyDevice: ServerDevice) = lifecycleScope.launch { - val client = blinkyDevice.connect(this@CSCService) + client = blinkyDevice.connect(this@CSCService) client.connectionState .onEach { repository.onConnectionStateChanged(it) } + .filterNotNull() + .onEach { stopIfDisconnected(it) } .launchIn(lifecycleScope) client.services @@ -102,4 +112,14 @@ internal class CSCService : NotificationService() { .onEach { repository.onCSCDataChanged(it) } .launchIn(lifecycleScope) } + + private fun stopIfDisconnected(connectionState: GattConnectionState) { + if (connectionState == GattConnectionState.STATE_DISCONNECTED) { + stopSelf() + } + } + + private fun disconnect() { + client.disconnect() + } } diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCScreen.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCScreen.kt index 89443dc4..003d895b 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCScreen.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCScreen.kt @@ -49,7 +49,6 @@ import no.nordicsemi.android.common.ui.scanner.view.Reason import no.nordicsemi.android.csc.R import no.nordicsemi.android.csc.viewmodel.CSCViewModel import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.service.DeviceHolder import no.nordicsemi.android.ui.view.BackIconAppBar import no.nordicsemi.android.ui.view.LoggerIconAppBar import no.nordicsemi.android.ui.view.NavigateUpButton @@ -74,6 +73,7 @@ fun CSCScreen() { when (state.cscManagerState) { NoDeviceState -> DeviceConnectingView() is WorkingState -> when (state.cscManagerState.result.connectionState) { + null, GattConnectionState.STATE_CONNECTING -> DeviceConnectingView { NavigateUpButton(navigateUp) } GattConnectionState.STATE_DISCONNECTED, GattConnectionState.STATE_DISCONNECTING -> DeviceDisconnectedView(Reason.UNKNOWN) { NavigateUpButton(navigateUp) } @@ -86,15 +86,11 @@ fun CSCScreen() { @Composable private fun AppBar(state: CSCViewState, navigateUp: () -> Unit, viewModel: CSCViewModel) { - val toolbarName = (state.cscManagerState as? WorkingState)?.let { - (it.result as? DeviceHolder)?.deviceName() - } - - if (toolbarName == null) { - BackIconAppBar(stringResource(id = R.string.csc_title), navigateUp) - } else { - LoggerIconAppBar(toolbarName, navigateUp, { viewModel.onEvent(OnDisconnectButtonClick) }) { + if (state.deviceName?.isNotBlank() == true) { + LoggerIconAppBar(state.deviceName, navigateUp, { viewModel.onEvent(OnDisconnectButtonClick) }) { viewModel.onEvent(OpenLogger) } + } else { + BackIconAppBar(stringResource(id = R.string.csc_title), navigateUp) } } 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 dc2840aa..9fbbe674 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 @@ -35,7 +35,8 @@ import no.nordicsemi.android.csc.data.CSCServicesData internal data class CSCViewState( val speedUnit: SpeedUnit = SpeedUnit.M_S, - val cscManagerState: CSCMangerState = NoDeviceState + val cscManagerState: CSCMangerState = NoDeviceState, + val deviceName: String? = null ) internal sealed class CSCMangerState diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/viewmodel/CSCViewModel.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/viewmodel/CSCViewModel.kt index c3fb8628..b6a0c75e 100644 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/viewmodel/CSCViewModel.kt +++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/viewmodel/CSCViewModel.kt @@ -99,10 +99,15 @@ internal class CSCViewModel @Inject constructor( private fun handleResult(result: NavigationResult) { when (result) { is NavigationResult.Cancelled -> navigationManager.navigateUp() - is NavigationResult.Success -> repository.launch(result.value) + is NavigationResult.Success -> onDeviceSelected(result.value) } } + private fun onDeviceSelected(device: ServerDevice) { + _state.value = _state.value.copy(deviceName = device.name) + repository.launch(device) + } + fun onEvent(event: CSCViewEvent) { when (event) { is OnSelectedSpeedUnitSelected -> setSpeedUnit(event.selectedSpeedUnit) diff --git a/profile_hts/build.gradle.kts b/profile_hts/build.gradle.kts index 7b8423eb..66296ad1 100644 --- a/profile_hts/build.gradle.kts +++ b/profile_hts/build.gradle.kts @@ -55,6 +55,7 @@ dependencies { implementation(libs.nordic.uiscanner) implementation(libs.nordic.navigation) implementation(libs.nordic.uilogger) + implementation(libs.nordic.core) implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.compose.material.iconsExtended) diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSManager.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSManager.kt deleted file mode 100644 index 5d4e8cb2..00000000 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSManager.kt +++ /dev/null @@ -1,119 +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.hts.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.ht.TemperatureMeasurementResponse -import no.nordicsemi.android.ble.ktx.asValidResponseFlow -import no.nordicsemi.android.common.logger.NordicLogger -import no.nordicsemi.android.service.ConnectionObserverAdapter -import java.util.* - -val HTS_SERVICE_UUID: UUID = UUID.fromString("00001809-0000-1000-8000-00805f9b34fb") -private val HT_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A1C-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 HTSManager internal constructor( - context: Context, - private val scope: CoroutineScope, - private val logger: NordicLogger -) : BleManager(context) { - - private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null - private var htCharacteristic: BluetoothGattCharacteristic? = null - - private val data = MutableStateFlow(HTSData()) - 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 HTManagerGattCallback() - } - - private inner class HTManagerGattCallback : BleManagerGattCallback() { - override fun initialize() { - super.initialize() - - setIndicationCallback(htCharacteristic) - .asValidResponseFlow() - .onEach { - data.tryEmit(data.value.copy(temperatureValue = it.temperature)) - }.launchIn(scope) - enableIndications(htCharacteristic).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(HTS_SERVICE_UUID)?.run { - htCharacteristic = getCharacteristic(HT_MEASUREMENT_CHARACTERISTIC_UUID) - } - gatt.getService(BATTERY_SERVICE_UUID)?.run { - batteryLevelCharacteristic = getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID) - } - return htCharacteristic != null - } - - override fun onServicesInvalidated() { - htCharacteristic = null - batteryLevelCharacteristic = null - } - } -} 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/HTSServicesData.kt similarity index 86% rename from profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSData.kt rename to profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSServicesData.kt index 1bfb09f1..05398976 100644 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSData.kt +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSServicesData.kt @@ -31,7 +31,11 @@ package no.nordicsemi.android.hts.data -internal data class HTSData( - val temperatureValue: Float = 0f, +import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState +import no.nordicsemi.android.kotlin.ble.profile.hts.HTSData + +internal data class HTSServicesData( + 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 a6b88dd3..7b382b23 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 @@ -33,22 +33,18 @@ package no.nordicsemi.android.hts.repository 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.hts.data.HTSData -import no.nordicsemi.android.hts.data.HTSManager +import no.nordicsemi.android.hts.data.HTSServicesData 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.hts.HTSData +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,40 @@ import javax.inject.Singleton class HTSRepository @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: HTSManager? = null private var logger: NordicLogger? = null - private val _data = MutableStateFlow>(IdleResult()) + private val _data = MutableStateFlow(HTSServicesData()) 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(HTSService::class.java, device) } - fun start(device: ServerDevice, scope: CoroutineScope) { - val createdLogger = loggerFactory.create(stringConst.APP_NAME, "HTS", device.address).also { - logger = it - } - val manager = HTSManager(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 onHTSDataChanged(data: HTSData) { + _data.value = _data.value.copy(data = data) + } - scope.launch { - manager.start(device) - } + fun onBatteryLevelChanged(batteryLevel: Int) { + _data.value = _data.value.copy(batteryLevel = batteryLevel) } fun openLogger() { NordicLogger.launch(context, logger) } - private suspend fun HTSManager.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_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSService.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSService.kt index 05ff5dbb..bd308176 100644 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSService.kt +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSService.kt @@ -31,33 +31,94 @@ package no.nordicsemi.android.hts.repository +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.hts.HTSDataParser import no.nordicsemi.android.service.DEVICE_DATA import no.nordicsemi.android.service.NotificationService +import java.util.* import javax.inject.Inject +val HTS_SERVICE_UUID: UUID = UUID.fromString("00001809-0000-1000-8000-00805f9b34fb") +private val HTS_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A1C-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 HTSService : NotificationService() { @Inject lateinit var repository: HTSRepository + 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@HTSService) + + 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(HTS_SERVICE_UUID)!! + val htsMeasurementCharacteristic = htsService.findCharacteristic(HTS_MEASUREMENT_CHARACTERISTIC_UUID)!! + val batteryService = services.findService(BATTERY_SERVICE_UUID)!! + val batteryLevelCharacteristic = batteryService.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)!! + + batteryLevelCharacteristic.getNotifications() + .mapNotNull { BatteryLevelParser.parse(it) } + .onEach { repository.onBatteryLevelChanged(it) } + .launchIn(lifecycleScope) + + htsMeasurementCharacteristic.getNotifications() + .mapNotNull { HTSDataParser.parse(it) } + .onEach { repository.onHTSDataChanged(it) } + .launchIn(lifecycleScope) + } + + private fun stopIfDisconnected(connectionState: GattConnectionState) { + if (connectionState == GattConnectionState.STATE_DISCONNECTED) { + stopSelf() + } + } + + private fun disconnect() { + client.disconnect() + } } 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 4c2242b6..2b5abc5f 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.HTSData +import no.nordicsemi.android.hts.data.HTSServicesData 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: HTSData, temperatureUnit: TemperatureUnit, onEvent: (HTSScreenViewEvent) -> Unit) { +internal fun HTSContentView(state: HTSServicesData, temperatureUnit: TemperatureUnit, onEvent: (HTSScreenViewEvent) -> Unit) { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally @@ -76,7 +76,7 @@ internal fun HTSContentView(state: HTSData, temperatureUnit: TemperatureUnit, on KeyValueField( stringResource(id = R.string.hts_temperature), - displayTemperature(state.temperatureValue, temperatureUnit) + displayTemperature(state.data.temperature, temperatureUnit) ) } @@ -99,5 +99,5 @@ internal fun HTSContentView(state: HTSData, temperatureUnit: TemperatureUnit, on @Preview @Composable private fun Preview() { - HTSContentView(state = HTSData(), TemperatureUnit.CELSIUS) { } + HTSContentView(state = HTSServicesData(), TemperatureUnit.CELSIUS) { } } diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreen.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreen.kt index 60f506cf..9a406c24 100644 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreen.kt +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreen.kt @@ -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.hts.R import no.nordicsemi.android.hts.viewmodel.HTSViewModel -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 @@ -80,15 +72,12 @@ fun HTSScreen() { ) { when (state.htsManagerState) { NoDeviceState -> DeviceConnectingView() - is WorkingState -> when (state.htsManagerState.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 -> HTSContentView(state.htsManagerState.result.data, state.temperatureUnit) { viewModel.onEvent(it) } + is WorkingState -> when (state.htsManagerState.result.connectionState) { + null, + GattConnectionState.STATE_CONNECTING -> DeviceConnectingView { NavigateUpButton(navigateUp) } + GattConnectionState.STATE_DISCONNECTED, + GattConnectionState.STATE_DISCONNECTING -> DeviceDisconnectedView(Reason.UNKNOWN) { NavigateUpButton(navigateUp) } + GattConnectionState.STATE_CONNECTED -> HTSContentView(state.htsManagerState.result, state.temperatureUnit) { viewModel.onEvent(it) } } } } @@ -97,16 +86,11 @@ fun HTSScreen() { @Composable private fun AppBar(state: HTSViewState, navigateUp: () -> Unit, viewModel: HTSViewModel) { - val toolbarName = (state.htsManagerState as? WorkingState)?.let { - (it.result as? DeviceHolder)?.deviceName() - } - - if (toolbarName == null) { - BackIconAppBar(stringResource(id = R.string.hts_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.hts_title), navigateUp) } } - 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 3e05d629..d954ab67 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,16 +31,16 @@ package no.nordicsemi.android.hts.view -import no.nordicsemi.android.hts.data.HTSData -import no.nordicsemi.android.service.BleManagerResult +import no.nordicsemi.android.hts.data.HTSServicesData internal data class HTSViewState( val temperatureUnit: TemperatureUnit = TemperatureUnit.CELSIUS, - val htsManagerState: HTSManagerState = NoDeviceState + val htsManagerState: HTSManagerState = NoDeviceState, + val deviceName: String? = null ) internal sealed class HTSManagerState -internal data class WorkingState(val result: BleManagerResult) : HTSManagerState() +internal data class WorkingState(val result: HTSServicesData) : HTSManagerState() internal object NoDeviceState : HTSManagerState() diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/viewmodel/HTSViewModel.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/viewmodel/HTSViewModel.kt index d11c8f85..c5a77335 100644 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/viewmodel/HTSViewModel.kt +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/viewmodel/HTSViewModel.kt @@ -46,8 +46,8 @@ 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.hts.data.HTS_SERVICE_UUID import no.nordicsemi.android.hts.repository.HTSRepository +import no.nordicsemi.android.hts.repository.HTS_SERVICE_UUID import no.nordicsemi.android.hts.view.DisconnectEvent import no.nordicsemi.android.hts.view.HTSScreenViewEvent import no.nordicsemi.android.hts.view.HTSViewState @@ -56,7 +56,7 @@ import no.nordicsemi.android.hts.view.OnTemperatureUnitSelected import no.nordicsemi.android.hts.view.OpenLoggerEvent import no.nordicsemi.android.hts.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 @@ -80,7 +80,7 @@ internal class HTSViewModel @Inject constructor( repository.data.onEach { _state.value = _state.value.copy(htsManagerState = WorkingState(it)) - (it as? ConnectedResult)?.let { + if (it.connectionState == GattConnectionState.STATE_CONNECTED) { analytics.logEvent(ProfileConnectedEvent(Profile.HTS)) } }.launchIn(viewModelScope) @@ -97,10 +97,15 @@ internal class HTSViewModel @Inject constructor( private fun handleResult(result: NavigationResult) { when (result) { is NavigationResult.Cancelled -> navigationManager.navigateUp() - is NavigationResult.Success -> repository.launch(result.value) + is NavigationResult.Success -> onDeviceSelected(result.value) } } + private fun onDeviceSelected(device: ServerDevice) { + _state.value = _state.value.copy(deviceName = device.name) + repository.launch(device) + } + fun onEvent(event: HTSScreenViewEvent) { when (event) { DisconnectEvent -> disconnect()