Change PRX module

This commit is contained in:
Sylwester Zieliński
2022-02-14 15:24:28 +01:00
parent 382208454f
commit f92e1b4adb
15 changed files with 277 additions and 196 deletions

View File

@@ -1,6 +1,6 @@
#Wed Sep 08 10:36:20 CEST 2021 #Mon Feb 14 14:46:55 CET 2022
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-bin.zip

View File

@@ -6,4 +6,5 @@ import no.nordicsemi.android.service.BleManagerResult
internal sealed class HRSViewState internal sealed class HRSViewState
internal data class WorkingState(val result: BleManagerResult<HRSData>) : HRSViewState() internal data class WorkingState(val result: BleManagerResult<HRSData>) : HRSViewState()
internal object NoDeviceState : HRSViewState() internal object NoDeviceState : HRSViewState()

View File

@@ -89,7 +89,7 @@ internal class HTSManager internal constructor(
gatt.getService(BATTERY_SERVICE_UUID)?.run { gatt.getService(BATTERY_SERVICE_UUID)?.run {
batteryLevelCharacteristic = getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID) batteryLevelCharacteristic = getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)
} }
return htCharacteristic != null return htCharacteristic != null && batteryLevelCharacteristic != null
} }
override fun onServicesInvalidated() { override fun onServicesInvalidated() {

View File

@@ -3,9 +3,11 @@ package no.nordicsemi.android.hts.viewmodel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.hts.data.HTSRepository import no.nordicsemi.android.hts.data.HTSRepository
import no.nordicsemi.android.hts.repository.HTSService
import no.nordicsemi.android.hts.repository.HTS_SERVICE_UUID import no.nordicsemi.android.hts.repository.HTS_SERVICE_UUID
import no.nordicsemi.android.hts.view.* import no.nordicsemi.android.hts.view.*
import no.nordicsemi.android.navigation.* import no.nordicsemi.android.navigation.*

View File

@@ -13,6 +13,7 @@ dependencies {
implementation libs.nordic.ui.scanner implementation libs.nordic.ui.scanner
implementation libs.nordic.navigation implementation libs.nordic.navigation
implementation libs.bundles.icons
implementation libs.bundles.compose implementation libs.bundles.compose
implementation libs.androidx.core implementation libs.androidx.core
implementation libs.material implementation libs.material

View File

@@ -1,62 +1,97 @@
package no.nordicsemi.android.prx.data package no.nordicsemi.android.prx.data
import kotlinx.coroutines.channels.BufferOverflow import android.bluetooth.BluetoothDevice
import kotlinx.coroutines.flow.* import android.content.Context
import no.nordicsemi.android.service.BleManagerStatus import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import no.nordicsemi.android.ble.ktx.suspend
import no.nordicsemi.android.prx.repository.AlarmHandler
import no.nordicsemi.android.prx.repository.PRXManager
import no.nordicsemi.android.prx.repository.PRXService
import no.nordicsemi.android.prx.repository.ProximityServerManager
import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.service.ConnectingResult
import no.nordicsemi.android.service.ServiceManager
import no.nordicsemi.android.service.SuccessResult
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
internal class PRXRepository @Inject constructor() { internal class PRXRepository @Inject constructor(
@ApplicationContext
private val context: Context,
private val serviceManager: ServiceManager,
private val proximityServerManager: ProximityServerManager,
private val alarmHandler: AlarmHandler
) {
private val _data = MutableStateFlow(PRXData()) private var manager: PRXManager? = null
val data: StateFlow<PRXData> = _data
private val _command = MutableSharedFlow<PRXCommand>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) private val _data = MutableStateFlow<BleManagerResult<PRXData>>(ConnectingResult())
val command = _command.asSharedFlow() internal val data = _data.asStateFlow()
private val _status = MutableStateFlow(BleManagerStatus.CONNECTING) private val _isRunning = MutableStateFlow(false)
val status = _status.asStateFlow() val isRunning = _isRunning.asStateFlow()
fun setBatteryLevel(batteryLevel: Int) { fun launch(device: BluetoothDevice) {
_data.tryEmit(_data.value.copy(batteryLevel = batteryLevel)) serviceManager.startService(PRXService::class.java, device)
proximityServerManager.open()
} }
fun setLocalAlarmLevel(value: Int) { fun start(device: BluetoothDevice, scope: CoroutineScope) {
val alarmLevel = AlarmLevel.create(value) val manager = PRXManager(context, scope)
_data.tryEmit(_data.value.copy(localAlarmLevel = alarmLevel)) this.manager = manager
manager.useServer(proximityServerManager)
manager.dataHolder.status.onEach {
_data.value = it
handleLocalAlarm(it)
}.launchIn(scope)
scope.launch {
manager.start(device)
}
} }
fun setLocalAlarmLevel(alarmLevel: AlarmLevel) { private suspend fun PRXManager.start(device: BluetoothDevice) {
_data.tryEmit(_data.value.copy(localAlarmLevel = alarmLevel)) try {
connect(device)
.useAutoConnect(false)
.retry(3, 100)
.suspend()
_isRunning.value = true
} catch (e: Exception) {
e.printStackTrace()
}
} }
fun setLinkLossLevel(value: Int) { private fun handleLocalAlarm(result: BleManagerResult<PRXData>) {
val alarmLevel = AlarmLevel.create(value) (result as? SuccessResult<PRXData>)?.let {
_data.tryEmit(_data.value.copy(linkLossAlarmLevel = alarmLevel)) if (it.data.localAlarmLevel != AlarmLevel.NONE) {
} alarmHandler.playAlarm(it.data.localAlarmLevel)
fun setRemoteAlarmLevel(isOn: Boolean) {
_data.tryEmit(_data.value.copy(isRemoteAlarm = isOn))
}
fun invokeCommand(command: PRXCommand) {
if (command == Disconnect) {
_command.tryEmit(command)
_status.tryEmit(BleManagerStatus.DISCONNECTED)
} else if (_command.subscriptionCount.value > 0) {
_command.tryEmit(command)
} else { } else {
_status.tryEmit(BleManagerStatus.DISCONNECTED) alarmHandler.pauseAlarm()
}
} }
} }
fun setNewStatus(status: BleManagerStatus) { fun enableAlarm() {
_status.value = status manager?.writeImmediateAlert(true)
} }
fun clear() { fun disableAlarm() {
_status.value = BleManagerStatus.CONNECTING manager?.writeImmediateAlert(false)
_data.tryEmit(PRXData()) }
fun release() {
serviceManager.stopService(PRXService::class.java)
manager?.disconnect()?.enqueue()
manager = null
_isRunning.value = false
} }
} }

View File

@@ -26,65 +26,84 @@ import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattServer import android.bluetooth.BluetoothGattServer
import android.content.Context import android.content.Context
import android.util.Log
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch 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.alert.AlertLevelDataCallback import no.nordicsemi.android.ble.common.callback.alert.AlertLevelDataCallback
import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelResponse
import no.nordicsemi.android.ble.common.data.alert.AlertLevelData 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.ble.ktx.suspend
import no.nordicsemi.android.prx.data.PRXRepository import no.nordicsemi.android.prx.data.AlarmLevel
import no.nordicsemi.android.service.BatteryManager import no.nordicsemi.android.prx.data.PRXData
import no.nordicsemi.android.service.ConnectionObserverAdapter
import no.nordicsemi.android.utils.launchWithCatch
import java.util.* import java.util.*
val LINK_LOSS_SERVICE_UUID = UUID.fromString("00001803-0000-1000-8000-00805f9b34fb")
val PRX_SERVICE_UUID = UUID.fromString("00001802-0000-1000-8000-00805f9b34fb") val PRX_SERVICE_UUID = UUID.fromString("00001802-0000-1000-8000-00805f9b34fb")
val LINK_LOSS_SERVICE_UUID = UUID.fromString("00001803-0000-1000-8000-00805f9b34fb")
val ALERT_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A06-0000-1000-8000-00805f9b34fb") 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")
internal class PRXManager( internal class PRXManager(
context: Context, context: Context,
scope: CoroutineScope, private val scope: CoroutineScope,
private val dataHolder: PRXRepository ) : BleManager(context) {
) : BatteryManager(context, scope) {
private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null
private var alertLevelCharacteristic: BluetoothGattCharacteristic? = null private var alertLevelCharacteristic: BluetoothGattCharacteristic? = null
private var linkLossCharacteristic: BluetoothGattCharacteristic? = null private var linkLossCharacteristic: BluetoothGattCharacteristic? = null
private var localAlertLevelCharacteristic: BluetoothGattCharacteristic? = null private var localAlertLevelCharacteristic: BluetoothGattCharacteristic? = null
private var linkLossServerCharacteristic: BluetoothGattCharacteristic? = null private var linkLossServerCharacteristic: BluetoothGattCharacteristic? = null
private val exceptionHandler = CoroutineExceptionHandler { _, t-> private var isAlertEnabled = false
Log.e("COROUTINE-EXCEPTION", "Uncaught exception", t)
private val data = MutableStateFlow(PRXData())
val dataHolder = ConnectionObserverAdapter<PRXData>()
init {
setConnectionObserver(dataHolder)
data.onEach {
dataHolder.setValue(it)
}.launchIn(scope)
} }
var isAlertEnabled = false private inner class ProximityManagerGattCallback : BleManagerGattCallback() {
private set
private inner class ProximityManagerGattCallback : BatteryManagerGattCallback() {
override fun initialize() { override fun initialize() {
super.initialize() super.initialize()
setWriteCallback(localAlertLevelCharacteristic) setWriteCallback(localAlertLevelCharacteristic)
.with(object : AlertLevelDataCallback() { .with(object : AlertLevelDataCallback() {
override fun onAlertLevelChanged(device: BluetoothDevice, level: Int) { override fun onAlertLevelChanged(device: BluetoothDevice, level: Int) {
dataHolder.setLocalAlarmLevel(level) data.value = data.value.copy(localAlarmLevel = AlarmLevel.create(level))
} }
}) })
setWriteCallback(linkLossServerCharacteristic) setWriteCallback(linkLossServerCharacteristic)
.with(object : AlertLevelDataCallback() { .with(object : AlertLevelDataCallback() {
override fun onAlertLevelChanged(device: BluetoothDevice, level: Int) { override fun onAlertLevelChanged(device: BluetoothDevice, level: Int) {
dataHolder.setLinkLossLevel(level) data.value = data.value.copy(linkLossAlarmLevel = AlarmLevel.create(level))
} }
}) })
scope.launch(exceptionHandler) {
writeCharacteristic( writeCharacteristic(
linkLossCharacteristic, linkLossCharacteristic,
AlertLevelData.highAlert(), AlertLevelData.highAlert(),
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
).suspend() ).enqueue()
}
setNotificationCallback(batteryLevelCharacteristic)
.asValidResponseFlow<BatteryLevelResponse>()
.onEach {
data.value = data.value.copy(batteryLevel = it.batteryLevel)
}.launchIn(scope)
enableNotifications(batteryLevelCharacteristic).enqueue()
} }
override fun onServerReady(server: BluetoothGattServer) { override fun onServerReady(server: BluetoothGattServer) {
@@ -98,29 +117,26 @@ internal class PRXManager(
} }
} }
override fun onServicesInvalidated() { }
override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean { override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
val llService = gatt.getService(LINK_LOSS_SERVICE_UUID) gatt.getService(LINK_LOSS_SERVICE_UUID)?.run {
if (llService != null) { linkLossCharacteristic = getCharacteristic(ALERT_LEVEL_CHARACTERISTIC_UUID)
linkLossCharacteristic = llService.getCharacteristic(ALERT_LEVEL_CHARACTERISTIC_UUID) }
gatt.getService(BATTERY_SERVICE_UUID)?.run {
batteryLevelCharacteristic = getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)
} }
return linkLossCharacteristic != null return linkLossCharacteristic != null
} }
override fun isOptionalServiceSupported(gatt: BluetoothGatt): Boolean { override fun isOptionalServiceSupported(gatt: BluetoothGatt): Boolean {
super.isOptionalServiceSupported(gatt) super.isOptionalServiceSupported(gatt)
val iaService = gatt.getService(PRX_SERVICE_UUID) gatt.getService(PRX_SERVICE_UUID)?.run {
if (iaService != null) { alertLevelCharacteristic = getCharacteristic(ALERT_LEVEL_CHARACTERISTIC_UUID)
alertLevelCharacteristic = iaService.getCharacteristic(
ALERT_LEVEL_CHARACTERISTIC_UUID
)
} }
return alertLevelCharacteristic != null return alertLevelCharacteristic != null && batteryLevelCharacteristic != null
} }
override fun onDeviceDisconnected() { override fun onServicesInvalidated() {
super.onDeviceDisconnected() batteryLevelCharacteristic = null
alertLevelCharacteristic = null alertLevelCharacteristic = null
linkLossCharacteristic = null linkLossCharacteristic = null
localAlertLevelCharacteristic = null localAlertLevelCharacteristic = null
@@ -131,7 +147,7 @@ internal class PRXManager(
fun writeImmediateAlert(on: Boolean) { fun writeImmediateAlert(on: Boolean) {
if (!isConnected) return if (!isConnected) return
scope.launch(exceptionHandler) { scope.launchWithCatch {
writeCharacteristic( writeCharacteristic(
alertLevelCharacteristic, alertLevelCharacteristic,
if (on) AlertLevelData.highAlert() else AlertLevelData.noAlert(), if (on) AlertLevelData.highAlert() else AlertLevelData.noAlert(),
@@ -139,14 +155,10 @@ internal class PRXManager(
).suspend() ).suspend()
isAlertEnabled = on isAlertEnabled = on
dataHolder.setRemoteAlarmLevel(on) data.value = data.value.copy(isRemoteAlarm = on)
} }
} }
override fun onBatteryLevelChanged(batteryLevel: Int) {
dataHolder.setBatteryLevel(batteryLevel)
}
override fun getGattCallback(): BleManagerGattCallback { override fun getGattCallback(): BleManagerGattCallback {
return ProximityManagerGattCallback() return ProximityManagerGattCallback()
} }

View File

@@ -1,77 +1,27 @@
package no.nordicsemi.android.prx.repository package no.nordicsemi.android.prx.repository
import android.bluetooth.BluetoothDevice
import android.content.Intent
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn import no.nordicsemi.android.prx.data.PRXRepository
import kotlinx.coroutines.flow.onEach import no.nordicsemi.android.service.DEVICE_DATA
import no.nordicsemi.android.prx.data.* import no.nordicsemi.android.service.NotificationService
import no.nordicsemi.android.service.ForegroundBleService
import no.nordicsemi.android.utils.exhaustive
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
internal class PRXService : ForegroundBleService() { internal class PRXService : NotificationService() {
@Inject @Inject
lateinit var repository: PRXRepository lateinit var repository: PRXRepository
@Inject override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
lateinit var alarmHandler: AlarmHandler super.onStartCommand(intent, flags, startId)
private var serverManager: ProximityServerManager = ProximityServerManager(this) val device = intent!!.getParcelableExtra<BluetoothDevice>(DEVICE_DATA)!!
override val manager: PRXManager by lazy { repository.start(device, lifecycleScope)
PRXManager(this, scope, repository).apply {
useServer(serverManager) return START_REDELIVER_INTENT
}
}
override fun onCreate() {
super.onCreate()
serverManager.open()
// status.onEach {
// val bleStatus = when (it) {
// BleServiceStatus.CONNECTING -> BleManagerStatus.CONNECTING
// BleServiceStatus.OK -> BleManagerStatus.OK
// BleServiceStatus.DISCONNECTED -> {
// scope.close()
// stopSelf()
// BleManagerStatus.DISCONNECTED
// }
// BleServiceStatus.LINK_LOSS -> null
// }.exhaustive
// bleStatus?.let { repository.setNewStatus(it) }
//
// if (BleServiceStatus.LINK_LOSS == it) {
// repository.setLocalAlarmLevel(repository.data.value.linkLossAlarmLevel)
// }
// }.launchIn(scope)
repository.command.onEach {
when (it) {
DisableAlarm -> manager.writeImmediateAlert(false)
EnableAlarm -> manager.writeImmediateAlert(true)
Disconnect -> stopSelf()
}.exhaustive
}.launchIn(scope)
repository.data.onEach {
if (it.localAlarmLevel != AlarmLevel.NONE) {
alarmHandler.playAlarm(it.localAlarmLevel)
} else {
alarmHandler.pauseAlarm()
}
}.launchIn(scope)
}
override fun shouldAutoConnect(): Boolean {
return true
}
override fun onDestroy() {
super.onDestroy()
alarmHandler.releaseAlarm()
serverManager.close()
} }
} }

View File

@@ -25,11 +25,16 @@ import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattService import android.bluetooth.BluetoothGattService
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import dagger.hilt.android.qualifiers.ApplicationContext
import no.nordicsemi.android.ble.BleServerManager import no.nordicsemi.android.ble.BleServerManager
import no.nordicsemi.android.ble.common.data.alert.AlertLevelData import no.nordicsemi.android.ble.common.data.alert.AlertLevelData
import java.util.* import javax.inject.Inject
internal class ProximityServerManager @Inject constructor(
@ApplicationContext
context: Context
) : BleServerManager(context) {
internal class ProximityServerManager(context: Context) : BleServerManager(context) {
override fun log(priority: Int, message: String) { override fun log(priority: Int, message: String) {
Log.println(priority, "BleManager", message) Log.println(priority, "BleManager", message)
} }

View File

@@ -0,0 +1,65 @@
package no.nordicsemi.android.prx.view
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.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.text.style.TextAlign
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.prx.R
import no.nordicsemi.android.theme.R as themeR
import no.nordicsemi.android.theme.view.ScreenSection
import androidx.compose.material.icons.filled.HighlightOff
@Composable
fun DeviceOutOfRangeView(navigateUp: () -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
ScreenSection {
Icon(
imageVector = Icons.Default.HighlightOff,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSecondary,
modifier = Modifier
.background(
color = MaterialTheme.colorScheme.secondary,
shape = CircleShape
)
.padding(8.dp)
)
Spacer(modifier = Modifier.size(16.dp))
Text(
text = stringResource(id = R.string.prx_device_out_of_range),
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.size(16.dp))
Text(
text = stringResource(id = R.string.prx_device_out_of_range_reason),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium
)
}
Spacer(modifier = Modifier.size(16.dp))
Button(onClick = { navigateUp() }) {
Text(text = stringResource(id = themeR.string.disconnect))
}
}
}

View File

@@ -11,7 +11,13 @@ import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.prx.R import no.nordicsemi.android.prx.R
import no.nordicsemi.android.prx.viewmodel.PRXViewModel import no.nordicsemi.android.prx.viewmodel.PRXViewModel
import no.nordicsemi.android.service.*
import no.nordicsemi.android.theme.view.BackIconAppBar import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.scanner.DeviceConnectingView
import no.nordicsemi.android.theme.view.scanner.DeviceDisconnectedView
import no.nordicsemi.android.theme.view.scanner.NoDeviceView
import no.nordicsemi.android.theme.view.scanner.Reason
import no.nordicsemi.android.utils.exhaustive
@Composable @Composable
fun PRXScreen() { fun PRXScreen() {
@@ -19,15 +25,22 @@ fun PRXScreen() {
val state = viewModel.state.collectAsState().value val state = viewModel.state.collectAsState().value
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
BackIconAppBar(stringResource(id = R.string.prx_title)) { val navigateUp = { viewModel.onEvent(NavigateUpEvent) }
viewModel.onEvent(DisconnectEvent)
} BackIconAppBar(stringResource(id = R.string.prx_title), navigateUp)
Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
// when (state) { when (state) {
// is DisplayDataState -> ContentView(state.data) { viewModel.onEvent(it) } NoDeviceState -> NoDeviceView()
// LoadingState -> DeviceConnectingView() is WorkingState -> when (state.result) {
// }.exhaustive is ConnectingResult,
is ReadyResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) }
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp)
is LinkLossResult -> DeviceOutOfRangeView { viewModel.onEvent(DisconnectEvent) }
is MissingServiceResult -> DeviceDisconnectedView(Reason.MISSING_SERVICE, navigateUp)
is SuccessResult -> ContentView(state.result.data) { viewModel.onEvent(it) }
}
}.exhaustive
} }
} }
} }

View File

@@ -2,6 +2,8 @@ package no.nordicsemi.android.prx.view
internal sealed class PRXScreenViewEvent internal sealed class PRXScreenViewEvent
internal object NavigateUpEvent : PRXScreenViewEvent()
internal object TurnOnAlert : PRXScreenViewEvent() internal object TurnOnAlert : PRXScreenViewEvent()
internal object TurnOffAlert : PRXScreenViewEvent() internal object TurnOffAlert : PRXScreenViewEvent()

View File

@@ -1,9 +1,10 @@
package no.nordicsemi.android.prx.view package no.nordicsemi.android.prx.view
import no.nordicsemi.android.prx.data.PRXData import no.nordicsemi.android.prx.data.PRXData
import no.nordicsemi.android.service.BleManagerResult
internal sealed class PRXViewState internal sealed class PRXViewState
internal object LoadingState : PRXViewState() internal data class WorkingState(val result: BleManagerResult<PRXData>) : PRXViewState()
internal data class DisplayDataState(val data: PRXData) : PRXViewState() internal object NoDeviceState : PRXViewState()

View File

@@ -3,17 +3,14 @@ package no.nordicsemi.android.prx.viewmodel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.navigation.* import no.nordicsemi.android.navigation.*
import no.nordicsemi.android.prx.data.DisableAlarm
import no.nordicsemi.android.prx.data.Disconnect
import no.nordicsemi.android.prx.data.EnableAlarm
import no.nordicsemi.android.prx.data.PRXRepository import no.nordicsemi.android.prx.data.PRXRepository
import no.nordicsemi.android.prx.repository.PRXService
import no.nordicsemi.android.prx.repository.PRX_SERVICE_UUID import no.nordicsemi.android.prx.repository.PRX_SERVICE_UUID
import no.nordicsemi.android.prx.view.* import no.nordicsemi.android.prx.view.*
import no.nordicsemi.android.service.BleManagerStatus
import no.nordicsemi.android.service.ServiceManager
import no.nordicsemi.android.utils.exhaustive import no.nordicsemi.android.utils.exhaustive
import no.nordicsemi.android.utils.getDevice import no.nordicsemi.android.utils.getDevice
import no.nordicsemi.ui.scanner.ScannerDestinationId import no.nordicsemi.ui.scanner.ScannerDestinationId
@@ -22,19 +19,23 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
internal class PRXViewModel @Inject constructor( internal class PRXViewModel @Inject constructor(
private val repository: PRXRepository, private val repository: PRXRepository,
private val serviceManager: ServiceManager,
private val navigationManager: NavigationManager private val navigationManager: NavigationManager
) : ViewModel() { ) : ViewModel() {
val state = repository.data.combine(repository.status) { data, status -> private val _state = MutableStateFlow<PRXViewState>(NoDeviceState)
// when (status) { val state = _state.asStateFlow()
// BleManagerStatus.CONNECTING -> LoadingState
// BleManagerStatus.OK,
// BleManagerStatus.DISCONNECTED -> DisplayDataState(data)
// }
}.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState)
init { init {
if (!repository.isRunning.value) {
requestBluetoothDevice()
}
repository.data.onEach {
_state.value = WorkingState(it)
}.launchIn(viewModelScope)
}
private fun requestBluetoothDevice() {
navigationManager.navigateTo(ScannerDestinationId, UUIDArgument(PRX_SERVICE_UUID)) navigationManager.navigateTo(ScannerDestinationId, UUIDArgument(PRX_SERVICE_UUID))
navigationManager.recentResult.onEach { navigationManager.recentResult.onEach {
@@ -42,36 +43,26 @@ internal class PRXViewModel @Inject constructor(
handleArgs(it) handleArgs(it)
} }
}.launchIn(viewModelScope) }.launchIn(viewModelScope)
repository.status.onEach {
if (it == BleManagerStatus.DISCONNECTED) {
navigationManager.navigateUp()
}
}.launchIn(viewModelScope)
} }
private fun handleArgs(args: DestinationResult) { private fun handleArgs(args: DestinationResult) {
when (args) { when (args) {
is CancelDestinationResult -> navigationManager.navigateUp() is CancelDestinationResult -> navigationManager.navigateUp()
is SuccessDestinationResult -> serviceManager.startService(PRXService::class.java, args.getDevice()) is SuccessDestinationResult -> repository.launch(args.getDevice().device)
}.exhaustive }.exhaustive
} }
fun onEvent(event: PRXScreenViewEvent) { fun onEvent(event: PRXScreenViewEvent) {
when (event) { when (event) {
DisconnectEvent -> onDisconnectButtonClick() DisconnectEvent -> disconnect()
TurnOffAlert -> repository.invokeCommand(DisableAlarm) TurnOffAlert -> repository.disableAlarm()
TurnOnAlert -> repository.invokeCommand(EnableAlarm) TurnOnAlert -> repository.enableAlarm()
NavigateUpEvent -> navigationManager.navigateUp()
}.exhaustive }.exhaustive
} }
private fun onDisconnectButtonClick() { private fun disconnect() {
repository.invokeCommand(Disconnect) repository.release()
repository.clear() navigationManager.navigateUp()
}
override fun onCleared() {
super.onCleared()
repository.clear()
} }
} }

View File

@@ -14,4 +14,7 @@
<string name="prx_is_remote_alarm">Remote alarm</string> <string name="prx_is_remote_alarm">Remote alarm</string>
<string name="prx_local_alarm_level">Local alarm level</string> <string name="prx_local_alarm_level">Local alarm level</string>
<string name="prx_device_out_of_range">Device out of range</string>
<string name="prx_device_out_of_range_reason">Device is out of range and it has disconnected. It should reconnect automatically after device is in range again.</string>
</resources> </resources>