Change CSC module

This commit is contained in:
Sylwester Zieliński
2022-02-11 14:03:59 +01:00
parent 8d21c591ee
commit 01ed437d45
14 changed files with 228 additions and 243 deletions

View File

@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.cgms.data.CGMRepository import no.nordicsemi.android.cgms.data.CGMRepository
import no.nordicsemi.android.csc.data.CSCRepository
import no.nordicsemi.android.navigation.NavigationManager import no.nordicsemi.android.navigation.NavigationManager
import no.nordicsemi.android.nrftoolbox.ProfileDestination import no.nordicsemi.android.nrftoolbox.ProfileDestination
import no.nordicsemi.android.nrftoolbox.view.HomeViewState import no.nordicsemi.android.nrftoolbox.view.HomeViewState
@@ -16,7 +17,8 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class HomeViewModel @Inject constructor( class HomeViewModel @Inject constructor(
private val navigationManager: NavigationManager, private val navigationManager: NavigationManager,
private val cgmRepository: CGMRepository cgmRepository: CGMRepository,
cscRepository: CSCRepository
) : ViewModel() { ) : ViewModel() {
private val _state = MutableStateFlow(HomeViewState()) private val _state = MutableStateFlow(HomeViewState())
@@ -26,6 +28,10 @@ class HomeViewModel @Inject constructor(
cgmRepository.isRunning.onEach { cgmRepository.isRunning.onEach {
_state.value = _state.value.copy(isCGMModuleRunning = it) _state.value = _state.value.copy(isCGMModuleRunning = it)
}.launchIn(viewModelScope) }.launchIn(viewModelScope)
cscRepository.isRunning.onEach {
_state.value = _state.value.copy(isCSCModuleRunning = it)
}.launchIn(viewModelScope)
} }
fun openProfile(destination: ProfileDestination) { fun openProfile(destination: ProfileDestination) {

View File

@@ -39,6 +39,7 @@ class CGMRepository @Inject constructor(
fun start(device: BluetoothDevice, scope: CoroutineScope) { fun start(device: BluetoothDevice, scope: CoroutineScope) {
val manager = CGMManager(context, scope) val manager = CGMManager(context, scope)
this.manager = manager
manager.dataHolder.status.onEach { manager.dataHolder.status.onEach {
_data.value = it _data.value = it

View File

@@ -14,7 +14,6 @@ import no.nordicsemi.android.cgms.view.*
import no.nordicsemi.android.navigation.* import no.nordicsemi.android.navigation.*
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.DiscoveredBluetoothDevice
import no.nordicsemi.ui.scanner.ScannerDestinationId import no.nordicsemi.ui.scanner.ScannerDestinationId
import javax.inject.Inject import javax.inject.Inject
@@ -58,7 +57,7 @@ internal class CGMScreenViewModel @Inject constructor(
private fun handleArgs(args: DestinationResult) { private fun handleArgs(args: DestinationResult) {
when (args) { when (args) {
is CancelDestinationResult -> navigationManager.navigateUp() is CancelDestinationResult -> navigationManager.navigateUp()
is SuccessDestinationResult -> connectDevice(args.getDevice()) is SuccessDestinationResult -> repository.launch(args.getDevice().device)
}.exhaustive }.exhaustive
} }
@@ -71,10 +70,6 @@ internal class CGMScreenViewModel @Inject constructor(
}.exhaustive }.exhaustive
} }
private fun connectDevice(deviceHolder: DiscoveredBluetoothDevice) {
repository.launch(deviceHolder.device)
}
private fun disconnect() { private fun disconnect() {
repository.release() repository.release()
navigationManager.navigateUp() navigationManager.navigateUp()

View File

@@ -1,17 +1,7 @@
package no.nordicsemi.android.csc.data package no.nordicsemi.android.csc.data
import no.nordicsemi.android.csc.view.SpeedUnit
import no.nordicsemi.android.material.you.RadioButtonItem
import no.nordicsemi.android.material.you.RadioGroupViewEntity
import java.util.*
private const val DISPLAY_M_S = "m/s"
private const val DISPLAY_KM_H = "km/h"
private const val DISPLAY_MPH = "mph"
internal data class CSCData( internal data class CSCData(
val scanDevices: Boolean = false, val scanDevices: Boolean = false,
val selectedSpeedUnit: SpeedUnit = SpeedUnit.M_S,
val speed: Float = 0f, val speed: Float = 0f,
val cadence: Float = 0f, val cadence: Float = 0f,
val distance: Float = 0f, val distance: Float = 0f,
@@ -19,70 +9,4 @@ internal data class CSCData(
val gearRatio: Float = 0f, val gearRatio: Float = 0f,
val batteryLevel: Int = 0, val batteryLevel: Int = 0,
val wheelSize: WheelSize = WheelSize() val wheelSize: WheelSize = WheelSize()
) { )
private val speedWithUnit = when (selectedSpeedUnit) {
SpeedUnit.M_S -> speed
SpeedUnit.KM_H -> speed * 3.6f
SpeedUnit.MPH -> speed * 2.2369f
}
fun displaySpeed(): String {
return when (selectedSpeedUnit) {
SpeedUnit.M_S -> String.format("%.1f m/s", speedWithUnit)
SpeedUnit.KM_H -> String.format("%.1f km/h", speedWithUnit)
SpeedUnit.MPH -> String.format("%.1f mph", speedWithUnit)
}
}
fun displayCadence(): String {
return String.format("%.0f RPM", cadence)
}
fun displayDistance(): String {
return when (selectedSpeedUnit) {
SpeedUnit.M_S -> String.format("%.0f m", distance)
SpeedUnit.KM_H -> String.format("%.0f m", distance)
SpeedUnit.MPH -> String.format("%.0f yd", distance)
}
}
fun displayTotalDistance(): String {
return when (selectedSpeedUnit) {
SpeedUnit.M_S -> String.format("%.2f km", totalDistance)
SpeedUnit.KM_H -> String.format("%.2f km", totalDistance)
SpeedUnit.MPH -> String.format("%.2f mile", totalDistance)
}
}
fun displayGearRatio(): String {
return String.format(Locale.US, "%.1f", gearRatio)
}
fun getSpeedUnit(label: String): SpeedUnit {
return when (label) {
DISPLAY_KM_H -> SpeedUnit.KM_H
DISPLAY_M_S -> SpeedUnit.M_S
DISPLAY_MPH -> SpeedUnit.MPH
else -> throw IllegalArgumentException("Can't create SpeedUnit from this label: $label")
}
}
fun temperatureSettingsItems(): RadioGroupViewEntity {
return RadioGroupViewEntity(
SpeedUnit.values().map { createRadioButtonItem(it) }
)
}
private fun createRadioButtonItem(unit: SpeedUnit): RadioButtonItem {
return RadioButtonItem(displayTemperature(unit), unit == selectedSpeedUnit)
}
private fun displayTemperature(unit: SpeedUnit): String {
return when (unit) {
SpeedUnit.KM_H -> DISPLAY_KM_H
SpeedUnit.M_S -> DISPLAY_M_S
SpeedUnit.MPH -> DISPLAY_MPH
}
}
}

View File

@@ -1,68 +1,74 @@
package no.nordicsemi.android.csc.data package no.nordicsemi.android.csc.data
import kotlinx.coroutines.channels.BufferOverflow import android.bluetooth.BluetoothDevice
import kotlinx.coroutines.flow.* import android.content.Context
import no.nordicsemi.android.csc.view.SpeedUnit import dagger.hilt.android.qualifiers.ApplicationContext
import no.nordicsemi.android.service.BleManagerStatus 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.csc.repository.CSCManager
import no.nordicsemi.android.csc.repository.CSCService
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.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
internal class CSCRepository @Inject constructor() { class CSCRepository @Inject constructor(
@ApplicationContext
private val context: Context,
private val serviceManager: ServiceManager,
) {
private var manager: CSCManager? = null
private val _data = MutableStateFlow(CSCData()) private val _data = MutableStateFlow<BleManagerResult<CSCData>>(ConnectingResult())
val data: StateFlow<CSCData> = _data.asStateFlow() internal val data = _data.asStateFlow()
private val _command = MutableSharedFlow<CSCServiceCommand>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST) private val _isRunning = MutableStateFlow(false)
val command = _command.asSharedFlow() val isRunning = _isRunning.asStateFlow()
private val _status = MutableStateFlow(BleManagerStatus.CONNECTING) fun launch(device: BluetoothDevice) {
val status = _status.asStateFlow() serviceManager.startService(CSCService::class.java, device)
fun setSpeedUnit(selectedSpeedUnit: SpeedUnit) {
_data.tryEmit(_data.value.copy(selectedSpeedUnit = selectedSpeedUnit))
} }
fun setNewDistance( fun start(device: BluetoothDevice, scope: CoroutineScope) {
totalDistance: Float, val manager = CSCManager(context, scope)
distance: Float, this.manager = manager
speed: Float,
wheelSize: WheelSize
) {
_data.tryEmit(_data.value.copy(
totalDistance = totalDistance,
distance = distance,
speed = speed,
wheelSize = wheelSize
))
}
fun setNewCrankCadence( manager.dataHolder.status.onEach {
crankCadence: Float, _data.value = it
gearRatio: Float, }.launchIn(scope)
wheelSize: WheelSize
) {
_data.tryEmit(_data.value.copy(cadence = crankCadence, gearRatio = gearRatio, wheelSize = wheelSize))
}
fun setBatteryLevel(batteryLevel: Int) { scope.launch {
_data.tryEmit(_data.value.copy(batteryLevel = batteryLevel)) manager.start(device)
}
fun sendNewServiceCommand(workingMode: CSCServiceCommand) {
if (_command.subscriptionCount.value > 0) {
_command.tryEmit(workingMode)
} else {
_status.tryEmit(BleManagerStatus.DISCONNECTED)
} }
} }
fun setNewStatus(status: BleManagerStatus) { fun setWheelSize(wheelSize: WheelSize) {
_status.value = status manager?.setWheelSize(wheelSize)
} }
fun clear() { private suspend fun CSCManager.start(device: BluetoothDevice) {
_status.value = BleManagerStatus.CONNECTING try {
_data.tryEmit(CSCData()) connect(device)
.useAutoConnect(false)
.retry(3, 100)
.suspend()
_isRunning.value = true
} catch (e: Exception) {
e.printStackTrace()
}
}
fun release() {
serviceManager.stopService(CSCService::class.java)
manager?.disconnect()?.enqueue()
manager = null
_isRunning.value = false
} }
} }

View File

@@ -21,17 +21,15 @@
*/ */
package no.nordicsemi.android.csc.repository package no.nordicsemi.android.csc.repository
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattCharacteristic
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import no.nordicsemi.android.ble.BleManager import no.nordicsemi.android.ble.BleManager
import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelResponse import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelResponse
@@ -40,9 +38,8 @@ 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.csc.data.CSCData import no.nordicsemi.android.csc.data.CSCData
import no.nordicsemi.android.csc.data.WheelSize import no.nordicsemi.android.csc.data.WheelSize
import no.nordicsemi.android.service.* import no.nordicsemi.android.service.ConnectionObserverAdapter
import java.util.* import java.util.*
import javax.inject.Inject
val CSC_SERVICE_UUID: UUID = UUID.fromString("00001816-0000-1000-8000-00805f9b34fb") val CSC_SERVICE_UUID: UUID = UUID.fromString("00001816-0000-1000-8000-00805f9b34fb")
private val CSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A5B-0000-1000-8000-00805f9b34fb") private val CSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A5B-0000-1000-8000-00805f9b34fb")
@@ -50,35 +47,6 @@ private val CSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A5B-0000
private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-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") private val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb")
internal class CSCRepo @Inject constructor(
@ApplicationContext
private val context: Context,
) {
suspend fun downloadData(device: BluetoothDevice): Flow<BleManagerResult<CSCData>> = callbackFlow {
val scope = CoroutineScope(coroutineContext)
val manager = CSCManager(context, scope)
manager.dataHolder.status.onEach {
trySend(it)
}.launchIn(scope)
scope.launch {
manager.connect(device)
.useAutoConnect(false)
.retry(3, 100)
.suspend()
}
awaitClose {
scope.launch {
manager.disconnect().suspend()
}
scope.cancel()
}
}
}
internal class CSCManager( internal class CSCManager(
context: Context, context: Context,
private val scope: CoroutineScope, private val scope: CoroutineScope,

View File

@@ -1,31 +1,27 @@
package no.nordicsemi.android.csc.repository package no.nordicsemi.android.csc.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 kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.csc.data.CSCRepository import no.nordicsemi.android.csc.data.CSCRepository
import no.nordicsemi.android.csc.data.DisconnectCommand import no.nordicsemi.android.service.DEVICE_DATA
import no.nordicsemi.android.csc.data.SetWheelSizeCommand 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 CSCService : ForegroundBleService() { internal class CSCService : NotificationService() {
@Inject @Inject
lateinit var repository: CSCRepository lateinit var repository: CSCRepository
override val manager: CSCManager by lazy { CSCManager(this, scope) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
override fun onCreate() { val device = intent!!.getParcelableExtra<BluetoothDevice>(DEVICE_DATA)!!
super.onCreate()
repository.command.onEach { repository.start(device, lifecycleScope)
when (it) {
DisconnectCommand -> stopSelf() return START_REDELIVER_INTENT
is SetWheelSizeCommand -> manager.setWheelSize(it.wheelSize)
}.exhaustive
}.launchIn(scope)
} }
} }

View File

@@ -28,7 +28,7 @@ import no.nordicsemi.android.theme.view.dialog.ItemSelectedResult
import no.nordicsemi.android.utils.exhaustive import no.nordicsemi.android.utils.exhaustive
@Composable @Composable
internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) { internal fun CSCContentView(state: CSCData, speedUnit: SpeedUnit, onEvent: (CSCViewEvent) -> Unit) {
val showDialog = rememberSaveable { mutableStateOf(false) } val showDialog = rememberSaveable { mutableStateOf(false) }
if (showDialog.value) { if (showDialog.value) {
@@ -51,11 +51,11 @@ internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(16.dp)
) { ) {
SettingsSection(state, onEvent) { showDialog.value = true } SettingsSection(state, speedUnit, onEvent) { showDialog.value = true }
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
SensorsReadingView(state = state) SensorsReadingView(state = state, speedUnit)
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@@ -70,6 +70,7 @@ internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
@Composable @Composable
private fun SettingsSection( private fun SettingsSection(
state: CSCData, state: CSCData,
speedUnit: SpeedUnit,
onEvent: (CSCViewEvent) -> Unit, onEvent: (CSCViewEvent) -> Unit,
onWheelButtonClick: () -> Unit, onWheelButtonClick: () -> Unit,
) { ) {
@@ -86,8 +87,8 @@ private fun SettingsSection(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
RadioButtonGroup(viewEntity = state.temperatureSettingsItems()) { RadioButtonGroup(viewEntity = speedUnit.temperatureSettingsItems()) {
onEvent(OnSelectedSpeedUnitSelected(state.getSpeedUnit(it.label))) onEvent(OnSelectedSpeedUnitSelected(it.label.toSpeedUnit()))
} }
} }
} }
@@ -96,5 +97,5 @@ private fun SettingsSection(
@Preview @Preview
@Composable @Composable
private fun ConnectedPreview() { private fun ConnectedPreview() {
CSCContentView(CSCData()) { } CSCContentView(CSCData(), SpeedUnit.KM_H) { }
} }

View File

@@ -0,0 +1,79 @@
package no.nordicsemi.android.csc.view
import no.nordicsemi.android.csc.data.CSCData
import no.nordicsemi.android.material.you.RadioButtonItem
import no.nordicsemi.android.material.you.RadioGroupViewEntity
import java.util.*
private const val DISPLAY_M_S = "m/s"
private const val DISPLAY_KM_H = "km/h"
private const val DISPLAY_MPH = "mph"
internal fun CSCData.speedWithSpeedUnit(speedUnit: SpeedUnit): Float {
return when (speedUnit) {
SpeedUnit.M_S -> speed
SpeedUnit.KM_H -> speed * 3.6f
SpeedUnit.MPH -> speed * 2.2369f
}
}
internal fun CSCData.displaySpeed(speedUnit: SpeedUnit): String {
val speedWithUnit = speedWithSpeedUnit(speedUnit)
return when (speedUnit) {
SpeedUnit.M_S -> String.format("%.1f m/s", speedWithUnit)
SpeedUnit.KM_H -> String.format("%.1f km/h", speedWithUnit)
SpeedUnit.MPH -> String.format("%.1f mph", speedWithUnit)
}
}
internal fun CSCData.displayCadence(): String {
return String.format("%.0f RPM", cadence)
}
internal fun CSCData.displayDistance(speedUnit: SpeedUnit): String {
return when (speedUnit) {
SpeedUnit.M_S -> String.format("%.0f m", distance)
SpeedUnit.KM_H -> String.format("%.0f m", distance)
SpeedUnit.MPH -> String.format("%.0f yd", distance)
}
}
internal fun CSCData.displayTotalDistance(speedUnit: SpeedUnit): String {
return when (speedUnit) {
SpeedUnit.M_S -> String.format("%.2f km", totalDistance)
SpeedUnit.KM_H -> String.format("%.2f km", totalDistance)
SpeedUnit.MPH -> String.format("%.2f mile", totalDistance)
}
}
internal fun CSCData.displayGearRatio(): String {
return String.format(Locale.US, "%.1f", gearRatio)
}
internal fun String.toSpeedUnit(): SpeedUnit {
return when (this) {
DISPLAY_KM_H -> SpeedUnit.KM_H
DISPLAY_M_S -> SpeedUnit.M_S
DISPLAY_MPH -> SpeedUnit.MPH
else -> throw IllegalArgumentException("Can't create SpeedUnit from this label: $this")
}
}
internal fun SpeedUnit.temperatureSettingsItems(): RadioGroupViewEntity {
return RadioGroupViewEntity(
SpeedUnit.values().map { createRadioButtonItem(it, this) }
)
}
private fun createRadioButtonItem(unit: SpeedUnit, selectedSpeedUnit: SpeedUnit): RadioButtonItem {
return RadioButtonItem(displayTemperature(unit), unit == selectedSpeedUnit)
}
private fun displayTemperature(unit: SpeedUnit): String {
return when (unit) {
SpeedUnit.KM_H -> DISPLAY_KM_H
SpeedUnit.M_S -> DISPLAY_M_S
SpeedUnit.MPH -> DISPLAY_MPH
}
}

View File

@@ -10,7 +10,13 @@ import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.csc.R import no.nordicsemi.android.csc.R
import no.nordicsemi.android.csc.viewmodel.CSCViewModel import no.nordicsemi.android.csc.viewmodel.CSCViewModel
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 CSCScreen() { fun CSCScreen() {
@@ -18,15 +24,22 @@ fun CSCScreen() {
val state = viewModel.state.collectAsState().value val state = viewModel.state.collectAsState().value
Column { Column {
BackIconAppBar(stringResource(id = R.string.csc_title)) { val navigateUp = { viewModel.onEvent(NavigateUp) }
viewModel.onEvent(OnDisconnectButtonClick)
} BackIconAppBar(stringResource(id = R.string.csc_title), navigateUp)
Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
// when (state) { when (state.cscManagerState) {
// is DisplayDataState -> CSCContentView(state.data) { viewModel.onEvent(it) } NoDeviceState -> NoDeviceView()
// LoadingState -> DeviceConnectingView() is WorkingState -> when (state.cscManagerState.result) {
// }.exhaustive is ConnectingResult,
is ReadyResult -> DeviceConnectingView { viewModel.onEvent(OnDisconnectButtonClick) }
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp)
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp)
is MissingServiceResult -> DeviceDisconnectedView(Reason.MISSING_SERVICE, navigateUp)
is SuccessResult -> CSCContentView(state.cscManagerState.result.data, state.speedUnit) { viewModel.onEvent(it) }
}
}.exhaustive
} }
} }
} }

View File

@@ -1,9 +1,15 @@
package no.nordicsemi.android.csc.view package no.nordicsemi.android.csc.view
import no.nordicsemi.android.csc.data.CSCData import no.nordicsemi.android.csc.data.CSCData
import no.nordicsemi.android.service.BleManagerResult
internal sealed class CSCViewState internal data class CSCViewState(
val speedUnit: SpeedUnit = SpeedUnit.M_S,
val cscManagerState: CSCMangerState = NoDeviceState
)
internal object LoadingState : CSCViewState() internal sealed class CSCMangerState
internal data class DisplayDataState(val data: CSCData) : CSCViewState() internal data class WorkingState(val result: BleManagerResult<CSCData>) : CSCMangerState()
internal object NoDeviceState : CSCMangerState()

View File

@@ -9,3 +9,5 @@ internal data class OnWheelSizeSelected(val wheelSize: WheelSize) : CSCViewEvent
internal data class OnSelectedSpeedUnitSelected(val selectedSpeedUnit: SpeedUnit) : CSCViewEvent() internal data class OnSelectedSpeedUnitSelected(val selectedSpeedUnit: SpeedUnit) : CSCViewEvent()
internal object OnDisconnectButtonClick : CSCViewEvent() internal object OnDisconnectButtonClick : CSCViewEvent()
internal object NavigateUp : CSCViewEvent()

View File

@@ -16,22 +16,22 @@ import no.nordicsemi.android.theme.view.ScreenSection
import no.nordicsemi.android.theme.view.SectionTitle import no.nordicsemi.android.theme.view.SectionTitle
@Composable @Composable
internal fun SensorsReadingView(state: CSCData) { internal fun SensorsReadingView(state: CSCData, speedUnit: SpeedUnit) {
ScreenSection { ScreenSection {
SectionTitle(resId = R.drawable.ic_records, title = "Records") SectionTitle(resId = R.drawable.ic_records, title = "Records")
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Column { Column {
KeyValueField(stringResource(id = R.string.csc_field_speed), state.displaySpeed()) KeyValueField(stringResource(id = R.string.csc_field_speed), state.displaySpeed(speedUnit))
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
KeyValueField(stringResource(id = R.string.csc_field_cadence), state.displayCadence()) KeyValueField(stringResource(id = R.string.csc_field_cadence), state.displayCadence())
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
KeyValueField(stringResource(id = R.string.csc_field_distance), state.displayDistance()) KeyValueField(stringResource(id = R.string.csc_field_distance), state.displayDistance(speedUnit))
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
KeyValueField( KeyValueField(
stringResource(id = R.string.csc_field_total_distance), stringResource(id = R.string.csc_field_total_distance),
state.displayTotalDistance() state.displayTotalDistance(speedUnit)
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
KeyValueField(stringResource(id = R.string.csc_field_gear_ratio), state.displayGearRatio()) KeyValueField(stringResource(id = R.string.csc_field_gear_ratio), state.displayGearRatio())
@@ -46,5 +46,5 @@ internal fun SensorsReadingView(state: CSCData) {
@Preview @Preview
@Composable @Composable
private fun Preview() { private fun Preview() {
SensorsReadingView(CSCData()) SensorsReadingView(CSCData(), SpeedUnit.KM_H)
} }

View File

@@ -3,16 +3,14 @@ package no.nordicsemi.android.csc.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.csc.data.CSCRepository import no.nordicsemi.android.csc.data.CSCRepository
import no.nordicsemi.android.csc.data.DisconnectCommand
import no.nordicsemi.android.csc.data.SetWheelSizeCommand
import no.nordicsemi.android.csc.repository.CSCService
import no.nordicsemi.android.csc.repository.CSC_SERVICE_UUID import no.nordicsemi.android.csc.repository.CSC_SERVICE_UUID
import no.nordicsemi.android.csc.view.* import no.nordicsemi.android.csc.view.*
import no.nordicsemi.android.navigation.* import no.nordicsemi.android.navigation.*
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
@@ -21,19 +19,23 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
internal class CSCViewModel @Inject constructor( internal class CSCViewModel @Inject constructor(
private val repository: CSCRepository, private val repository: CSCRepository,
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(CSCViewState())
// 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 = _state.value.copy(cscManagerState = WorkingState(it))
}.launchIn(viewModelScope)
}
private fun requestBluetoothDevice() {
navigationManager.navigateTo(ScannerDestinationId, UUIDArgument(CSC_SERVICE_UUID)) navigationManager.navigateTo(ScannerDestinationId, UUIDArgument(CSC_SERVICE_UUID))
navigationManager.recentResult.onEach { navigationManager.recentResult.onEach {
@@ -41,44 +43,30 @@ internal class CSCViewModel @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(CSCService::class.java, args.getDevice()) is SuccessDestinationResult -> repository.launch(args.getDevice().device)
}.exhaustive }.exhaustive
} }
fun onEvent(event: CSCViewEvent) { fun onEvent(event: CSCViewEvent) {
when (event) { when (event) {
is OnSelectedSpeedUnitSelected -> onSelectedSpeedUnit(event) is OnSelectedSpeedUnitSelected -> setSpeedUnit(event.selectedSpeedUnit)
is OnWheelSizeSelected -> onWheelSizeChanged(event) is OnWheelSizeSelected -> repository.setWheelSize(event.wheelSize)
OnDisconnectButtonClick -> onDisconnectButtonClick() OnDisconnectButtonClick -> disconnect()
NavigateUp -> navigationManager.navigateUp()
}.exhaustive }.exhaustive
} }
private fun onSelectedSpeedUnit(event: OnSelectedSpeedUnitSelected) { private fun setSpeedUnit(speedUnit: SpeedUnit) {
repository.setSpeedUnit(event.selectedSpeedUnit) _state.value = _state.value.copy(speedUnit = speedUnit)
} }
private fun onWheelSizeChanged(event: OnWheelSizeSelected) { private fun disconnect() {
repository.sendNewServiceCommand(SetWheelSizeCommand(event.wheelSize)) repository.release()
} navigationManager.navigateUp()
private fun onDisconnectButtonClick() {
repository.sendNewServiceCommand(DisconnectCommand)
repository.clear()
}
override fun onCleared() {
super.onCleared()
repository.clear()
} }
} }