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
|
//https://github.com/google/dagger/issues/2123
|
||||||
implementation project(":feature_csc")
|
implementation project(":feature_csc")
|
||||||
implementation project(":feature_hrs")
|
implementation project(":feature_hrs")
|
||||||
|
implementation project(":feature_gls")
|
||||||
implementation project(':feature_scanner')
|
implementation project(':feature_scanner')
|
||||||
implementation project(":lib_theme")
|
implementation project(":lib_theme")
|
||||||
implementation project(":lib_utils")
|
implementation project(":lib_utils")
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import androidx.navigation.compose.NavHost
|
|||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import no.nordicsemi.android.csc.view.CscScreen
|
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.hrs.view.HRSScreen
|
||||||
import no.nordicsemi.android.scanner.view.BluetoothNotAvailableScreen
|
import no.nordicsemi.android.scanner.view.BluetoothNotAvailableScreen
|
||||||
import no.nordicsemi.android.scanner.view.BluetoothNotEnabledScreen
|
import no.nordicsemi.android.scanner.view.BluetoothNotEnabledScreen
|
||||||
@@ -29,7 +30,7 @@ import no.nordicsemi.android.scanner.view.ScanDeviceScreenResult
|
|||||||
import no.nordicsemi.android.utils.exhaustive
|
import no.nordicsemi.android.utils.exhaustive
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeScreen() {
|
internal fun HomeScreen() {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
val viewModel = hiltViewModel<NavigationViewModel>()
|
val viewModel = hiltViewModel<NavigationViewModel>()
|
||||||
@@ -42,6 +43,7 @@ fun HomeScreen() {
|
|||||||
composable(NavDestination.HOME.id) { HomeView { viewModel.navigate(it) } }
|
composable(NavDestination.HOME.id) { HomeView { viewModel.navigate(it) } }
|
||||||
composable(NavDestination.CSC.id) { CscScreen { viewModel.navigateUp() } }
|
composable(NavDestination.CSC.id) { CscScreen { viewModel.navigateUp() } }
|
||||||
composable(NavDestination.HRS.id) { HRSScreen { 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.REQUEST_PERMISSION.id) { RequestPermissionScreen(continueAction) }
|
||||||
composable(NavDestination.BLUETOOTH_NOT_AVAILABLE.id) { BluetoothNotAvailableScreen() }
|
composable(NavDestination.BLUETOOTH_NOT_AVAILABLE.id) { BluetoothNotAvailableScreen() }
|
||||||
composable(NavDestination.BLUETOOTH_NOT_ENABLED.id) {
|
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_csc, R.string.csc_module) { callback(NavDestination.CSC) }
|
||||||
FeatureButton(R.drawable.ic_hrs, R.string.hrs_module) { callback(NavDestination.HRS) }
|
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"),
|
HOME("home-screen"),
|
||||||
CSC("csc-screen"),
|
CSC("csc-screen"),
|
||||||
HRS("hrs-screen"),
|
HRS("hrs-screen"),
|
||||||
|
GLS("gls-screen"),
|
||||||
REQUEST_PERMISSION("request-permission"),
|
REQUEST_PERMISSION("request-permission"),
|
||||||
BLUETOOTH_NOT_AVAILABLE("bluetooth-not-available"),
|
BLUETOOTH_NOT_AVAILABLE("bluetooth-not-available"),
|
||||||
BLUETOOTH_NOT_ENABLED("bluetooth-not-enabled"),
|
BLUETOOTH_NOT_ENABLED("bluetooth-not-enabled"),
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import javax.inject.Inject
|
|||||||
class NavigationViewModel @Inject constructor(
|
class NavigationViewModel @Inject constructor(
|
||||||
private val bleScanner: NordicBleScanner,
|
private val bleScanner: NordicBleScanner,
|
||||||
private val permissionHelper: PermissionHelper,
|
private val permissionHelper: PermissionHelper,
|
||||||
private val selectedDevice: no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
|
private val selectedDevice: SelectedBluetoothDeviceHolder
|
||||||
): ViewModel() {
|
): ViewModel() {
|
||||||
|
|
||||||
val state= MutableStateFlow(NavDestination.HOME)
|
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"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:viewportWidth="1024" android:width="80dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
android:width="80dp"
|
||||||
<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"/>
|
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>
|
</vector>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="csc_module">CSC</string>
|
<string name="csc_module">CSC</string>
|
||||||
<string name="hrs_module">HRS</string>
|
<string name="hrs_module">HRS</string>
|
||||||
|
<string name="gls_module">GLS</string>
|
||||||
</resources>
|
</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.CSCSettings
|
||||||
import no.nordicsemi.android.csc.view.SpeedUnit
|
import no.nordicsemi.android.csc.view.SpeedUnit
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
internal data class CSCViewState(
|
internal data class CSCData(
|
||||||
val showDialog: Boolean = false,
|
val showDialog: Boolean = false,
|
||||||
val scanDevices: Boolean = false,
|
val scanDevices: Boolean = false,
|
||||||
val selectedSpeedUnit: SpeedUnit = SpeedUnit.M_S,
|
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.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
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 no.nordicsemi.android.service.BluetoothDataReadBroadcast
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
internal class CSCDataReadBroadcast @Inject constructor() : BluetoothDataReadBroadcast<CSCServiceEvent>() {
|
internal class CSCDataReadBroadcast @Inject constructor() : BluetoothDataReadBroadcast<CSCData>() {
|
||||||
|
|
||||||
private val _wheelSize = MutableSharedFlow<Int>(
|
private val _wheelSize = MutableSharedFlow<Int>(
|
||||||
replay = 1,
|
replay = 1,
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import no.nordicsemi.android.csc.events.CrankDataChanged
|
import no.nordicsemi.android.csc.data.CSCData
|
||||||
import no.nordicsemi.android.csc.events.OnBatteryLevelChanged
|
|
||||||
import no.nordicsemi.android.csc.events.OnDistanceChangedEvent
|
|
||||||
import no.nordicsemi.android.service.ForegroundBleService
|
import no.nordicsemi.android.service.ForegroundBleService
|
||||||
import no.nordicsemi.android.service.LoggableBleManager
|
import no.nordicsemi.android.service.LoggableBleManager
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -15,6 +13,8 @@ import javax.inject.Inject
|
|||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
internal class CSCService : ForegroundBleService<CSCManager>(), CSCManagerCallbacks {
|
internal class CSCService : ForegroundBleService<CSCManager>(), CSCManagerCallbacks {
|
||||||
|
|
||||||
|
private var data = CSCData()
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var localBroadcast: CSCDataReadBroadcast
|
lateinit var localBroadcast: CSCDataReadBroadcast
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ internal class CSCService : ForegroundBleService<CSCManager>(), CSCManagerCallba
|
|||||||
distance: Float,
|
distance: Float,
|
||||||
speed: Float
|
speed: Float
|
||||||
) {
|
) {
|
||||||
localBroadcast.offer(OnDistanceChangedEvent(bluetoothDevice, speed, distance, totalDistance))
|
localBroadcast.offer(data.copy(speed = speed, distance = distance, totalDistance = totalDistance))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCrankDataChanged(
|
override fun onCrankDataChanged(
|
||||||
@@ -50,10 +50,10 @@ internal class CSCService : ForegroundBleService<CSCManager>(), CSCManagerCallba
|
|||||||
crankCadence: Float,
|
crankCadence: Float,
|
||||||
gearRatio: 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) {
|
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.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import no.nordicsemi.android.csc.R
|
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.NordicColors
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ContentView(state: CSCViewState, onEvent: (CSCViewEvent) -> Unit) {
|
internal fun ContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
||||||
if (state.showDialog) {
|
if (state.showDialog) {
|
||||||
SelectWheelSizeDialog { onEvent(it) }
|
SelectWheelSizeDialog { onEvent(it) }
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,7 @@ internal fun ContentView(state: CSCViewState, onEvent: (CSCViewEvent) -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SettingsSection(state: CSCViewState, onEvent: (CSCViewEvent) -> Unit) {
|
private fun SettingsSection(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
||||||
Card(
|
Card(
|
||||||
backgroundColor = NordicColors.NordicGray4.value(),
|
backgroundColor = NordicColors.NordicGray4.value(),
|
||||||
shape = RoundedCornerShape(10.dp),
|
shape = RoundedCornerShape(10.dp),
|
||||||
@@ -70,5 +70,5 @@ private fun SettingsSection(state: CSCViewState, onEvent: (CSCViewEvent) -> Unit
|
|||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
private fun ConnectedPreview() {
|
private fun ConnectedPreview() {
|
||||||
ContentView(CSCViewState()) { }
|
ContentView(CSCData()) { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ 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.service.CSCService
|
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.csc.viewmodel.CscViewModel
|
||||||
import no.nordicsemi.android.utils.isServiceRunning
|
import no.nordicsemi.android.utils.isServiceRunning
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ fun CscScreen(finishAction: () -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CSCView(state: CSCViewState, onEvent: (CSCViewEvent) -> Unit) {
|
private fun CSCView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
||||||
Column {
|
Column {
|
||||||
TopAppBar(title = { Text(text = stringResource(id = R.string.csc_title)) })
|
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.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import no.nordicsemi.android.csc.R
|
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.NordicColors
|
||||||
import no.nordicsemi.android.theme.view.BatteryLevelView
|
import no.nordicsemi.android.theme.view.BatteryLevelView
|
||||||
import no.nordicsemi.android.theme.view.KeyValueField
|
import no.nordicsemi.android.theme.view.KeyValueField
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SensorsReadingView(state: CSCViewState) {
|
internal fun SensorsReadingView(state: CSCData) {
|
||||||
Card(
|
Card(
|
||||||
backgroundColor = NordicColors.NordicGray4.value(),
|
backgroundColor = NordicColors.NordicGray4.value(),
|
||||||
shape = RoundedCornerShape(10.dp),
|
shape = RoundedCornerShape(10.dp),
|
||||||
@@ -48,5 +48,5 @@ internal fun SensorsReadingView(state: CSCViewState) {
|
|||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
private fun Preview() {
|
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.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import no.nordicsemi.android.csc.R
|
import no.nordicsemi.android.csc.R
|
||||||
import no.nordicsemi.android.csc.viewmodel.CSCViewState
|
import no.nordicsemi.android.csc.data.CSCData
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun WheelSizeView(state: CSCViewState, onEvent: (CSCViewEvent) -> Unit) {
|
internal fun WheelSizeView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
value = state.wheelSize,
|
value = state.wheelSize,
|
||||||
@@ -36,5 +36,5 @@ private fun EditIcon(onEvent: (CSCViewEvent) -> Unit) {
|
|||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
private fun WheelSizeViewPreview() {
|
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.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import no.nordicsemi.android.csc.events.CSCServiceEvent
|
import no.nordicsemi.android.csc.data.CSCData
|
||||||
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.service.CSCDataReadBroadcast
|
import no.nordicsemi.android.csc.service.CSCDataReadBroadcast
|
||||||
import no.nordicsemi.android.csc.view.CSCViewEvent
|
import no.nordicsemi.android.csc.view.CSCViewEvent
|
||||||
import no.nordicsemi.android.csc.view.OnDisconnectButtonClick
|
import no.nordicsemi.android.csc.view.OnDisconnectButtonClick
|
||||||
@@ -26,44 +23,14 @@ internal class CscViewModel @Inject constructor(
|
|||||||
private val localBroadcast: CSCDataReadBroadcast
|
private val localBroadcast: CSCDataReadBroadcast
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val state = MutableStateFlow(CSCViewState())
|
val state = MutableStateFlow(CSCData())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
localBroadcast.events.onEach {
|
localBroadcast.events.onEach {
|
||||||
withContext(Dispatchers.Main) { consumeEvent(it) }
|
withContext(Dispatchers.Main) { state.value = it }
|
||||||
}.launchIn(viewModelScope)
|
}.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) {
|
fun onEvent(event: CSCViewEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is OnSelectedSpeedUnitSelected -> onSelectedSpeedUnit(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 heartRates: List<Int> = emptyList(),
|
||||||
val batteryLevel: Int = 0,
|
val batteryLevel: Int = 0,
|
||||||
val sensorLocation: Int = 0
|
val sensorLocation: Int = 0
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package no.nordicsemi.android.hrs.service
|
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 no.nordicsemi.android.service.BluetoothDataReadBroadcast
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@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 android.bluetooth.BluetoothDevice
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import no.nordicsemi.android.ble.BleManagerCallbacks
|
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.ForegroundBleService
|
||||||
import no.nordicsemi.android.service.LoggableBleManager
|
import no.nordicsemi.android.service.LoggableBleManager
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -11,7 +11,7 @@ import javax.inject.Inject
|
|||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
internal class HRSService : ForegroundBleService<HRSManager>(), HRSManagerCallbacks {
|
internal class HRSService : ForegroundBleService<HRSManager>(), HRSManagerCallbacks {
|
||||||
|
|
||||||
private var data = HRSAggregatedData()
|
private var data = HRSData()
|
||||||
private val points = mutableListOf<Int>()
|
private val points = mutableListOf<Int>()
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@@ -46,7 +46,7 @@ internal class HRSService : ForegroundBleService<HRSManager>(), HRSManagerCallba
|
|||||||
sendNewData(data.copy(heartRates = points))
|
sendNewData(data.copy(heartRates = points))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendNewData(newData: HRSAggregatedData) {
|
private fun sendNewData(newData: HRSData) {
|
||||||
data = newData
|
data = newData
|
||||||
localBroadcast.offer(newData)
|
localBroadcast.offer(newData)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ internal fun ContentView(state: HRSViewState, onEvent: (HRSScreenViewEvent) -> U
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LineChartView(state: HRSViewState) {
|
internal fun LineChartView(state: HRSViewState) {
|
||||||
AndroidView(
|
AndroidView(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.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 {
|
return LineChart(context).apply {
|
||||||
setBackgroundColor(Color.WHITE)
|
setBackgroundColor(Color.WHITE)
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.withContext
|
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.service.HRSDataBroadcast
|
||||||
import no.nordicsemi.android.hrs.view.DisconnectEvent
|
import no.nordicsemi.android.hrs.view.DisconnectEvent
|
||||||
import no.nordicsemi.android.hrs.view.HRSScreenViewEvent
|
import no.nordicsemi.android.hrs.view.HRSScreenViewEvent
|
||||||
@@ -27,7 +27,7 @@ internal class HRSViewModel @Inject constructor(
|
|||||||
}.launchIn(viewModelScope)
|
}.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun consumeEvent(event: HRSAggregatedData) {
|
private fun consumeEvent(event: HRSData) {
|
||||||
state.value = state.value.copy(
|
state.value = state.value.copy(
|
||||||
points = event.heartRates,
|
points = event.heartRates,
|
||||||
batteryLevel = event.batteryLevel,
|
batteryLevel = event.batteryLevel,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
package="no.nordicsemi.android.service">
|
package="no.nordicsemi.android.service">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -16,6 +16,15 @@ class SelectedBluetoothDeviceHolder constructor(
|
|||||||
return deviceManager.associations.firstOrNull()?.let { bluetoothAdapter?.getRemoteDevice(it) }
|
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() {
|
fun forgetDevice() {
|
||||||
device?.let {
|
device?.let {
|
||||||
val deviceManager = context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
|
val deviceManager = context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ rootProject.name = "Android-nRF-Toolbox"
|
|||||||
include ':app'
|
include ':app'
|
||||||
|
|
||||||
include ':feature_csc'
|
include ':feature_csc'
|
||||||
|
include ':feature_gls'
|
||||||
include ':feature_hrs'
|
include ':feature_hrs'
|
||||||
include ':feature_scanner'
|
include ':feature_scanner'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user