Change RSCS screen

This commit is contained in:
Sylwester Zieliński
2022-02-14 15:58:10 +01:00
parent f92e1b4adb
commit 91e3bcc1ea
9 changed files with 166 additions and 117 deletions

View File

@@ -14,6 +14,8 @@ import no.nordicsemi.android.hts.data.HTSRepository
import no.nordicsemi.android.navigation.NavigationManager
import no.nordicsemi.android.nrftoolbox.ProfileDestination
import no.nordicsemi.android.nrftoolbox.view.HomeViewState
import no.nordicsemi.android.prx.data.PRXRepository
import no.nordicsemi.android.rscs.data.RSCSRepository
import javax.inject.Inject
@HiltViewModel
@@ -22,7 +24,9 @@ class HomeViewModel @Inject constructor(
cgmRepository: CGMRepository,
cscRepository: CSCRepository,
hrsRepository: HRSRepository,
htsRepository: HTSRepository
htsRepository: HTSRepository,
prxRepository: PRXRepository,
rscsRepository: RSCSRepository
) : ViewModel() {
private val _state = MutableStateFlow(HomeViewState())
@@ -44,6 +48,14 @@ class HomeViewModel @Inject constructor(
htsRepository.isRunning.onEach {
_state.value = _state.value.copy(isHTSModuleRunning = it)
}.launchIn(viewModelScope)
prxRepository.isRunning.onEach {
_state.value = _state.value.copy(isPRXModuleRunning = it)
}.launchIn(viewModelScope)
rscsRepository.isRunning.onEach {
_state.value = _state.value.copy(isRSCSModuleRunning = it)
}.launchIn(viewModelScope)
}
fun openProfile(destination: ProfileDestination) {

View File

@@ -22,7 +22,7 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
internal class PRXRepository @Inject constructor(
class PRXRepository @Inject internal constructor(
@ApplicationContext
private val context: Context,
private val serviceManager: ServiceManager,

View File

@@ -1,57 +1,70 @@
package no.nordicsemi.android.rscs.data
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.*
import no.nordicsemi.android.service.BleManagerStatus
import android.bluetooth.BluetoothDevice
import android.content.Context
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.rscs.repository.RSCSManager
import no.nordicsemi.android.rscs.repository.RSCSService
import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.service.ConnectingResult
import no.nordicsemi.android.service.ServiceManager
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
internal class RSCSRepository @Inject constructor() {
class RSCSRepository @Inject constructor(
@ApplicationContext
private val context: Context,
private val serviceManager: ServiceManager,
) {
private var manager: RSCSManager? = null
private val _data = MutableStateFlow(RSCSData())
val data: StateFlow<RSCSData> = _data.asStateFlow()
private val _data = MutableStateFlow<BleManagerResult<RSCSData>>(ConnectingResult())
internal val data = _data.asStateFlow()
private val _command = MutableSharedFlow<DisconnectCommand>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST)
val command = _command.asSharedFlow()
private val _isRunning = MutableStateFlow(false)
val isRunning = _isRunning.asStateFlow()
private val _status = MutableStateFlow(BleManagerStatus.CONNECTING)
val status = _status.asStateFlow()
fun setNewData(
running: Boolean,
instantaneousSpeed: Float,
instantaneousCadence: Int,
strideLength: Int?,
totalDistance: Long?
) {
_data.tryEmit(_data.value.copy(
running = running,
instantaneousCadence = instantaneousCadence,
instantaneousSpeed = instantaneousSpeed,
strideLength = strideLength,
totalDistance = totalDistance
))
fun launch(device: BluetoothDevice) {
serviceManager.startService(RSCSService::class.java, device)
}
fun setNewStatus(status: BleManagerStatus) {
_status.value = status
}
fun start(device: BluetoothDevice, scope: CoroutineScope) {
val manager = RSCSManager(context, scope)
this.manager = manager
fun setBatteryLevel(batteryLevel: Int) {
_data.tryEmit(_data.value.copy(batteryLevel = batteryLevel))
}
manager.dataHolder.status.onEach {
_data.value = it
}.launchIn(scope)
fun sendDisconnectCommand() {
if (_command.subscriptionCount.value > 0) {
_command.tryEmit(DisconnectCommand)
} else {
_status.tryEmit(BleManagerStatus.DISCONNECTED)
scope.launch {
manager.start(device)
}
}
fun clear() {
_status.value = BleManagerStatus.CONNECTING
_data.tryEmit(RSCSData())
private suspend fun RSCSManager.start(device: BluetoothDevice) {
try {
connect(device)
.useAutoConnect(false)
.retry(3, 100)
.suspend()
_isRunning.value = true
} catch (e: Exception) {
e.printStackTrace()
}
}
fun release() {
serviceManager.stopService(RSCSService::class.java)
manager?.disconnect()?.enqueue()
manager = null
_isRunning.value = false
}
}

View File

@@ -24,66 +24,81 @@ package no.nordicsemi.android.rscs.repository
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.content.Context
import android.util.Log
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import no.nordicsemi.android.ble.BleManager
import no.nordicsemi.android.ble.common.callback.rsc.RunningSpeedAndCadenceMeasurementResponse
import no.nordicsemi.android.ble.ktx.asValidResponseFlow
import no.nordicsemi.android.ble.ktx.suspend
import no.nordicsemi.android.rscs.data.RSCSRepository
import no.nordicsemi.android.service.BatteryManager
import no.nordicsemi.android.rscs.data.RSCSData
import no.nordicsemi.android.service.ConnectionObserverAdapter
import no.nordicsemi.android.utils.launchWithCatch
import java.util.*
val RSCS_SERVICE_UUID: UUID = UUID.fromString("00001814-0000-1000-8000-00805F9B34FB")
private val RSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A53-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 RSCSManager internal constructor(
context: Context,
scope: CoroutineScope,
private val dataHolder: RSCSRepository
) : BatteryManager(context, scope) {
private val scope: CoroutineScope
) : BleManager(context) {
private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null
private var rscMeasurementCharacteristic: BluetoothGattCharacteristic? = null
private val exceptionHandler = CoroutineExceptionHandler { _, t->
Log.e("COROUTINE-EXCEPTION", "Uncaught exception", t)
private val data = MutableStateFlow(RSCSData())
val dataHolder = ConnectionObserverAdapter<RSCSData>()
init {
setConnectionObserver(dataHolder)
data.onEach {
dataHolder.setValue(it)
}.launchIn(scope)
}
override fun onBatteryLevelChanged(batteryLevel: Int) {
dataHolder.setBatteryLevel(batteryLevel)
}
override fun getGattCallback(): BatteryManagerGattCallback {
return RSCManagerGattCallback()
}
private inner class RSCManagerGattCallback : BatteryManagerGattCallback() {
private inner class RSCManagerGattCallback : BleManagerGattCallback() {
override fun initialize() {
super.initialize()
setNotificationCallback(rscMeasurementCharacteristic).asValidResponseFlow<RunningSpeedAndCadenceMeasurementResponse>()
.onEach {
dataHolder.setNewData(it.isRunning, it.instantaneousSpeed, it.instantaneousCadence, it.strideLength, it.totalDistance)
}.launchIn(scope)
data.tryEmit(data.value.copy(
running = it.isRunning,
instantaneousCadence = it.instantaneousCadence,
instantaneousSpeed = it.instantaneousSpeed,
strideLength = it.strideLength,
totalDistance = it.totalDistance
))
}.launchIn(scope)
scope.launch(exceptionHandler) {
scope.launchWithCatch {
enableNotifications(rscMeasurementCharacteristic).suspend()
}
}
public override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
val service = gatt.getService(RSCS_SERVICE_UUID)
if (service != null) {
rscMeasurementCharacteristic = service.getCharacteristic(RSC_MEASUREMENT_CHARACTERISTIC_UUID)
gatt.getService(RSCS_SERVICE_UUID)?.run {
rscMeasurementCharacteristic = getCharacteristic(RSC_MEASUREMENT_CHARACTERISTIC_UUID)
}
return rscMeasurementCharacteristic != null
gatt.getService(BATTERY_SERVICE_UUID)?.run {
batteryLevelCharacteristic = getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)
}
return rscMeasurementCharacteristic != null && batteryLevelCharacteristic != null
}
override fun onServicesInvalidated() {
rscMeasurementCharacteristic = null
batteryLevelCharacteristic = null
}
}
override fun getGattCallback(): BleManagerGattCallback {
return RSCManagerGattCallback()
}
}

View File

@@ -1,31 +1,30 @@
package no.nordicsemi.android.rscs.repository
import android.bluetooth.BluetoothDevice
import android.content.Intent
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.rscs.data.RSCSRepository
import no.nordicsemi.android.service.DEVICE_DATA
import no.nordicsemi.android.service.ForegroundBleService
import no.nordicsemi.android.service.NotificationService
import javax.inject.Inject
@AndroidEntryPoint
internal class RSCSService : ForegroundBleService() {
internal class RSCSService : NotificationService() {
@Inject
lateinit var repository: RSCSRepository
override val manager: RSCSManager by lazy { RSCSManager(this, scope, repository) }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
override fun onCreate() {
super.onCreate()
val device = intent!!.getParcelableExtra<BluetoothDevice>(DEVICE_DATA)!!
// status.onEach {
// val status = it.mapToSimpleManagerStatus()
// repository.setNewStatus(status)
// stopIfDisconnected(status)
// }.launchIn(scope)
repository.start(device, lifecycleScope)
repository.command.onEach {
stopSelf()
}.launchIn(scope)
return START_REDELIVER_INTENT
}
}

View File

@@ -10,7 +10,13 @@ import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.rscs.R
import no.nordicsemi.android.rscs.viewmodel.RSCSViewModel
import no.nordicsemi.android.service.*
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
fun RSCSScreen() {
@@ -18,15 +24,25 @@ fun RSCSScreen() {
val state = viewModel.state.collectAsState().value
Column {
val navigateUp = { viewModel.onEvent(NavigateUpEvent) }
BackIconAppBar(stringResource(id = R.string.rscs_title)) {
viewModel.onEvent(DisconnectEvent)
}
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
// when (state) {
// is DisplayDataState -> RSCSContentView(state.data) { viewModel.onEvent(it) }
// LoadingState -> DeviceConnectingView()
// }.exhaustive
when (state) {
NoDeviceState -> NoDeviceView()
is WorkingState -> when (state.result) {
is ConnectingResult,
is ReadyResult
-> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) }
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp)
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp)
is MissingServiceResult -> DeviceDisconnectedView(Reason.MISSING_SERVICE, navigateUp)
is SuccessResult -> RSCSContentView(state.result.data) { viewModel.onEvent(it) }
}
}.exhaustive
}
}
}

View File

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

View File

@@ -2,4 +2,6 @@ package no.nordicsemi.android.rscs.view
internal sealed class RSCScreenViewEvent
internal object NavigateUpEvent : RSCScreenViewEvent()
internal object DisconnectEvent : RSCScreenViewEvent()

View File

@@ -3,17 +3,14 @@ package no.nordicsemi.android.rscs.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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.rscs.data.RSCSRepository
import no.nordicsemi.android.rscs.repository.RSCSService
import no.nordicsemi.android.rscs.repository.RSCS_SERVICE_UUID
import no.nordicsemi.android.rscs.view.DisconnectEvent
import no.nordicsemi.android.rscs.view.DisplayDataState
import no.nordicsemi.android.rscs.view.LoadingState
import no.nordicsemi.android.rscs.view.RSCScreenViewEvent
import no.nordicsemi.android.service.BleManagerStatus
import no.nordicsemi.android.service.ServiceManager
import no.nordicsemi.android.rscs.view.*
import no.nordicsemi.android.utils.exhaustive
import no.nordicsemi.android.utils.getDevice
import no.nordicsemi.ui.scanner.ScannerDestinationId
@@ -22,19 +19,23 @@ import javax.inject.Inject
@HiltViewModel
internal class RSCSViewModel @Inject constructor(
private val repository: RSCSRepository,
private val serviceManager: ServiceManager,
private val navigationManager: NavigationManager
) : ViewModel() {
val state = repository.data.combine(repository.status) { data, status ->
// when (status) {
// BleManagerStatus.CONNECTING -> LoadingState
// BleManagerStatus.OK,
// BleManagerStatus.DISCONNECTED -> DisplayDataState(data)
// }
}.stateIn(viewModelScope, SharingStarted.Lazily, LoadingState)
private val _state = MutableStateFlow<RSCSViewState>(NoDeviceState)
val state = _state.asStateFlow()
init {
if (!repository.isRunning.value) {
requestBluetoothDevice()
}
repository.data.onEach {
_state.value = WorkingState(it)
}.launchIn(viewModelScope)
}
private fun requestBluetoothDevice() {
navigationManager.navigateTo(ScannerDestinationId, UUIDArgument(RSCS_SERVICE_UUID))
navigationManager.recentResult.onEach {
@@ -42,34 +43,24 @@ internal class RSCSViewModel @Inject constructor(
handleArgs(it)
}
}.launchIn(viewModelScope)
repository.status.onEach {
if (it == BleManagerStatus.DISCONNECTED) {
navigationManager.navigateUp()
}
}.launchIn(viewModelScope)
}
private fun handleArgs(args: DestinationResult) {
when (args) {
is CancelDestinationResult -> navigationManager.navigateUp()
is SuccessDestinationResult -> serviceManager.startService(RSCSService::class.java, args.getDevice())
is SuccessDestinationResult -> repository.launch(args.getDevice().device)
}.exhaustive
}
fun onEvent(event: RSCScreenViewEvent) {
when (event) {
DisconnectEvent -> onDisconnectButtonClick()
DisconnectEvent -> disconnect()
NavigateUpEvent -> navigationManager.navigateUp()
}.exhaustive
}
private fun onDisconnectButtonClick() {
repository.sendDisconnectCommand()
repository.clear()
}
override fun onCleared() {
super.onCleared()
repository.clear()
private fun disconnect() {
repository.release()
navigationManager.navigateUp()
}
}