diff --git a/profile_prx/build.gradle.kts b/profile_prx/build.gradle.kts index ade0bcee..58cea4fe 100644 --- a/profile_prx/build.gradle.kts +++ b/profile_prx/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_prx/src/main/java/no/nordicsemi/android/prx/data/PRXData.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXData.kt deleted file mode 100644 index 1488297d..00000000 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXData.kt +++ /dev/null @@ -1,52 +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.prx.data - -internal data class PRXData( - val batteryLevel: Int? = null, - val localAlarmLevel: AlarmLevel = AlarmLevel.NONE, - val isRemoteAlarm: Boolean = false, - val linkLossAlarmLevel: AlarmLevel = AlarmLevel.HIGH -) - -internal enum class AlarmLevel(val value: Int) { - NONE(0x00), - MEDIUM(0x01), - HIGH(0x02); - - companion object { - fun create(value: Int): AlarmLevel { - return AlarmLevel.values().firstOrNull { it.value == value } - ?: throw IllegalArgumentException("Cannot find AlarmLevel for provided value: $value") - } - } -} diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXManager.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXManager.kt index 292b2bf8..d5a11630 100644 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXManager.kt +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXManager.kt @@ -47,6 +47,7 @@ import no.nordicsemi.android.ble.common.data.alert.AlertLevelData import no.nordicsemi.android.ble.ktx.asValidResponseFlow import no.nordicsemi.android.ble.ktx.suspend import no.nordicsemi.android.common.logger.NordicLogger +import no.nordicsemi.android.kotlin.ble.profile.prx.AlarmLevel import no.nordicsemi.android.service.ConnectionObserverAdapter import no.nordicsemi.android.utils.launchWithCatch import java.util.* diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXServiceData.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXServiceData.kt new file mode 100644 index 00000000..2c520815 --- /dev/null +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXServiceData.kt @@ -0,0 +1,13 @@ +package no.nordicsemi.android.prx.data + +import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState +import no.nordicsemi.android.kotlin.ble.profile.prx.AlarmLevel + +data class PRXServiceData( + val localAlarmLevel: AlarmLevel = AlarmLevel.NONE, + val linkLossAlarmLevel: AlarmLevel = AlarmLevel.HIGH, + val batteryLevel: Int? = null, + val connectionState: GattConnectionState? = null, + val isRemoteAlarm: Boolean = false, + val deviceName: String? = null +) diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXRepository.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXRepository.kt index 20b4c13e..c420435a 100644 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXRepository.kt +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXRepository.kt @@ -33,21 +33,22 @@ package no.nordicsemi.android.prx.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 no.nordicsemi.android.common.core.simpleSharedFlow import no.nordicsemi.android.common.logger.NordicLogger import no.nordicsemi.android.common.logger.NordicLoggerFactory import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.prx.data.AlarmLevel -import no.nordicsemi.android.prx.data.PRXData -import no.nordicsemi.android.prx.data.PRXManager +import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState +import no.nordicsemi.android.kotlin.ble.profile.hrs.data.HRSData +import no.nordicsemi.android.kotlin.ble.profile.prx.AlarmLevel +import no.nordicsemi.android.kotlin.ble.profile.prx.PRXData +import no.nordicsemi.android.prx.data.PRXServiceData import no.nordicsemi.android.prx.data.ProximityServerManager import no.nordicsemi.android.service.BleManagerResult -import no.nordicsemi.android.service.IdleResult +import no.nordicsemi.android.service.DisconnectAndStopEvent import no.nordicsemi.android.service.LinkLossResult import no.nordicsemi.android.service.ServiceManager import no.nordicsemi.android.service.SuccessResult @@ -66,37 +67,24 @@ class PRXRepository @Inject internal constructor( private val stringConst: StringConst ) { - private var manager: PRXManager? = null - private var logger: NordicLogger? = null - - private val _data = MutableStateFlow>(IdleResult()) + private val _data = MutableStateFlow(PRXServiceData()) internal val data = _data.asStateFlow() - val isRunning = data.map { it.isRunning() } - val hasBeenDisconnectedWithoutLinkLoss = data.map { it.hasBeenDisconnectedWithoutLinkLoss() } + private val _stopEvent = simpleSharedFlow() + internal val stopEvent = _stopEvent.asSharedFlow() + + val isRunning = data.map { it.connectionState == GattConnectionState.STATE_CONNECTED } fun launch(device: ServerDevice) { serviceManager.startService(PRXService::class.java, device) - proximityServerManager.open() } - fun start(device: ServerDevice, scope: CoroutineScope) { - val createdLogger = loggerFactory.create(stringConst.APP_NAME, "PRX", device.address).also { - logger = it - } - val manager = PRXManager(context, scope, createdLogger) - this.manager = manager - manager.useServer(proximityServerManager) + fun onInitComplete(device: ServerDevice) { + _data.value = _data.value.copy(deviceName = device.name) + } - manager.dataHolder.status.onEach { - _data.value = it - handleLocalAlarm(it) - }.launchIn(scope) - -// manager.connect(device.device) -// .useAutoConnect(true) -// .retry(3, 100) -// .enqueue() + fun onConnectionStateChanged(connectionState: GattConnectionState?) { + _data.value = _data.value.copy(connectionState = connectionState) } private fun handleLocalAlarm(result: BleManagerResult) { @@ -113,6 +101,10 @@ class PRXRepository @Inject internal constructor( } } + fun onBatteryLevelChanged(batteryLevel: Int) { + _data.value = _data.value.copy(batteryLevel = batteryLevel) + } + fun enableAlarm() { manager?.writeImmediateAlert(true) } @@ -127,8 +119,6 @@ class PRXRepository @Inject internal constructor( fun release() { disableAlarm() - manager?.disconnect()?.enqueue() - manager = null - logger = null + _stopEvent.tryEmit(DisconnectAndStopEvent()) } } diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXService.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXService.kt index 9925bf4b..7dbf20b7 100644 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXService.kt +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXService.kt @@ -31,33 +31,133 @@ package no.nordicsemi.android.prx.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.BleGattCharacteristic +import no.nordicsemi.android.kotlin.ble.core.client.service.BleGattServices +import no.nordicsemi.android.kotlin.ble.core.data.BleGattPermission +import no.nordicsemi.android.kotlin.ble.core.data.BleGattProperty +import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState +import no.nordicsemi.android.kotlin.ble.core.server.BleGattServer +import no.nordicsemi.android.kotlin.ble.core.server.service.service.BleGattServerServiceType +import no.nordicsemi.android.kotlin.ble.core.server.service.service.BleServerGattCharacteristicConfig +import no.nordicsemi.android.kotlin.ble.core.server.service.service.BleServerGattServiceConfig +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 PRX_SERVICE_UUID = UUID.fromString("00001802-0000-1000-8000-00805f9b34fb") +private val LINK_LOSS_SERVICE_UUID = UUID.fromString("00001803-0000-1000-8000-00805f9b34fb") +private val ALERT_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A06-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 PRXService : NotificationService() { @Inject lateinit var repository: PRXRepository + private lateinit var client: BleGattClient + + private lateinit var alertLevelCharacteristic: BleGattCharacteristic + 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.hasBeenDisconnectedWithoutLinkLoss.onEach { - if (it) stopSelf() - }.launchIn(lifecycleScope) + repository.stopEvent + .onEach { disconnect() } + .launchIn(lifecycleScope) return START_REDELIVER_INTENT } + + private fun startServer() { + val alertLevelCharacteristic = BleServerGattCharacteristicConfig( + ALERT_LEVEL_CHARACTERISTIC_UUID, + listOf(BleGattProperty.PROPERTY_WRITE_NO_RESPONSE), + listOf(BleGattPermission.PERMISSION_WRITE) + ) + + val linkLossCharacteristic = BleServerGattCharacteristicConfig( + LINK_LOSS_SERVICE_UUID, + listOf(BleGattProperty.PROPERTY_WRITE, BleGattProperty.PROPERTY_READ), + listOf(BleGattPermission.PERMISSION_WRITE, BleGattPermission.PERMISSION_READ) + ) + + val serviceConfig = BleServerGattServiceConfig( + PRX_SERVICE_UUID, + BleGattServerServiceType.SERVICE_TYPE_PRIMARY, + listOf(alertLevelCharacteristic, linkLossCharacteristic) + ) + + val server = BleGattServer.create(this@PRXService, serviceConfig) + + TODO("Initialize characteristic with value") + } + + private fun startGattClient(device: ServerDevice) = lifecycleScope.launch { + client = device.connect(this@PRXService) + + client.connectionState + .onEach { repository.onConnectionStateChanged(it) } + .filterNotNull() + .onEach { stopIfDisconnected(it) } + .launchIn(lifecycleScope) + + client.services + .filterNotNull() + .onEach { configureGatt(it, device) } + .launchIn(lifecycleScope) + } + + private suspend fun configureGatt(services: BleGattServices, device: ServerDevice) { + val prxService = services.findService(PRX_SERVICE_UUID)!! + alertLevelCharacteristic = prxService.findCharacteristic(ALERT_LEVEL_CHARACTERISTIC_UUID)!! + val linkLossCharacteristic = prxService.findCharacteristic(LINK_LOSS_SERVICE_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) + + linkLossCharacteristic.write(Alert) + + htsMeasurementCharacteristic.getNotifications() + .mapNotNull { HTSDataParser.parse(it) } + .onEach { repository.onHTSDataChanged(it) } + .launchIn(lifecycleScope) + + repository.onInitComplete(device) + } + + private fun stopIfDisconnected(connectionState: GattConnectionState) { + if (connectionState == GattConnectionState.STATE_DISCONNECTED) { + stopSelf() + } + } + + private fun disconnect() { + client.disconnect() + } } diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXContentView.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXContentView.kt index e47fa8b1..16fd4a71 100644 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXContentView.kt +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXContentView.kt @@ -44,7 +44,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import no.nordicsemi.android.prx.R -import no.nordicsemi.android.prx.data.PRXData import no.nordicsemi.android.ui.view.BatteryLevelView import no.nordicsemi.android.ui.view.KeyValueField import no.nordicsemi.android.ui.view.ScreenSection diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXState.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXState.kt deleted file mode 100644 index c95e4004..00000000 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXState.kt +++ /dev/null @@ -1,41 +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.prx.view - -import no.nordicsemi.android.prx.data.PRXData -import no.nordicsemi.android.service.BleManagerResult - -internal sealed class PRXViewState - -internal data class WorkingState(val result: BleManagerResult) : PRXViewState() - -internal object NoDeviceState : PRXViewState() diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/viewmodel/PRXViewModel.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/viewmodel/PRXViewModel.kt index 6acfe572..093be6e7 100644 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/viewmodel/PRXViewModel.kt +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/viewmodel/PRXViewModel.kt @@ -35,8 +35,6 @@ import android.os.ParcelUuid import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -47,18 +45,15 @@ import no.nordicsemi.android.analytics.ProfileConnectedEvent import no.nordicsemi.android.common.navigation.NavigationResult import no.nordicsemi.android.common.navigation.Navigator import no.nordicsemi.android.kotlin.ble.core.ServerDevice +import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState import no.nordicsemi.android.prx.data.PRX_SERVICE_UUID import no.nordicsemi.android.prx.repository.PRXRepository import no.nordicsemi.android.prx.view.DisconnectEvent import no.nordicsemi.android.prx.view.NavigateUpEvent -import no.nordicsemi.android.prx.view.NoDeviceState import no.nordicsemi.android.prx.view.OpenLoggerEvent import no.nordicsemi.android.prx.view.PRXScreenViewEvent -import no.nordicsemi.android.prx.view.PRXViewState import no.nordicsemi.android.prx.view.TurnOffAlert import no.nordicsemi.android.prx.view.TurnOnAlert -import no.nordicsemi.android.prx.view.WorkingState -import no.nordicsemi.android.service.ConnectedResult import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId import javax.inject.Inject @@ -69,8 +64,7 @@ internal class PRXViewModel @Inject constructor( private val analytics: AppAnalytics ) : ViewModel() { - private val _state = MutableStateFlow(NoDeviceState) - val state = _state.asStateFlow() + val state = repository.data init { viewModelScope.launch { @@ -80,9 +74,7 @@ internal class PRXViewModel @Inject constructor( } repository.data.onEach { - _state.value = WorkingState(it) - - (it as? ConnectedResult)?.let { + if (it.connectionState == GattConnectionState.STATE_CONNECTED) { analytics.logEvent(ProfileConnectedEvent(Profile.PRX)) } }.launchIn(viewModelScope)