mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2025-12-19 15:34:26 +01:00
Add GLS feature
This commit is contained in:
@@ -52,6 +52,7 @@ dependencies {
|
||||
//https://github.com/google/dagger/issues/2123
|
||||
implementation project(":feature_csc")
|
||||
implementation project(":feature_hrs")
|
||||
implementation project(":feature_gls")
|
||||
implementation project(':feature_scanner')
|
||||
implementation project(":lib_theme")
|
||||
implementation project(":lib_utils")
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import no.nordicsemi.android.csc.view.CscScreen
|
||||
import no.nordicsemi.android.gls.view.GLSScreen
|
||||
import no.nordicsemi.android.hrs.view.HRSScreen
|
||||
import no.nordicsemi.android.scanner.view.BluetoothNotAvailableScreen
|
||||
import no.nordicsemi.android.scanner.view.BluetoothNotEnabledScreen
|
||||
@@ -29,7 +30,7 @@ import no.nordicsemi.android.scanner.view.ScanDeviceScreenResult
|
||||
import no.nordicsemi.android.utils.exhaustive
|
||||
|
||||
@Composable
|
||||
fun HomeScreen() {
|
||||
internal fun HomeScreen() {
|
||||
val navController = rememberNavController()
|
||||
|
||||
val viewModel = hiltViewModel<NavigationViewModel>()
|
||||
@@ -42,6 +43,7 @@ fun HomeScreen() {
|
||||
composable(NavDestination.HOME.id) { HomeView { viewModel.navigate(it) } }
|
||||
composable(NavDestination.CSC.id) { CscScreen { viewModel.navigateUp() } }
|
||||
composable(NavDestination.HRS.id) { HRSScreen { viewModel.navigateUp() } }
|
||||
composable(NavDestination.GLS.id) { GLSScreen { viewModel.navigateUp() } }
|
||||
composable(NavDestination.REQUEST_PERMISSION.id) { RequestPermissionScreen(continueAction) }
|
||||
composable(NavDestination.BLUETOOTH_NOT_AVAILABLE.id) { BluetoothNotAvailableScreen() }
|
||||
composable(NavDestination.BLUETOOTH_NOT_ENABLED.id) {
|
||||
@@ -69,6 +71,7 @@ fun HomeView(callback: (NavDestination) -> Unit) {
|
||||
|
||||
FeatureButton(R.drawable.ic_csc, R.string.csc_module) { callback(NavDestination.CSC) }
|
||||
FeatureButton(R.drawable.ic_hrs, R.string.hrs_module) { callback(NavDestination.HRS) }
|
||||
FeatureButton(R.drawable.ic_gls, R.string.gls_module) { callback(NavDestination.GLS) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ enum class NavDestination(val id: String) {
|
||||
HOME("home-screen"),
|
||||
CSC("csc-screen"),
|
||||
HRS("hrs-screen"),
|
||||
GLS("gls-screen"),
|
||||
REQUEST_PERMISSION("request-permission"),
|
||||
BLUETOOTH_NOT_AVAILABLE("bluetooth-not-available"),
|
||||
BLUETOOTH_NOT_ENABLED("bluetooth-not-enabled"),
|
||||
|
||||
@@ -14,7 +14,7 @@ import javax.inject.Inject
|
||||
class NavigationViewModel @Inject constructor(
|
||||
private val bleScanner: NordicBleScanner,
|
||||
private val permissionHelper: PermissionHelper,
|
||||
private val selectedDevice: no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
|
||||
private val selectedDevice: SelectedBluetoothDeviceHolder
|
||||
): ViewModel() {
|
||||
|
||||
val state= MutableStateFlow(NavDestination.HOME)
|
||||
|
||||
12
app/src/main/res/drawable/ic_gls.xml
Normal file
12
app/src/main/res/drawable/ic_gls.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="80dp"
|
||||
android:height="80dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:fillColor="#00B3DC"
|
||||
android:pathData="M386.1,310.1c0,-21.9 -5.5,-43.7 -15.9,-63c-0.2,-0.4 -0.5,-0.9 -0.7,-1.3c-2.4,-4.3 -5,-8.4 -7.8,-12.4C311,152.8 276,96.7 271.7,89.2c-4.3,-8.4 -12.5,-14.3 -22.1,-15.4c-11,-1.3 -21.7,3.9 -27.5,13.3L133,230.6c-4.8,6.4 -8.9,13.2 -12.5,20.3c-0.1,0.2 -0.2,0.4 -0.3,0.7c-9.1,18.4 -13.6,38.1 -13.6,58.6c0,74.9 62.7,135.9 139.8,135.9S386.1,385 386.1,310.1zM163.5,310.1c0,-11.8 2.7,-23.1 7.9,-33.6c0,0 0,0 0,-0.1c2.2,-4.3 4.8,-8.6 7.8,-12.5c0.5,-0.7 1,-1.4 1.5,-2.2l65.4,-105.3c4,6.3 8.5,13.6 13.8,22.1c19.9,31.8 42.3,67.5 53.9,85.9c0.3,0.5 0.6,1 0.9,1.4c1.8,2.5 3.5,5.2 5,7.9c0,0 0,0 0,0c0,0 0,0.1 0,0.1c6.2,11.3 9.3,23.5 9.3,36.3c0,43.5 -37.1,78.9 -82.8,78.9S163.5,353.6 163.5,310.1z" />
|
||||
<path
|
||||
android:fillColor="#00B3DC"
|
||||
android:pathData="M975.7,103c-50.2,-48.2 -130.2,-46.5 -178.4,3.7L228.8,699.8c-10.9,11.4 -10.5,29.4 0.8,40.3l49.9,47.9c-1.7,1.2 -3.4,2.7 -4.8,4.3l-91.4,102.9c-10.5,11.8 -9.4,29.8 2.4,40.2c5.4,4.8 12.2,7.2 18.9,7.2c7.9,0 15.7,-3.2 21.3,-9.6l91.4,-102.9c1,-1.1 1.8,-2.2 2.6,-3.4l50.7,48.7c5.3,5.1 12.4,7.9 19.7,7.9c0.2,0 0.4,0 0.6,0c7.6,-0.2 14.8,-3.3 20,-8.8l568.4,-593c0,0 0.1,-0.1 0.1,-0.1C1027.6,231.2 1025.9,151.2 975.7,103zM938.3,241.9C938.3,241.9 938.3,241.9 938.3,241.9L389.5,814.5l-99.8,-95.8l548.8,-572.5c26.4,-27.5 70.3,-28.4 97.8,-2C963.8,170.5 964.7,214.3 938.3,241.9z" />
|
||||
</vector>
|
||||
@@ -1,4 +1,9 @@
|
||||
<vector android:height="80dp" android:viewportHeight="1024"
|
||||
android:viewportWidth="1024" android:width="80dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#00B3DC" android:pathData="M863.6,210.5c-42.2,-42.2 -98.4,-65.5 -158.2,-65.5c-57.2,0 -111.7,21.6 -153.3,60.8c-16.9,16 -31.1,34.2 -42.2,54c-11.1,-19.9 -25.2,-38.1 -42.2,-54c-41.6,-39.2 -96.1,-60.8 -153.3,-60.8c-59.7,0 -115.9,23.3 -158.2,65.5c-42.2,42.2 -65.5,98.4 -65.5,158.2c0,41.7 11.5,82.3 33.4,117.6c0.3,0.5 0.6,1.1 1,1.6C200,607.1 476.7,896 488.4,908.3c5.6,5.8 13.1,8.8 20.6,8.8c0.3,0 0.6,0 0.9,0c0.3,0 0.6,0 0.9,0c7.5,0 15,-2.9 20.6,-8.8c11.8,-12.3 288.5,-301.2 363.3,-420.5c0.3,-0.5 0.7,-1 1,-1.5c21.9,-35.3 33.4,-76 33.4,-117.6C929.1,308.9 905.9,252.7 863.6,210.5zM846.9,456.9C846.8,456.9 846.8,456.9 846.9,456.9c-0.1,0.1 -0.1,0.1 -0.1,0.1c-60.1,96.2 -271.2,321.8 -336.9,391.2c-65.7,-69.4 -276.8,-295 -336.8,-391.2c0,0 0,0 0,0c0,0 0,-0.1 -0.1,-0.1c-16.5,-26.4 -25.2,-56.9 -25.2,-88.2c0,-91.9 74.8,-166.7 166.7,-166.7c87.7,0 160.7,68.5 166.3,155.9c1,15.3 13.9,27.1 29.2,26.7c15.3,0.4 28.2,-11.3 29.2,-26.7c5.6,-87.4 78.6,-155.9 166.3,-155.9c91.9,0 166.7,74.8 166.7,166.7C872.1,399.9 863.4,430.4 846.9,456.9z"/>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="80dp"
|
||||
android:height="80dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:fillColor="#00B3DC"
|
||||
android:pathData="M863.6,210.5c-42.2,-42.2 -98.4,-65.5 -158.2,-65.5c-57.2,0 -111.7,21.6 -153.3,60.8c-16.9,16 -31.1,34.2 -42.2,54c-11.1,-19.9 -25.2,-38.1 -42.2,-54c-41.6,-39.2 -96.1,-60.8 -153.3,-60.8c-59.7,0 -115.9,23.3 -158.2,65.5c-42.2,42.2 -65.5,98.4 -65.5,158.2c0,41.7 11.5,82.3 33.4,117.6c0.3,0.5 0.6,1.1 1,1.6C200,607.1 476.7,896 488.4,908.3c5.6,5.8 13.1,8.8 20.6,8.8c0.3,0 0.6,0 0.9,0c0.3,0 0.6,0 0.9,0c7.5,0 15,-2.9 20.6,-8.8c11.8,-12.3 288.5,-301.2 363.3,-420.5c0.3,-0.5 0.7,-1 1,-1.5c21.9,-35.3 33.4,-76 33.4,-117.6C929.1,308.9 905.9,252.7 863.6,210.5zM846.9,456.9C846.8,456.9 846.8,456.9 846.9,456.9c-0.1,0.1 -0.1,0.1 -0.1,0.1c-60.1,96.2 -271.2,321.8 -336.9,391.2c-65.7,-69.4 -276.8,-295 -336.8,-391.2c0,0 0,0 0,0c0,0 0,-0.1 -0.1,-0.1c-16.5,-26.4 -25.2,-56.9 -25.2,-88.2c0,-91.9 74.8,-166.7 166.7,-166.7c87.7,0 160.7,68.5 166.3,155.9c1,15.3 13.9,27.1 29.2,26.7c15.3,0.4 28.2,-11.3 29.2,-26.7c5.6,-87.4 78.6,-155.9 166.3,-155.9c91.9,0 166.7,74.8 166.7,166.7C872.1,399.9 863.4,430.4 846.9,456.9z" />
|
||||
</vector>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<resources>
|
||||
<string name="csc_module">CSC</string>
|
||||
<string name="hrs_module">HRS</string>
|
||||
<string name="gls_module">GLS</string>
|
||||
</resources>
|
||||
@@ -1,10 +1,10 @@
|
||||
package no.nordicsemi.android.csc.viewmodel
|
||||
package no.nordicsemi.android.csc.data
|
||||
|
||||
import no.nordicsemi.android.csc.view.CSCSettings
|
||||
import no.nordicsemi.android.csc.view.SpeedUnit
|
||||
import java.util.*
|
||||
|
||||
internal data class CSCViewState(
|
||||
internal data class CSCData(
|
||||
val showDialog: Boolean = false,
|
||||
val scanDevices: Boolean = false,
|
||||
val selectedSpeedUnit: SpeedUnit = SpeedUnit.M_S,
|
||||
@@ -1,28 +0,0 @@
|
||||
package no.nordicsemi.android.csc.events
|
||||
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
internal sealed class CSCServiceEvent : Parcelable
|
||||
|
||||
@Parcelize
|
||||
internal data class OnDistanceChangedEvent(
|
||||
val bluetoothDevice: BluetoothDevice,
|
||||
val speed: Float,
|
||||
val distance: Float,
|
||||
val totalDistance: Float
|
||||
) : CSCServiceEvent()
|
||||
|
||||
@Parcelize
|
||||
internal data class CrankDataChanged(
|
||||
val bluetoothDevice: BluetoothDevice,
|
||||
val crankCadence: Int,
|
||||
val gearRatio: Float
|
||||
) : CSCServiceEvent()
|
||||
|
||||
@Parcelize
|
||||
internal data class OnBatteryLevelChanged(
|
||||
val device: BluetoothDevice,
|
||||
val batteryLevel: Int
|
||||
) : CSCServiceEvent()
|
||||
@@ -3,13 +3,13 @@ package no.nordicsemi.android.csc.service
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import no.nordicsemi.android.csc.events.CSCServiceEvent
|
||||
import no.nordicsemi.android.csc.data.CSCData
|
||||
import no.nordicsemi.android.service.BluetoothDataReadBroadcast
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
internal class CSCDataReadBroadcast @Inject constructor() : BluetoothDataReadBroadcast<CSCServiceEvent>() {
|
||||
internal class CSCDataReadBroadcast @Inject constructor() : BluetoothDataReadBroadcast<CSCData>() {
|
||||
|
||||
private val _wheelSize = MutableSharedFlow<Int>(
|
||||
replay = 1,
|
||||
|
||||
@@ -5,9 +5,7 @@ import androidx.lifecycle.lifecycleScope
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import no.nordicsemi.android.csc.events.CrankDataChanged
|
||||
import no.nordicsemi.android.csc.events.OnBatteryLevelChanged
|
||||
import no.nordicsemi.android.csc.events.OnDistanceChangedEvent
|
||||
import no.nordicsemi.android.csc.data.CSCData
|
||||
import no.nordicsemi.android.service.ForegroundBleService
|
||||
import no.nordicsemi.android.service.LoggableBleManager
|
||||
import javax.inject.Inject
|
||||
@@ -15,6 +13,8 @@ import javax.inject.Inject
|
||||
@AndroidEntryPoint
|
||||
internal class CSCService : ForegroundBleService<CSCManager>(), CSCManagerCallbacks {
|
||||
|
||||
private var data = CSCData()
|
||||
|
||||
@Inject
|
||||
lateinit var localBroadcast: CSCDataReadBroadcast
|
||||
|
||||
@@ -42,7 +42,7 @@ internal class CSCService : ForegroundBleService<CSCManager>(), CSCManagerCallba
|
||||
distance: Float,
|
||||
speed: Float
|
||||
) {
|
||||
localBroadcast.offer(OnDistanceChangedEvent(bluetoothDevice, speed, distance, totalDistance))
|
||||
localBroadcast.offer(data.copy(speed = speed, distance = distance, totalDistance = totalDistance))
|
||||
}
|
||||
|
||||
override fun onCrankDataChanged(
|
||||
@@ -50,10 +50,10 @@ internal class CSCService : ForegroundBleService<CSCManager>(), CSCManagerCallba
|
||||
crankCadence: Float,
|
||||
gearRatio: Float
|
||||
) {
|
||||
localBroadcast.offer(CrankDataChanged(bluetoothDevice, crankCadence.toInt(), gearRatio))
|
||||
localBroadcast.offer(data.copy(cadence = crankCadence.toInt(), gearRatio = gearRatio))
|
||||
}
|
||||
|
||||
override fun onBatteryLevelChanged(device: BluetoothDevice, batteryLevel: Int) {
|
||||
localBroadcast.offer(OnBatteryLevelChanged(bluetoothDevice, batteryLevel))
|
||||
localBroadcast.offer(data.copy(batteryLevel = batteryLevel))
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,11 @@ 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.viewmodel.CSCViewState
|
||||
import no.nordicsemi.android.csc.data.CSCData
|
||||
import no.nordicsemi.android.theme.NordicColors
|
||||
|
||||
@Composable
|
||||
internal fun ContentView(state: CSCViewState, onEvent: (CSCViewEvent) -> Unit) {
|
||||
internal fun ContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
||||
if (state.showDialog) {
|
||||
SelectWheelSizeDialog { onEvent(it) }
|
||||
}
|
||||
@@ -48,7 +48,7 @@ internal fun ContentView(state: CSCViewState, onEvent: (CSCViewEvent) -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsSection(state: CSCViewState, onEvent: (CSCViewEvent) -> Unit) {
|
||||
private fun SettingsSection(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
||||
Card(
|
||||
backgroundColor = NordicColors.NordicGray4.value(),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
@@ -70,5 +70,5 @@ private fun SettingsSection(state: CSCViewState, onEvent: (CSCViewEvent) -> Unit
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ConnectedPreview() {
|
||||
ContentView(CSCViewState()) { }
|
||||
ContentView(CSCData()) { }
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import no.nordicsemi.android.csc.R
|
||||
import no.nordicsemi.android.csc.service.CSCService
|
||||
import no.nordicsemi.android.csc.viewmodel.CSCViewState
|
||||
import no.nordicsemi.android.csc.data.CSCData
|
||||
import no.nordicsemi.android.csc.viewmodel.CscViewModel
|
||||
import no.nordicsemi.android.utils.isServiceRunning
|
||||
|
||||
@@ -43,7 +43,7 @@ fun CscScreen(finishAction: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CSCView(state: CSCViewState, onEvent: (CSCViewEvent) -> Unit) {
|
||||
private fun CSCView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
||||
Column {
|
||||
TopAppBar(title = { Text(text = stringResource(id = R.string.csc_title)) })
|
||||
|
||||
|
||||
@@ -12,13 +12,13 @@ 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.viewmodel.CSCViewState
|
||||
import no.nordicsemi.android.csc.data.CSCData
|
||||
import no.nordicsemi.android.theme.NordicColors
|
||||
import no.nordicsemi.android.theme.view.BatteryLevelView
|
||||
import no.nordicsemi.android.theme.view.KeyValueField
|
||||
|
||||
@Composable
|
||||
internal fun SensorsReadingView(state: CSCViewState) {
|
||||
internal fun SensorsReadingView(state: CSCData) {
|
||||
Card(
|
||||
backgroundColor = NordicColors.NordicGray4.value(),
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
@@ -48,5 +48,5 @@ internal fun SensorsReadingView(state: CSCViewState) {
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
SensorsReadingView(CSCViewState())
|
||||
SensorsReadingView(CSCData())
|
||||
}
|
||||
|
||||
@@ -12,10 +12,10 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import no.nordicsemi.android.csc.R
|
||||
import no.nordicsemi.android.csc.viewmodel.CSCViewState
|
||||
import no.nordicsemi.android.csc.data.CSCData
|
||||
|
||||
@Composable
|
||||
internal fun WheelSizeView(state: CSCViewState, onEvent: (CSCViewEvent) -> Unit) {
|
||||
internal fun WheelSizeView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = state.wheelSize,
|
||||
@@ -36,5 +36,5 @@ private fun EditIcon(onEvent: (CSCViewEvent) -> Unit) {
|
||||
@Preview
|
||||
@Composable
|
||||
private fun WheelSizeViewPreview() {
|
||||
WheelSizeView(CSCViewState()) { }
|
||||
WheelSizeView(CSCData()) { }
|
||||
}
|
||||
|
||||
@@ -8,10 +8,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.withContext
|
||||
import no.nordicsemi.android.csc.events.CSCServiceEvent
|
||||
import no.nordicsemi.android.csc.events.CrankDataChanged
|
||||
import no.nordicsemi.android.csc.events.OnBatteryLevelChanged
|
||||
import no.nordicsemi.android.csc.events.OnDistanceChangedEvent
|
||||
import no.nordicsemi.android.csc.data.CSCData
|
||||
import no.nordicsemi.android.csc.service.CSCDataReadBroadcast
|
||||
import no.nordicsemi.android.csc.view.CSCViewEvent
|
||||
import no.nordicsemi.android.csc.view.OnDisconnectButtonClick
|
||||
@@ -26,44 +23,14 @@ internal class CscViewModel @Inject constructor(
|
||||
private val localBroadcast: CSCDataReadBroadcast
|
||||
) : ViewModel() {
|
||||
|
||||
val state = MutableStateFlow(CSCViewState())
|
||||
val state = MutableStateFlow(CSCData())
|
||||
|
||||
init {
|
||||
localBroadcast.events.onEach {
|
||||
withContext(Dispatchers.Main) { consumeEvent(it) }
|
||||
withContext(Dispatchers.Main) { state.value = it }
|
||||
}.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
private fun consumeEvent(event: CSCServiceEvent) {
|
||||
val newValue = when (event) {
|
||||
is CrankDataChanged -> createNewState(event)
|
||||
is OnBatteryLevelChanged -> createNewState(event)
|
||||
is OnDistanceChangedEvent -> createNewState(event)
|
||||
}
|
||||
state.value = newValue
|
||||
}
|
||||
|
||||
private fun createNewState(event: CrankDataChanged): CSCViewState {
|
||||
return state.value.copy(
|
||||
cadence = event.crankCadence,
|
||||
gearRatio = event.gearRatio
|
||||
)
|
||||
}
|
||||
|
||||
private fun createNewState(event: OnBatteryLevelChanged): CSCViewState {
|
||||
return state.value.copy(
|
||||
batteryLevel = event.batteryLevel
|
||||
)
|
||||
}
|
||||
|
||||
private fun createNewState(event: OnDistanceChangedEvent): CSCViewState {
|
||||
return state.value.copy(
|
||||
speed = event.speed,
|
||||
distance = event.distance,
|
||||
totalDistance = event.totalDistance
|
||||
)
|
||||
}
|
||||
|
||||
fun onEvent(event: CSCViewEvent) {
|
||||
when (event) {
|
||||
is OnSelectedSpeedUnitSelected -> onSelectedSpeedUnit(event)
|
||||
|
||||
28
feature_gls/build.gradle
Normal file
28
feature_gls/build.gradle
Normal file
@@ -0,0 +1,28 @@
|
||||
apply from: rootProject.file("library.gradle")
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
|
||||
dependencies {
|
||||
implementation project(":lib_service")
|
||||
implementation project(":lib_theme")
|
||||
implementation project(":lib_utils")
|
||||
|
||||
implementation libs.chart
|
||||
|
||||
implementation libs.nordic.ble.common
|
||||
|
||||
implementation libs.nordic.log
|
||||
|
||||
implementation libs.bundles.compose
|
||||
implementation libs.androidx.core
|
||||
implementation libs.material
|
||||
implementation libs.lifecycle.activity
|
||||
implementation libs.lifecycle.service
|
||||
implementation libs.compose.lifecycle
|
||||
implementation libs.compose.activity
|
||||
|
||||
testImplementation libs.test.junit
|
||||
androidTestImplementation libs.android.test.junit
|
||||
androidTestImplementation libs.android.test.espresso
|
||||
androidTestImplementation libs.android.test.compose.ui
|
||||
debugImplementation libs.android.test.compose.tooling
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package no.nordicsemi.android.gls
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("no.nordicsemi.android.gls.test", appContext.packageName)
|
||||
}
|
||||
}
|
||||
5
feature_gls/src/main/AndroidManifest.xml
Normal file
5
feature_gls/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="no.nordicsemi.android.gls">
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,11 @@
|
||||
package no.nordicsemi.android.gls.data
|
||||
|
||||
internal data class GLSData(
|
||||
val record: List<GLSRecord> = emptyList(),
|
||||
val batteryLevel: Int = 0,
|
||||
val requestStatus: RequestStatus = RequestStatus.IDLE
|
||||
)
|
||||
|
||||
internal enum class RequestStatus {
|
||||
IDLE, PENDING, SUCCESS, ABORTED, FAILED, NOT_SUPPORTED
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright (c) 2015, 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.gls.data
|
||||
|
||||
import java.util.*
|
||||
|
||||
internal data class GLSRecord(
|
||||
/** Record sequence number */
|
||||
val sequenceNumber: Int = 0,
|
||||
|
||||
/** The base time of the measurement */
|
||||
val time: Calendar? = null,
|
||||
|
||||
/** The glucose concentration. 0 if not present */
|
||||
val glucoseConcentration: Float = 0f,
|
||||
|
||||
/** Concentration unit. One of the following: [GLSRecord.UNIT_kgpl], [GLSRecord.UNIT_molpl] */
|
||||
val unit: ConcentrationUnit = ConcentrationUnit.UNIT_KGPL,
|
||||
|
||||
/** The type of the record. 0 if not present */
|
||||
val type: Int = 0,
|
||||
|
||||
/** The sample location. 0 if unknown */
|
||||
val sampleLocation: Int = 0,
|
||||
|
||||
/** Sensor status annunciation flags. 0 if not present */
|
||||
val status: Int = 0,
|
||||
|
||||
var context: MeasurementContext? = null
|
||||
)
|
||||
|
||||
internal data class MeasurementContext(
|
||||
|
||||
val carbohydrateId: CarbohydrateId = CarbohydrateId.NOT_PRESENT,
|
||||
|
||||
/** Number of kilograms of carbohydrate */
|
||||
val carbohydrateUnits: Float = 0f,
|
||||
|
||||
val meal: TypeOfMeal = TypeOfMeal.NOT_PRESENT,
|
||||
|
||||
val tester: TestType = TestType.NOT_PRESENT,
|
||||
|
||||
val health: HealthStatus = HealthStatus.NOT_PRESENT,
|
||||
|
||||
/** Exercise duration in seconds. 0 if not present */
|
||||
val exerciseDuration: Int = 0,
|
||||
|
||||
/** Exercise intensity in percent. 0 if not present */
|
||||
val exerciseIntensity: Int = 0,
|
||||
|
||||
val medicationId: MedicationId = MedicationId.NOT_PRESENT,
|
||||
|
||||
/** Quantity of medication. See [.medicationUnit] for the unit. */
|
||||
val medicationQuantity: Float = 0f,
|
||||
|
||||
/** One of the following: [MeasurementContext.UNIT_kg], [MeasurementContext.UNIT_l]. */
|
||||
val medicationUnit: MedicationUnit = MedicationUnit.UNIT_KG,
|
||||
|
||||
/** HbA1c value. 0 if not present */
|
||||
val HbA1c: Float = 0f
|
||||
)
|
||||
|
||||
internal enum class ConcentrationUnit(val id: Int) {
|
||||
UNIT_KGPL(0),
|
||||
UNIT_MOLPL(1);
|
||||
|
||||
companion object {
|
||||
fun create(value: Int): ConcentrationUnit {
|
||||
return values().firstOrNull { it.id == value }
|
||||
?: throw IllegalArgumentException("Cannot find element for provided value.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal enum class CarbohydrateId(val id: Int) {
|
||||
NOT_PRESENT(0),
|
||||
BREAKFAST(1),
|
||||
LUNCH(2),
|
||||
DINNER(3),
|
||||
SNACK(4),
|
||||
DRINK(5),
|
||||
SUPPER(6),
|
||||
BRUNCH(7);
|
||||
|
||||
companion object {
|
||||
fun create(value: Byte): CarbohydrateId {
|
||||
return values().firstOrNull { it.id == value.toInt() }
|
||||
?: throw IllegalArgumentException("Cannot find element for provided value.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal enum class TypeOfMeal(val id: Int) {
|
||||
NOT_PRESENT(0),
|
||||
PREPRANDIAL(1),
|
||||
POSTPRANDIAL(2),
|
||||
FASTING(3),
|
||||
CASUAL(4),
|
||||
BEDTIME(5);
|
||||
|
||||
companion object {
|
||||
fun create(value: Byte): TypeOfMeal {
|
||||
return values().firstOrNull { it.id == value.toInt() }
|
||||
?: throw IllegalArgumentException("Cannot find element for provided value.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal enum class TestType(val id: Int) {
|
||||
NOT_PRESENT(0),
|
||||
SELF(1),
|
||||
HEALTH_CARE_PROFESSIONAL(2),
|
||||
LAB_TEST(3),
|
||||
VALUE_NOT_AVAILABLE(15);
|
||||
|
||||
companion object {
|
||||
fun create(value: Byte): TestType {
|
||||
return values().firstOrNull { it.id == value.toInt() }
|
||||
?: throw IllegalArgumentException("Cannot find element for provided value.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal enum class HealthStatus(val id: Int) {
|
||||
NOT_PRESENT(0),
|
||||
MINOR_HEALTH_ISSUES(1),
|
||||
MAJOR_HEALTH_ISSUES(2),
|
||||
DURING_MENSES(3),
|
||||
UNDER_STRESS(4),
|
||||
NO_HEALTH_ISSUES(5),
|
||||
VALUE_NOT_AVAILABLE(15);
|
||||
|
||||
companion object {
|
||||
fun create(value: Byte): HealthStatus {
|
||||
return values().firstOrNull { it.id == value.toInt() }
|
||||
?: throw IllegalArgumentException("Cannot find element for provided value.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal enum class MedicationId(val id: Int) {
|
||||
NOT_PRESENT(0),
|
||||
RAPID_ACTING_INSULIN(1),
|
||||
SHORT_ACTING_INSULIN(2),
|
||||
INTERMEDIATE_ACTING_INSULIN(3),
|
||||
LONG_ACTING_INSULIN(4),
|
||||
PRE_MIXED_INSULIN(5);
|
||||
|
||||
companion object {
|
||||
fun create(value: Byte): MedicationId {
|
||||
return values().firstOrNull { it.id == value.toInt() }
|
||||
?: throw IllegalArgumentException("Cannot find element for provided value.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal enum class MedicationUnit(val id: Int) {
|
||||
UNIT_KG(0),
|
||||
UNIT_L(1);
|
||||
|
||||
companion object {
|
||||
fun create(value: Int): MedicationUnit {
|
||||
return values().firstOrNull { it.id == value }
|
||||
?: throw IllegalArgumentException("Cannot find element for provided value.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
/*
|
||||
* Copyright (c) 2015, 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.gls.repository
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothGatt
|
||||
import android.bluetooth.BluetoothGattCharacteristic
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointDataCallback
|
||||
import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementContextDataCallback
|
||||
import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementDataCallback
|
||||
import no.nordicsemi.android.ble.common.data.RecordAccessControlPointData
|
||||
import no.nordicsemi.android.ble.common.profile.RecordAccessControlPointCallback.RACPErrorCode
|
||||
import no.nordicsemi.android.ble.common.profile.RecordAccessControlPointCallback.RACPOpCode
|
||||
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementCallback.GlucoseStatus
|
||||
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Carbohydrate
|
||||
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Health
|
||||
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Meal
|
||||
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Medication
|
||||
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Tester
|
||||
import no.nordicsemi.android.ble.data.Data
|
||||
import no.nordicsemi.android.gls.data.CarbohydrateId
|
||||
import no.nordicsemi.android.gls.data.ConcentrationUnit
|
||||
import no.nordicsemi.android.gls.data.GLSData
|
||||
import no.nordicsemi.android.gls.data.GLSRecord
|
||||
import no.nordicsemi.android.gls.data.HealthStatus
|
||||
import no.nordicsemi.android.gls.data.MeasurementContext
|
||||
import no.nordicsemi.android.gls.data.MedicationId
|
||||
import no.nordicsemi.android.gls.data.MedicationUnit
|
||||
import no.nordicsemi.android.gls.data.RequestStatus
|
||||
import no.nordicsemi.android.gls.data.TestType
|
||||
import no.nordicsemi.android.gls.data.TypeOfMeal
|
||||
import no.nordicsemi.android.log.LogContract
|
||||
import no.nordicsemi.android.service.BatteryManager
|
||||
import no.nordicsemi.android.service.BatteryManagerCallbacks
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** Glucose service UUID */
|
||||
private val GLS_SERVICE_UUID = UUID.fromString("00001808-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
/** Glucose Measurement characteristic UUID */
|
||||
private val GM_CHARACTERISTIC = UUID.fromString("00002A18-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
/** Glucose Measurement Context characteristic UUID */
|
||||
private val GM_CONTEXT_CHARACTERISTIC =
|
||||
UUID.fromString("00002A34-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
/** Glucose Feature characteristic UUID */
|
||||
private val GF_CHARACTERISTIC = UUID.fromString("00002A51-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
/** Record Access Control Point characteristic UUID */
|
||||
private val RACP_CHARACTERISTIC = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
@Singleton
|
||||
internal class GLSManager @Inject constructor(
|
||||
@ApplicationContext context: Context
|
||||
) : BatteryManager<BatteryManagerCallbacks?>(context) {
|
||||
|
||||
val data = MutableStateFlow(GLSData())
|
||||
private val records = hashMapOf<Int, GLSRecord>()
|
||||
|
||||
private var glucoseMeasurementCharacteristic: BluetoothGattCharacteristic? = null
|
||||
private var glucoseMeasurementContextCharacteristic: BluetoothGattCharacteristic? = null
|
||||
private var recordAccessControlPointCharacteristic: BluetoothGattCharacteristic? = null
|
||||
|
||||
override fun getGattCallback(): BatteryManagerGattCallback {
|
||||
return GlucoseManagerGattCallback()
|
||||
}
|
||||
|
||||
/**
|
||||
* BluetoothGatt callbacks for connection/disconnection, service discovery,
|
||||
* receiving notification, etc.
|
||||
*/
|
||||
private inner class GlucoseManagerGattCallback : BatteryManagerGattCallback() {
|
||||
override fun initialize() {
|
||||
super.initialize()
|
||||
|
||||
// The gatt.setCharacteristicNotification(...) method is called in BleManager during
|
||||
// enabling notifications or indications
|
||||
// (see BleManager#internalEnableNotifications/Indications).
|
||||
// However, on Samsung S3 with Android 4.3 it looks like the 2 gatt calls
|
||||
// (gatt.setCharacteristicNotification(...) and gatt.writeDescriptor(...)) are called
|
||||
// too quickly, or from a wrong thread, and in result the notification listener is not
|
||||
// set, causing onCharacteristicChanged(...) callback never being called when a
|
||||
// notification comes. Enabling them here, like below, solves the problem.
|
||||
// However... the original approach works for the Battery Level CCCD, which makes it
|
||||
// even weirder.
|
||||
/*
|
||||
gatt.setCharacteristicNotification(glucoseMeasurementCharacteristic, true);
|
||||
if (glucoseMeasurementContextCharacteristic != null) {
|
||||
device.setCharacteristicNotification(glucoseMeasurementContextCharacteristic, true);
|
||||
}
|
||||
device.setCharacteristicNotification(recordAccessControlPointCharacteristic, true);
|
||||
*/
|
||||
setNotificationCallback(glucoseMeasurementCharacteristic)
|
||||
.with(object : GlucoseMeasurementDataCallback() {
|
||||
|
||||
override fun onGlucoseMeasurementReceived(
|
||||
device: BluetoothDevice, sequenceNumber: Int,
|
||||
time: Calendar, glucoseConcentration: Float?,
|
||||
unit: Int?, type: Int?,
|
||||
sampleLocation: Int?, status: GlucoseStatus?,
|
||||
contextInformationFollows: Boolean
|
||||
) {
|
||||
val record = GLSRecord(
|
||||
sequenceNumber = sequenceNumber,
|
||||
time = time,
|
||||
glucoseConcentration = glucoseConcentration ?: 0f,
|
||||
unit = unit?.let { ConcentrationUnit.create(it) }
|
||||
?: ConcentrationUnit.UNIT_KGPL,
|
||||
type = type ?: 0,
|
||||
sampleLocation = sampleLocation ?: 0,
|
||||
status = status?.value ?: 0
|
||||
)
|
||||
|
||||
records[record.sequenceNumber] = record
|
||||
if (!contextInformationFollows) {
|
||||
data.tryEmit(data.value.copy(record = records.values.toList()))
|
||||
}
|
||||
}
|
||||
})
|
||||
setNotificationCallback(glucoseMeasurementContextCharacteristic)
|
||||
.with(object : GlucoseMeasurementContextDataCallback() {
|
||||
|
||||
override fun onGlucoseMeasurementContextReceived(
|
||||
device: BluetoothDevice, sequenceNumber: Int,
|
||||
carbohydrate: Carbohydrate?, carbohydrateAmount: Float?,
|
||||
meal: Meal?, tester: Tester?,
|
||||
health: Health?, exerciseDuration: Int?,
|
||||
exerciseIntensity: Int?, medication: Medication?,
|
||||
medicationAmount: Float?, medicationUnit: Int?,
|
||||
HbA1c: Float?
|
||||
) {
|
||||
val record = records[sequenceNumber] ?: return
|
||||
|
||||
val context = MeasurementContext(
|
||||
carbohydrateId = carbohydrate?.value?.let { CarbohydrateId.create(it) }
|
||||
?: CarbohydrateId.NOT_PRESENT,
|
||||
carbohydrateUnits = carbohydrateAmount ?: 0f,
|
||||
meal = meal?.value?.let { TypeOfMeal.create(it) }
|
||||
?: TypeOfMeal.NOT_PRESENT,
|
||||
tester = tester?.value?.let { TestType.create(it) }
|
||||
?: TestType.NOT_PRESENT,
|
||||
health = health?.value?.let { HealthStatus.create(it) }
|
||||
?: HealthStatus.NOT_PRESENT,
|
||||
exerciseDuration = exerciseDuration ?: 0,
|
||||
exerciseIntensity = exerciseIntensity ?: 0,
|
||||
medicationId = medication?.value?.let { MedicationId.create(it) }
|
||||
?: MedicationId.NOT_PRESENT,
|
||||
medicationQuantity = medicationAmount ?: 0f,
|
||||
medicationUnit = medicationUnit?.let { MedicationUnit.create(it) }
|
||||
?: MedicationUnit.UNIT_KG,
|
||||
HbA1c = HbA1c ?: 0f
|
||||
)
|
||||
record.context = context
|
||||
|
||||
data.tryEmit(data.value)
|
||||
}
|
||||
})
|
||||
setIndicationCallback(recordAccessControlPointCharacteristic)
|
||||
.with(object : RecordAccessControlPointDataCallback() {
|
||||
|
||||
@SuppressLint("SwitchIntDef")
|
||||
override fun onRecordAccessOperationCompleted(
|
||||
device: BluetoothDevice,
|
||||
@RACPOpCode requestCode: Int
|
||||
) {
|
||||
val status = when (requestCode) {
|
||||
RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED
|
||||
else -> RequestStatus.SUCCESS
|
||||
}
|
||||
data.tryEmit(data.value.copy(requestStatus = status))
|
||||
}
|
||||
|
||||
override fun onRecordAccessOperationCompletedWithNoRecordsFound(
|
||||
device: BluetoothDevice,
|
||||
@RACPOpCode requestCode: Int
|
||||
) {
|
||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.SUCCESS))
|
||||
}
|
||||
|
||||
override fun onNumberOfRecordsReceived(
|
||||
device: BluetoothDevice,
|
||||
numberOfRecords: Int
|
||||
) {
|
||||
//TODO("Probably not needed")
|
||||
// mCallbacks!!.onNumberOfRecordsRequested(device, numberOfRecords)
|
||||
if (numberOfRecords > 0) {
|
||||
if (records.size > 0) {
|
||||
val sequenceNumber = records.keys.last() + 1
|
||||
writeCharacteristic(
|
||||
recordAccessControlPointCharacteristic,
|
||||
RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(
|
||||
sequenceNumber
|
||||
)
|
||||
)
|
||||
.enqueue()
|
||||
} else {
|
||||
writeCharacteristic(
|
||||
recordAccessControlPointCharacteristic,
|
||||
RecordAccessControlPointData.reportAllStoredRecords()
|
||||
)
|
||||
.enqueue()
|
||||
}
|
||||
} else {
|
||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.SUCCESS))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRecordAccessOperationError(
|
||||
device: BluetoothDevice,
|
||||
@RACPOpCode requestCode: Int,
|
||||
@RACPErrorCode errorCode: Int
|
||||
) {
|
||||
log(Log.WARN, "Record Access operation failed (error $errorCode)")
|
||||
if (errorCode == RACP_ERROR_OP_CODE_NOT_SUPPORTED) {
|
||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.NOT_SUPPORTED))
|
||||
} else {
|
||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.FAILED))
|
||||
}
|
||||
}
|
||||
})
|
||||
enableNotifications(glucoseMeasurementCharacteristic).enqueue()
|
||||
enableNotifications(glucoseMeasurementContextCharacteristic).enqueue()
|
||||
enableIndications(recordAccessControlPointCharacteristic)
|
||||
.fail { device: BluetoothDevice?, status: Int ->
|
||||
log(
|
||||
Log.WARN,
|
||||
"Failed to enabled Record Access Control Point indications (error $status)"
|
||||
)
|
||||
}
|
||||
.enqueue()
|
||||
}
|
||||
|
||||
public override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
|
||||
val service = gatt.getService(GLS_SERVICE_UUID)
|
||||
if (service != null) {
|
||||
glucoseMeasurementCharacteristic = service.getCharacteristic(GM_CHARACTERISTIC)
|
||||
glucoseMeasurementContextCharacteristic = service.getCharacteristic(
|
||||
GM_CONTEXT_CHARACTERISTIC
|
||||
)
|
||||
recordAccessControlPointCharacteristic = service.getCharacteristic(
|
||||
RACP_CHARACTERISTIC
|
||||
)
|
||||
}
|
||||
return glucoseMeasurementCharacteristic != null && recordAccessControlPointCharacteristic != null
|
||||
}
|
||||
|
||||
override fun onServicesInvalidated() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isOptionalServiceSupported(gatt: BluetoothGatt): Boolean {
|
||||
super.isOptionalServiceSupported(gatt)
|
||||
return glucoseMeasurementContextCharacteristic != null
|
||||
}
|
||||
|
||||
override fun onDeviceDisconnected() {
|
||||
glucoseMeasurementCharacteristic = null
|
||||
glucoseMeasurementContextCharacteristic = null
|
||||
recordAccessControlPointCharacteristic = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the records list locally.
|
||||
*/
|
||||
fun clear() {
|
||||
records.clear()
|
||||
val target = bluetoothDevice
|
||||
if (target != null) {
|
||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.SUCCESS))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the request to obtain the last (most recent) record from glucose device. The data will
|
||||
* be returned to Glucose Measurement characteristic as a notification followed by Record Access
|
||||
* Control Point indication with status code Success or other in case of error.
|
||||
*/
|
||||
fun lastRecord(): Unit {
|
||||
if (recordAccessControlPointCharacteristic == null) return
|
||||
val target = bluetoothDevice ?: return
|
||||
clear()
|
||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
|
||||
writeCharacteristic(
|
||||
recordAccessControlPointCharacteristic,
|
||||
RecordAccessControlPointData.reportLastStoredRecord()
|
||||
)
|
||||
.with { device: BluetoothDevice, data: Data ->
|
||||
log(
|
||||
LogContract.Log.Level.APPLICATION,
|
||||
"\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent"
|
||||
)
|
||||
}
|
||||
.enqueue()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the request to obtain the first (oldest) record from glucose device. The data will be
|
||||
* returned to Glucose Measurement characteristic as a notification followed by Record Access
|
||||
* Control Point indication with status code Success or other in case of error.
|
||||
*/
|
||||
fun requestFirstRecord(): Unit {
|
||||
if (recordAccessControlPointCharacteristic == null) return
|
||||
val target = bluetoothDevice ?: return
|
||||
clear()
|
||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
|
||||
writeCharacteristic(
|
||||
recordAccessControlPointCharacteristic,
|
||||
RecordAccessControlPointData.reportFirstStoredRecord()
|
||||
)
|
||||
.with { device: BluetoothDevice, data: Data ->
|
||||
log(
|
||||
LogContract.Log.Level.APPLICATION,
|
||||
"\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent"
|
||||
)
|
||||
}
|
||||
.enqueue()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the request to obtain all records from glucose device. Initially we want to notify user
|
||||
* about the number of the records so the 'Report Number of Stored Records' is send. The data
|
||||
* will be returned to Glucose Measurement characteristic as a notification followed by
|
||||
* Record Access Control Point indication with status code Success or other in case of error.
|
||||
*/
|
||||
fun requestAllRecords(): Unit {
|
||||
if (recordAccessControlPointCharacteristic == null) return
|
||||
val target = bluetoothDevice ?: return
|
||||
clear()
|
||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
|
||||
writeCharacteristic(
|
||||
recordAccessControlPointCharacteristic,
|
||||
RecordAccessControlPointData.reportNumberOfAllStoredRecords()
|
||||
)
|
||||
.with { device: BluetoothDevice, data: Data ->
|
||||
log(
|
||||
LogContract.Log.Level.APPLICATION,
|
||||
"\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent"
|
||||
)
|
||||
}
|
||||
.enqueue()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the request to obtain from the glucose device all records newer than the newest one
|
||||
* from local storage. The data will be returned to Glucose Measurement characteristic as
|
||||
* a notification followed by Record Access Control Point indication with status code Success
|
||||
* or other in case of error.
|
||||
*
|
||||
*
|
||||
* Refresh button will not download records older than the oldest in the local memory.
|
||||
* E.g. if you have pressed Last and then Refresh, than it will try to get only newer records.
|
||||
* However if there are no records, it will download all existing (using [.getAllRecords]).
|
||||
*/
|
||||
fun refreshRecords() {
|
||||
if (recordAccessControlPointCharacteristic == null) return
|
||||
val target = bluetoothDevice ?: return
|
||||
if (records.size == 0) {
|
||||
requestAllRecords()
|
||||
} else {
|
||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
|
||||
|
||||
// obtain the last sequence number
|
||||
val sequenceNumber = records.keys.last() + 1
|
||||
writeCharacteristic(
|
||||
recordAccessControlPointCharacteristic,
|
||||
RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber)
|
||||
)
|
||||
.with { device: BluetoothDevice, data: Data ->
|
||||
log(
|
||||
LogContract.Log.Level.APPLICATION,
|
||||
"\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent"
|
||||
)
|
||||
}
|
||||
.enqueue()
|
||||
// Info:
|
||||
// Operators OPERATOR_LESS_THEN_OR_EQUAL and OPERATOR_RANGE are not supported by Nordic Semiconductor Glucose Service in SDK 4.4.2.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends abort operation signal to the device.
|
||||
*/
|
||||
fun abort() {
|
||||
if (recordAccessControlPointCharacteristic == null) return
|
||||
val target = bluetoothDevice ?: return
|
||||
writeCharacteristic(
|
||||
recordAccessControlPointCharacteristic,
|
||||
RecordAccessControlPointData.abortOperation()
|
||||
)
|
||||
.with { device: BluetoothDevice, data: Data ->
|
||||
log(
|
||||
LogContract.Log.Level.APPLICATION,
|
||||
"\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent"
|
||||
)
|
||||
}
|
||||
.enqueue()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the request to delete all data from the device. A Record Access Control Point
|
||||
* indication with status code Success (or other in case of error) will be send.
|
||||
*/
|
||||
fun deleteAllRecords() {
|
||||
if (recordAccessControlPointCharacteristic == null) return
|
||||
val target = bluetoothDevice ?: return
|
||||
clear()
|
||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
|
||||
writeCharacteristic(
|
||||
recordAccessControlPointCharacteristic,
|
||||
RecordAccessControlPointData.deleteAllStoredRecords()
|
||||
)
|
||||
.with { device: BluetoothDevice, data: Data ->
|
||||
log(
|
||||
LogContract.Log.Level.APPLICATION,
|
||||
"\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent"
|
||||
)
|
||||
}
|
||||
.enqueue()
|
||||
|
||||
val elements = listOf<Int>(1, 2, 3)
|
||||
val result = elements.all { it > 3 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright (c) 2015, 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.gls.repository
|
||||
|
||||
import no.nordicsemi.android.ble.data.Data
|
||||
|
||||
object GLSRecordAccessControlPointParser {
|
||||
|
||||
private const val OP_CODE_REPORT_STORED_RECORDS = 1
|
||||
private const val OP_CODE_DELETE_STORED_RECORDS = 2
|
||||
private const val OP_CODE_ABORT_OPERATION = 3
|
||||
private const val OP_CODE_REPORT_NUMBER_OF_RECORDS = 4
|
||||
private const val OP_CODE_NUMBER_OF_STORED_RECORDS_RESPONSE = 5
|
||||
private const val OP_CODE_RESPONSE_CODE = 6
|
||||
private const val OPERATOR_NULL = 0
|
||||
private const val OPERATOR_ALL_RECORDS = 1
|
||||
private const val OPERATOR_LESS_THEN_OR_EQUAL = 2
|
||||
private const val OPERATOR_GREATER_THEN_OR_EQUAL = 3
|
||||
private const val OPERATOR_WITHING_RANGE = 4
|
||||
private const val OPERATOR_FIRST_RECORD = 5
|
||||
private const val OPERATOR_LAST_RECORD = 6
|
||||
private const val RESPONSE_SUCCESS = 1
|
||||
private const val RESPONSE_OP_CODE_NOT_SUPPORTED = 2
|
||||
private const val RESPONSE_INVALID_OPERATOR = 3
|
||||
private const val RESPONSE_OPERATOR_NOT_SUPPORTED = 4
|
||||
private const val RESPONSE_INVALID_OPERAND = 5
|
||||
private const val RESPONSE_NO_RECORDS_FOUND = 6
|
||||
private const val RESPONSE_ABORT_UNSUCCESSFUL = 7
|
||||
private const val RESPONSE_PROCEDURE_NOT_COMPLETED = 8
|
||||
private const val RESPONSE_OPERAND_NOT_SUPPORTED = 9
|
||||
|
||||
fun parse(data: Data): String {
|
||||
val builder = StringBuilder()
|
||||
val opCode = data.getIntValue(Data.FORMAT_UINT8, 0)!!
|
||||
val operator = data.getIntValue(Data.FORMAT_UINT8, 1)!!
|
||||
when (opCode) {
|
||||
OP_CODE_REPORT_STORED_RECORDS, OP_CODE_DELETE_STORED_RECORDS, OP_CODE_ABORT_OPERATION, OP_CODE_REPORT_NUMBER_OF_RECORDS -> builder.append(
|
||||
getOpCode(opCode)
|
||||
).append("\n")
|
||||
OP_CODE_NUMBER_OF_STORED_RECORDS_RESPONSE -> {
|
||||
builder.append(getOpCode(opCode)).append(": ")
|
||||
val value = data.getIntValue(Data.FORMAT_UINT16, 2)!!
|
||||
builder.append(value).append("\n")
|
||||
}
|
||||
OP_CODE_RESPONSE_CODE -> {
|
||||
builder.append(getOpCode(opCode)).append(" for ")
|
||||
val targetOpCode = data.getIntValue(Data.FORMAT_UINT8, 2)!!
|
||||
builder.append(getOpCode(targetOpCode)).append(": ")
|
||||
val status = data.getIntValue(Data.FORMAT_UINT8, 3)!!
|
||||
builder.append(getStatus(status)).append("\n")
|
||||
}
|
||||
}
|
||||
when (operator) {
|
||||
OPERATOR_ALL_RECORDS, OPERATOR_FIRST_RECORD, OPERATOR_LAST_RECORD -> builder.append("Operator: ")
|
||||
.append(
|
||||
getOperator(operator)
|
||||
).append("\n")
|
||||
OPERATOR_GREATER_THEN_OR_EQUAL, OPERATOR_LESS_THEN_OR_EQUAL -> {
|
||||
val filter = data.getIntValue(Data.FORMAT_UINT8, 2)!!
|
||||
val value = data.getIntValue(Data.FORMAT_UINT16, 3)!!
|
||||
builder.append("Operator: ").append(getOperator(operator)).append(" ").append(value)
|
||||
.append(" (filter: ").append(filter).append(")\n")
|
||||
}
|
||||
OPERATOR_WITHING_RANGE -> {
|
||||
val filter = data.getIntValue(Data.FORMAT_UINT8, 2)!!
|
||||
val value1 = data.getIntValue(Data.FORMAT_UINT16, 3)!!
|
||||
val value2 = data.getIntValue(Data.FORMAT_UINT16, 5)!!
|
||||
builder.append("Operator: ").append(getOperator(operator)).append(" ")
|
||||
.append(value1).append("-").append(value2).append(" (filter: ").append(filter)
|
||||
.append(")\n")
|
||||
}
|
||||
}
|
||||
if (builder.isNotEmpty()) {
|
||||
builder.setLength(builder.length - 1)
|
||||
}
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
private fun getOpCode(opCode: Int): String {
|
||||
return when (opCode) {
|
||||
OP_CODE_REPORT_STORED_RECORDS -> "Report stored records"
|
||||
OP_CODE_DELETE_STORED_RECORDS -> "Delete stored records"
|
||||
OP_CODE_ABORT_OPERATION -> "Abort operation"
|
||||
OP_CODE_REPORT_NUMBER_OF_RECORDS -> "Report number of stored records"
|
||||
OP_CODE_NUMBER_OF_STORED_RECORDS_RESPONSE -> "Number of stored records response"
|
||||
OP_CODE_RESPONSE_CODE -> "Response Code"
|
||||
else -> "Reserved for future use"
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOperator(operator: Int): String {
|
||||
return when (operator) {
|
||||
OPERATOR_NULL -> "Null"
|
||||
OPERATOR_ALL_RECORDS -> "All records"
|
||||
OPERATOR_LESS_THEN_OR_EQUAL -> "Less than or equal to"
|
||||
OPERATOR_GREATER_THEN_OR_EQUAL -> "Greater than or equal to"
|
||||
OPERATOR_WITHING_RANGE -> "Within range of"
|
||||
OPERATOR_FIRST_RECORD -> "First record(i.e. oldest record)"
|
||||
OPERATOR_LAST_RECORD -> "Last record (i.e. most recent record)"
|
||||
else -> "Reserved for future use"
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStatus(status: Int): String {
|
||||
return when (status) {
|
||||
RESPONSE_SUCCESS -> "Success"
|
||||
RESPONSE_OP_CODE_NOT_SUPPORTED -> "Operation not supported"
|
||||
RESPONSE_INVALID_OPERATOR -> "Invalid operator"
|
||||
RESPONSE_OPERATOR_NOT_SUPPORTED -> "Operator not supported"
|
||||
RESPONSE_INVALID_OPERAND -> "Invalid operand"
|
||||
RESPONSE_NO_RECORDS_FOUND -> "No records found"
|
||||
RESPONSE_ABORT_UNSUCCESSFUL -> "Abort unsuccessful"
|
||||
RESPONSE_PROCEDURE_NOT_COMPLETED -> "Procedure not completed"
|
||||
RESPONSE_OPERAND_NOT_SUPPORTED -> "Operand not supported"
|
||||
else -> "Reserved for future use"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package no.nordicsemi.android.gls.view
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.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.unit.dp
|
||||
import no.nordicsemi.android.gls.R
|
||||
import no.nordicsemi.android.gls.data.GLSData
|
||||
import no.nordicsemi.android.gls.viewmodel.DisconnectEvent
|
||||
import no.nordicsemi.android.gls.viewmodel.GLSScreenViewEvent
|
||||
import no.nordicsemi.android.theme.view.BatteryLevelView
|
||||
|
||||
@Composable
|
||||
internal fun GLSContentView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
BatteryLevelView(state.batteryLevel)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Button(
|
||||
colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.secondary),
|
||||
onClick = { onEvent(DisconnectEvent) }
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.disconnect))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package no.nordicsemi.android.gls.view
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import no.nordicsemi.android.gls.R
|
||||
import no.nordicsemi.android.gls.data.GLSData
|
||||
import no.nordicsemi.android.gls.viewmodel.GLSScreenViewEvent
|
||||
import no.nordicsemi.android.gls.viewmodel.GLSViewModel
|
||||
|
||||
@Composable
|
||||
fun GLSScreen(finishAction: () -> Unit) {
|
||||
val viewModel: GLSViewModel = hiltViewModel()
|
||||
val state = viewModel.state.collectAsState().value
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GLSView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) {
|
||||
Column {
|
||||
TopAppBar(title = { Text(text = stringResource(id = R.string.gls_title)) })
|
||||
|
||||
GLSContentView(state, onEvent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package no.nordicsemi.android.gls.viewmodel
|
||||
|
||||
sealed class GLSScreenViewEvent
|
||||
|
||||
object DisconnectEvent : GLSScreenViewEvent()
|
||||
@@ -0,0 +1,20 @@
|
||||
package no.nordicsemi.android.gls.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import no.nordicsemi.android.gls.repository.GLSManager
|
||||
import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class GLSViewModel @Inject constructor(
|
||||
private val glsManager: GLSManager,
|
||||
private val deviceHolder: SelectedBluetoothDeviceHolder
|
||||
) : ViewModel() {
|
||||
|
||||
val state = glsManager.data
|
||||
|
||||
fun bondDevice() {
|
||||
|
||||
}
|
||||
}
|
||||
4
feature_gls/src/main/res/values/strings.xml
Normal file
4
feature_gls/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="gls_title">GLS</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,17 @@
|
||||
package no.nordicsemi.android.gls
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package no.nordicsemi.android.hrs.events
|
||||
package no.nordicsemi.android.hrs.data
|
||||
|
||||
internal data class HRSAggregatedData(
|
||||
internal data class HRSData(
|
||||
val heartRates: List<Int> = emptyList(),
|
||||
val batteryLevel: Int = 0,
|
||||
val sensorLocation: Int = 0
|
||||
@@ -1,9 +1,9 @@
|
||||
package no.nordicsemi.android.hrs.service
|
||||
|
||||
import no.nordicsemi.android.hrs.events.HRSAggregatedData
|
||||
import no.nordicsemi.android.hrs.data.HRSData
|
||||
import no.nordicsemi.android.service.BluetoothDataReadBroadcast
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
internal class HRSDataBroadcast @Inject constructor() : BluetoothDataReadBroadcast<HRSAggregatedData>()
|
||||
internal class HRSDataBroadcast @Inject constructor() : BluetoothDataReadBroadcast<HRSData>()
|
||||
|
||||
@@ -3,7 +3,7 @@ package no.nordicsemi.android.hrs.service
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import no.nordicsemi.android.ble.BleManagerCallbacks
|
||||
import no.nordicsemi.android.hrs.events.HRSAggregatedData
|
||||
import no.nordicsemi.android.hrs.data.HRSData
|
||||
import no.nordicsemi.android.service.ForegroundBleService
|
||||
import no.nordicsemi.android.service.LoggableBleManager
|
||||
import javax.inject.Inject
|
||||
@@ -11,7 +11,7 @@ import javax.inject.Inject
|
||||
@AndroidEntryPoint
|
||||
internal class HRSService : ForegroundBleService<HRSManager>(), HRSManagerCallbacks {
|
||||
|
||||
private var data = HRSAggregatedData()
|
||||
private var data = HRSData()
|
||||
private val points = mutableListOf<Int>()
|
||||
|
||||
@Inject
|
||||
@@ -46,7 +46,7 @@ internal class HRSService : ForegroundBleService<HRSManager>(), HRSManagerCallba
|
||||
sendNewData(data.copy(heartRates = points))
|
||||
}
|
||||
|
||||
private fun sendNewData(newData: HRSAggregatedData) {
|
||||
private fun sendNewData(newData: HRSData) {
|
||||
data = newData
|
||||
localBroadcast.offer(newData)
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ internal fun ContentView(state: HRSViewState, onEvent: (HRSScreenViewEvent) -> U
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LineChartView(state: HRSViewState) {
|
||||
internal fun LineChartView(state: HRSViewState) {
|
||||
AndroidView(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -81,7 +81,7 @@ fun LineChartView(state: HRSViewState) {
|
||||
)
|
||||
}
|
||||
|
||||
fun createLineChartView(context: Context, state: HRSViewState): LineChart {
|
||||
internal fun createLineChartView(context: Context, state: HRSViewState): LineChart {
|
||||
return LineChart(context).apply {
|
||||
setBackgroundColor(Color.WHITE)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.withContext
|
||||
import no.nordicsemi.android.hrs.events.HRSAggregatedData
|
||||
import no.nordicsemi.android.hrs.data.HRSData
|
||||
import no.nordicsemi.android.hrs.service.HRSDataBroadcast
|
||||
import no.nordicsemi.android.hrs.view.DisconnectEvent
|
||||
import no.nordicsemi.android.hrs.view.HRSScreenViewEvent
|
||||
@@ -27,7 +27,7 @@ internal class HRSViewModel @Inject constructor(
|
||||
}.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
private fun consumeEvent(event: HRSAggregatedData) {
|
||||
private fun consumeEvent(event: HRSData) {
|
||||
state.value = state.value.copy(
|
||||
points = event.heartRates,
|
||||
batteryLevel = event.batteryLevel,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
package="no.nordicsemi.android.service">
|
||||
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
|
||||
</manifest>
|
||||
@@ -16,6 +16,15 @@ class SelectedBluetoothDeviceHolder constructor(
|
||||
return deviceManager.associations.firstOrNull()?.let { bluetoothAdapter?.getRemoteDevice(it) }
|
||||
}
|
||||
|
||||
//TODO: Check if starts automatically
|
||||
fun bondDevice() {
|
||||
device?.let {
|
||||
if (it.bondState == BluetoothDevice.BOND_NONE) {
|
||||
it.createBond()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun forgetDevice() {
|
||||
device?.let {
|
||||
val deviceManager = context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
|
||||
|
||||
@@ -62,6 +62,7 @@ rootProject.name = "Android-nRF-Toolbox"
|
||||
include ':app'
|
||||
|
||||
include ':feature_csc'
|
||||
include ':feature_gls'
|
||||
include ':feature_hrs'
|
||||
include ':feature_scanner'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user