Add connecting view to profiles

This commit is contained in:
Sylwester Zieliński
2022-01-18 09:59:30 +01:00
parent 2a9b66c357
commit 2c304e80f6
104 changed files with 834 additions and 951 deletions

View File

@@ -1,13 +1,11 @@
package no.nordicsemi.android.csc
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*

View File

@@ -1,6 +1,5 @@
package no.nordicsemi.android.csc.data
import no.nordicsemi.android.csc.view.CSCSettings
import no.nordicsemi.android.csc.view.SpeedUnit
import no.nordicsemi.android.material.you.RadioButtonItem
import no.nordicsemi.android.material.you.RadioGroupViewEntity
@@ -11,7 +10,6 @@ private const val DISPLAY_KM_H = "km/h"
private const val DISPLAY_MPH = "mph"
internal data class CSCData(
val showDialog: Boolean = false,
val scanDevices: Boolean = false,
val selectedSpeedUnit: SpeedUnit = SpeedUnit.M_S,
val speed: Float = 0f,
@@ -20,8 +18,7 @@ internal data class CSCData(
val totalDistance: Float = 0f,
val gearRatio: Float = 0f,
val batteryLevel: Int = 0,
val wheelSize: Int = CSCSettings.DefaultWheelSize.VALUE,
val wheelSizeDisplay: String = CSCSettings.DefaultWheelSize.NAME
val wheelSize: WheelSize = WheelSize()
) {
private val speedWithUnit = when (selectedSpeedUnit) {

View File

@@ -5,7 +5,9 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import no.nordicsemi.android.csc.view.SpeedUnit
import no.nordicsemi.android.service.BleManagerStatus
import javax.inject.Inject
import javax.inject.Singleton
@@ -13,37 +15,38 @@ import javax.inject.Singleton
internal class CSCRepository @Inject constructor() {
private val _data = MutableStateFlow(CSCData())
val data: StateFlow<CSCData> = _data
val data: StateFlow<CSCData> = _data.asStateFlow()
private val _command = MutableSharedFlow<CSCServiceCommand>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST)
val command = _command.asSharedFlow()
fun setWheelSize(wheelSize: Int, wheelSizeDisplay: String) {
_data.tryEmit(_data.value.copy(
wheelSize = wheelSize,
wheelSizeDisplay = wheelSizeDisplay,
showDialog = false
))
}
private val _status = MutableStateFlow(BleManagerStatus.CONNECTING)
val status = _status.asStateFlow()
fun setSpeedUnit(selectedSpeedUnit: SpeedUnit) {
_data.tryEmit(_data.value.copy(selectedSpeedUnit = selectedSpeedUnit))
}
fun setHideWheelSizeDialog() {
_data.tryEmit(_data.value.copy(showDialog = false))
fun setNewDistance(
totalDistance: Float,
distance: Float,
speed: Float,
wheelSize: WheelSize
) {
_data.tryEmit(_data.value.copy(
totalDistance = totalDistance,
distance = distance,
speed = speed,
wheelSize = wheelSize
))
}
fun setDisplayWheelSizeDialog() {
_data.tryEmit(_data.value.copy(showDialog = true))
}
fun setNewDistance(totalDistance: Float, distance: Float, speed: Float) {
_data.tryEmit(_data.value.copy(totalDistance = totalDistance, distance = distance, speed = speed))
}
fun setNewCrankCadence(crankCadence: Float, gearRatio: Float) {
_data.tryEmit(_data.value.copy(cadence = crankCadence, gearRatio = gearRatio))
fun setNewCrankCadence(
crankCadence: Float,
gearRatio: Float,
wheelSize: WheelSize
) {
_data.tryEmit(_data.value.copy(cadence = crankCadence, gearRatio = gearRatio, wheelSize = wheelSize))
}
fun setBatteryLevel(batteryLevel: Int) {
@@ -54,7 +57,12 @@ internal class CSCRepository @Inject constructor() {
_command.tryEmit(workingMode)
}
fun setNewStatus(status: BleManagerStatus) {
_status.value = status
}
fun clear() {
_status.value = BleManagerStatus.CONNECTING
_data.tryEmit(CSCData())
}
}

View File

@@ -2,6 +2,6 @@ package no.nordicsemi.android.csc.data
internal sealed class CSCServiceCommand
internal data class SetWheelSizeCommand(val size: Int) : CSCServiceCommand()
internal data class SetWheelSizeCommand(val wheelSize: WheelSize) : CSCServiceCommand()
internal object DisconnectCommand : CSCServiceCommand()

View File

@@ -0,0 +1,8 @@
package no.nordicsemi.android.csc.data
import no.nordicsemi.android.csc.view.CSCSettings
data class WheelSize(
val value: Int = CSCSettings.DefaultWheelSize.VALUE,
val name: String = CSCSettings.DefaultWheelSize.NAME
)

View File

@@ -30,9 +30,7 @@ import androidx.annotation.FloatRange
import no.nordicsemi.android.ble.common.callback.csc.CyclingSpeedAndCadenceMeasurementDataCallback
import no.nordicsemi.android.ble.data.Data
import no.nordicsemi.android.csc.data.CSCRepository
import no.nordicsemi.android.csc.repository.CSCMeasurementParser.parse
import no.nordicsemi.android.csc.view.CSCSettings
import no.nordicsemi.android.log.LogContract
import no.nordicsemi.android.csc.data.WheelSize
import no.nordicsemi.android.service.BatteryManager
import java.util.*
@@ -42,20 +40,20 @@ val CYCLING_SPEED_AND_CADENCE_SERVICE_UUID: UUID = UUID.fromString("00001816-000
/** Cycling Speed and Cadence Measurement characteristic UUID. */
private val CSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A5B-0000-1000-8000-00805f9b34fb")
internal class CSCManager(context: Context, private val dataHolder: CSCRepository) : BatteryManager(context) {
internal class CSCManager(context: Context, private val repository: CSCRepository) : BatteryManager(context) {
private var cscMeasurementCharacteristic: BluetoothGattCharacteristic? = null
private var wheelSize = CSCSettings.DefaultWheelSize.VALUE
private var wheelSize: WheelSize = WheelSize()
override fun onBatteryLevelChanged(batteryLevel: Int) {
dataHolder.setBatteryLevel(batteryLevel)
repository.setBatteryLevel(batteryLevel)
}
override fun getGattCallback(): BatteryManagerGattCallback {
return CSCManagerGattCallback()
}
fun setWheelSize(value: Int) {
fun setWheelSize(value: WheelSize) {
wheelSize = value
}
@@ -72,7 +70,7 @@ internal class CSCManager(context: Context, private val dataHolder: CSCRepositor
.with(object : CyclingSpeedAndCadenceMeasurementDataCallback() {
override fun getWheelCircumference(): Float {
return wheelSize.toFloat()
return wheelSize.value.toFloat()
}
override fun onDistanceChanged(
@@ -81,7 +79,7 @@ internal class CSCManager(context: Context, private val dataHolder: CSCRepositor
@FloatRange(from = 0.0) distance: Float,
@FloatRange(from = 0.0) speed: Float
) {
dataHolder.setNewDistance(totalDistance, distance, speed)
repository.setNewDistance(totalDistance, distance, speed, wheelSize)
}
override fun onCrankDataChanged(
@@ -89,7 +87,7 @@ internal class CSCManager(context: Context, private val dataHolder: CSCRepositor
@FloatRange(from = 0.0) crankCadence: Float,
gearRatio: Float
) {
dataHolder.setNewCrankCadence(crankCadence, gearRatio)
repository.setNewCrankCadence(crankCadence, gearRatio, wheelSize)
}
override fun onInvalidDataReceived(

View File

@@ -1,5 +1,6 @@
package no.nordicsemi.android.csc.repository
import android.util.Log
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -21,10 +22,14 @@ internal class CSCService : ForegroundBleService() {
override fun onCreate() {
super.onCreate()
status.onEach {
repository.setNewStatus(it)
}.launchIn(scope)
repository.command.onEach {
when (it) {
DisconnectCommand -> stopSelf()
is SetWheelSizeCommand -> manager.setWheelSize(it.size)
is SetWheelSizeCommand -> manager.setWheelSize(it.wheelSize)
}.exhaustive
}.launchIn(scope)
}

View File

@@ -11,21 +11,41 @@ import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.csc.R
import no.nordicsemi.android.csc.data.CSCData
import no.nordicsemi.android.csc.data.WheelSize
import no.nordicsemi.android.material.you.RadioButtonGroup
import no.nordicsemi.android.theme.view.ScreenSection
import no.nordicsemi.android.theme.view.SectionTitle
import no.nordicsemi.android.theme.view.dialog.FlowCanceled
import no.nordicsemi.android.theme.view.dialog.ItemSelectedResult
import no.nordicsemi.android.utils.exhaustive
@Composable
internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
if (state.showDialog) {
SelectWheelSizeDialog { onEvent(it) }
val showDialog = rememberSaveable { mutableStateOf(false) }
if (showDialog.value) {
val wheelEntries = stringArrayResource(R.array.wheel_entries)
val wheelValues = stringArrayResource(R.array.wheel_values)
SelectWheelSizeDialog {
when (it) {
FlowCanceled -> showDialog.value = false
is ItemSelectedResult -> {
onEvent(OnWheelSizeSelected(WheelSize(wheelValues[it.index].toInt(), wheelEntries[it.index])))
showDialog.value = false
}
}.exhaustive
}
}
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
@@ -33,7 +53,7 @@ internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(16.dp)
) {
SettingsSection(state, onEvent)
SettingsSection(state, onEvent) { showDialog.value = true }
Spacer(modifier = Modifier.height(16.dp))
@@ -51,7 +71,7 @@ internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
}
@Composable
private fun SettingsSection(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
private fun SettingsSection(state: CSCData, onEvent: (CSCViewEvent) -> Unit, onWheelButtonClick: () -> Unit) {
ScreenSection {
Column(
horizontalAlignment = Alignment.CenterHorizontally
@@ -60,7 +80,7 @@ private fun SettingsSection(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
Spacer(modifier = Modifier.height(16.dp))
WheelSizeView(state, onEvent)
WheelSizeView(state, onWheelButtonClick)
Spacer(modifier = Modifier.height(16.dp))

View File

@@ -9,46 +9,40 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.csc.R
import no.nordicsemi.android.csc.data.CSCData
import no.nordicsemi.android.csc.repository.CSCService
import no.nordicsemi.android.csc.viewmodel.CSCViewModel
import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.utils.isServiceRunning
import no.nordicsemi.android.theme.view.DeviceConnectingView
import no.nordicsemi.android.utils.exhaustive
@Composable
fun CSCScreen(finishAction: () -> Unit) {
val viewModel: CSCViewModel = hiltViewModel()
val state = viewModel.state.collectAsState().value
val isScreenActive = viewModel.isActive.collectAsState().value
val context = LocalContext.current
LaunchedEffect(isScreenActive) {
if (!isScreenActive) {
finishAction()
}
if (context.isServiceRunning(CSCService::class.java.name)) {
val intent = Intent(context, CSCService::class.java)
context.stopService(intent)
}
}
LaunchedEffect("start-service") {
if (!context.isServiceRunning(CSCService::class.java.name)) {
LaunchedEffect(state.isActive) {
if (state.isActive) {
val intent = Intent(context, CSCService::class.java)
context.startService(intent)
} else {
finishAction()
}
}
CSCView(state) { viewModel.onEvent(it) }
CSCView(state.viewState) { viewModel.onEvent(it) }
}
@Composable
private fun CSCView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
private fun CSCView(state: CSCViewState, onEvent: (CSCViewEvent) -> Unit) {
Column {
BackIconAppBar(stringResource(id = R.string.csc_title)) {
onEvent(OnDisconnectButtonClick)
}
CSCContentView(state) { onEvent(it) }
when (state) {
is DisplayDataState -> CSCContentView(state.data, onEvent)
LoadingState -> DeviceConnectingView()
}.exhaustive
}
}

View File

@@ -0,0 +1,14 @@
package no.nordicsemi.android.csc.view
import no.nordicsemi.android.csc.data.CSCData
internal data class CSCState(
val viewState: CSCViewState,
val isActive: Boolean = true
)
internal sealed class CSCViewState
internal object LoadingState : CSCViewState()
internal data class DisplayDataState(val data: CSCData) : CSCViewState()

View File

@@ -1,12 +1,10 @@
package no.nordicsemi.android.csc.view
import no.nordicsemi.android.csc.data.WheelSize
internal sealed class CSCViewEvent
internal object OnShowEditWheelSizeDialogButtonClick : CSCViewEvent()
internal data class OnWheelSizeSelected(val wheelSize: Int, val wheelSizeDisplayInfo: String) : CSCViewEvent()
internal object OnCloseSelectWheelSizeDialog : CSCViewEvent()
internal data class OnWheelSizeSelected(val wheelSize: WheelSize) : CSCViewEvent()
internal data class OnSelectedSpeedUnitSelected(val selectedSpeedUnit: SpeedUnit) : CSCViewEvent()

View File

@@ -5,6 +5,7 @@ import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import no.nordicsemi.android.csc.R
import no.nordicsemi.android.csc.data.WheelSize
import no.nordicsemi.android.material.you.NordicTheme
import no.nordicsemi.android.theme.view.dialog.FlowCanceled
import no.nordicsemi.android.theme.view.dialog.ItemSelectedResult
@@ -15,16 +16,12 @@ import no.nordicsemi.android.theme.view.dialog.toAnnotatedString
import no.nordicsemi.android.utils.exhaustive
@Composable
internal fun SelectWheelSizeDialog(onEvent: (CSCViewEvent) -> Unit) {
internal fun SelectWheelSizeDialog(onEvent: (StringListDialogResult) -> Unit) {
val wheelEntries = stringArrayResource(R.array.wheel_entries)
val wheelValues = stringArrayResource(R.array.wheel_values)
StringListDialog(createConfig(wheelEntries) {
when (it) {
FlowCanceled -> onEvent(OnCloseSelectWheelSizeDialog)
is ItemSelectedResult ->
onEvent(OnWheelSizeSelected(wheelValues[it.index].toInt(), wheelEntries[it.index]))
}.exhaustive
onEvent(it)
})
}

View File

@@ -19,8 +19,8 @@ import no.nordicsemi.android.csc.R
import no.nordicsemi.android.csc.data.CSCData
@Composable
internal fun WheelSizeView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
OutlinedButton(onClick = { onEvent(OnShowEditWheelSizeDialogButtonClick) }) {
internal fun WheelSizeView(state: CSCData, onClick: () -> Unit) {
OutlinedButton(onClick = { onClick() }) {
Row(
modifier = Modifier.fillMaxWidth(0.5f),
verticalAlignment = Alignment.CenterVertically,
@@ -31,7 +31,7 @@ internal fun WheelSizeView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
text = stringResource(id = R.string.csc_field_wheel_size),
style = MaterialTheme.typography.labelSmall
)
Text(text = state.wheelSizeDisplay, style = MaterialTheme.typography.bodyMedium)
Text(text = state.wheelSize.name, style = MaterialTheme.typography.bodyMedium)
}
Icon(Icons.Default.ArrowDropDown, contentDescription = "")

View File

@@ -1,52 +1,61 @@
package no.nordicsemi.android.csc.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
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.view.CSCState
import no.nordicsemi.android.csc.view.CSCViewEvent
import no.nordicsemi.android.csc.view.OnCloseSelectWheelSizeDialog
import no.nordicsemi.android.csc.view.DisplayDataState
import no.nordicsemi.android.csc.view.LoadingState
import no.nordicsemi.android.csc.view.OnDisconnectButtonClick
import no.nordicsemi.android.csc.view.OnSelectedSpeedUnitSelected
import no.nordicsemi.android.csc.view.OnShowEditWheelSizeDialogButtonClick
import no.nordicsemi.android.csc.view.OnWheelSizeSelected
import no.nordicsemi.android.service.BleManagerStatus
import no.nordicsemi.android.utils.exhaustive
import javax.inject.Inject
@HiltViewModel
internal class CSCViewModel @Inject constructor(
private val dataHolder: CSCRepository
private val repository: CSCRepository
) : ViewModel() {
val state = dataHolder.data
val state = repository.data.combine(repository.status) { data, status ->
when (status) {
BleManagerStatus.CONNECTING -> CSCState(LoadingState)
BleManagerStatus.OK -> CSCState(DisplayDataState(data))
BleManagerStatus.DISCONNECTED -> CSCState(DisplayDataState(data), false)
}
}.stateIn(viewModelScope, SharingStarted.Lazily, CSCState(LoadingState))
fun onEvent(event: CSCViewEvent) {
when (event) {
is OnSelectedSpeedUnitSelected -> onSelectedSpeedUnit(event)
OnShowEditWheelSizeDialogButtonClick -> onShowDialogEvent()
is OnWheelSizeSelected -> onWheelSizeChanged(event)
OnDisconnectButtonClick -> onDisconnectButtonClick()
OnCloseSelectWheelSizeDialog -> onHideDialogEvent()
}.exhaustive
}
private fun onSelectedSpeedUnit(event: OnSelectedSpeedUnitSelected) {
dataHolder.setSpeedUnit(event.selectedSpeedUnit)
}
private fun onShowDialogEvent() {
dataHolder.setDisplayWheelSizeDialog()
repository.setSpeedUnit(event.selectedSpeedUnit)
}
private fun onWheelSizeChanged(event: OnWheelSizeSelected) {
dataHolder.setWheelSize(event.wheelSize, event.wheelSizeDisplayInfo)
repository.sendNewServiceCommand(SetWheelSizeCommand(event.wheelSize))
}
private fun onDisconnectButtonClick() {
finish()
dataHolder.clear()
repository.sendNewServiceCommand(DisconnectCommand)
repository.clear()
}
private fun onHideDialogEvent() {
dataHolder.setHideWheelSizeDialog()
override fun onCleared() {
super.onCleared()
repository.clear()
}
}