mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2025-12-19 15:34:26 +01:00
Refactoring & CR fixes
This commit is contained in:
@@ -50,11 +50,11 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
//Hilt requires to implement every module in the main app module
|
//Hilt requires to implement every module in the main app module
|
||||||
//https://github.com/google/dagger/issues/2123
|
//https://github.com/google/dagger/issues/2123
|
||||||
implementation project(":feature_csc")
|
implementation project(':profile_csc')
|
||||||
implementation project(":feature_hrs")
|
implementation project(':profile_hrs')
|
||||||
implementation project(":feature_hts")
|
implementation project(':profile_hts')
|
||||||
implementation project(":feature_gls")
|
implementation project(':profile_gls')
|
||||||
implementation project(':feature_scanner')
|
implementation project(':profile_scanner')
|
||||||
implementation project(":lib_theme")
|
implementation project(":lib_theme")
|
||||||
implementation project(":lib_utils")
|
implementation project(":lib_utils")
|
||||||
|
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ import androidx.annotation.DrawableRes
|
|||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.Button
|
|
||||||
import androidx.compose.material.ButtonDefaults
|
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -25,10 +25,13 @@ import no.nordicsemi.android.theme.NordicColors
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FeatureButton(@DrawableRes iconId: Int, @StringRes nameId: Int, onClick: () -> Unit) {
|
fun FeatureButton(@DrawableRes iconId: Int, @StringRes nameId: Int, onClick: () -> Unit) {
|
||||||
Button(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
onClick = { onClick() },
|
.fillMaxWidth()
|
||||||
colors = ButtonDefaults.buttonColors(backgroundColor = NordicColors.NordicGray4.value()),
|
.clickable { onClick() }
|
||||||
|
.background(NordicColors.ItemHighlight.value())
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(iconId),
|
painter = painterResource(iconId),
|
||||||
@@ -41,14 +44,10 @@ fun FeatureButton(@DrawableRes iconId: Int, @StringRes nameId: Int, onClick: ()
|
|||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(16.dp)
|
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(text = stringResource(id = nameId))
|
||||||
text = stringResource(id = nameId),
|
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package no.nordicsemi.android.nrftoolbox
|
package no.nordicsemi.android.nrftoolbox
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.material.TopAppBar
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -12,14 +13,17 @@ import androidx.compose.runtime.SideEffect
|
|||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
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 androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.navigation.compose.NavHost
|
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.gls.view.GLSScreen
|
||||||
import no.nordicsemi.android.hrs.view.HRSScreen
|
import no.nordicsemi.android.hrs.view.HRSScreen
|
||||||
import no.nordicsemi.android.hts.view.HTSScreen
|
import no.nordicsemi.android.hts.view.HTSScreen
|
||||||
@@ -28,6 +32,7 @@ import no.nordicsemi.android.scanner.view.BluetoothNotEnabledScreen
|
|||||||
import no.nordicsemi.android.scanner.view.RequestPermissionScreen
|
import no.nordicsemi.android.scanner.view.RequestPermissionScreen
|
||||||
import no.nordicsemi.android.scanner.view.ScanDeviceScreen
|
import no.nordicsemi.android.scanner.view.ScanDeviceScreen
|
||||||
import no.nordicsemi.android.scanner.view.ScanDeviceScreenResult
|
import no.nordicsemi.android.scanner.view.ScanDeviceScreenResult
|
||||||
|
import no.nordicsemi.android.theme.view.CloseIconAppBar
|
||||||
import no.nordicsemi.android.utils.exhaustive
|
import no.nordicsemi.android.utils.exhaustive
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -42,12 +47,12 @@ internal fun HomeScreen() {
|
|||||||
|
|
||||||
NavHost(navController = navController, startDestination = NavDestination.HOME.id) {
|
NavHost(navController = navController, startDestination = NavDestination.HOME.id) {
|
||||||
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.HTS.id) { HTSScreen { viewModel.navigateUp() } }
|
composable(NavDestination.HTS.id) { HTSScreen { viewModel.navigateUp() } }
|
||||||
composable(NavDestination.GLS.id) { GLSScreen { 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{ viewModel.finish() } }
|
||||||
composable(NavDestination.BLUETOOTH_NOT_ENABLED.id) {
|
composable(NavDestination.BLUETOOTH_NOT_ENABLED.id) {
|
||||||
BluetoothNotEnabledScreen(continueAction)
|
BluetoothNotEnabledScreen(continueAction)
|
||||||
}
|
}
|
||||||
@@ -69,11 +74,18 @@ internal fun HomeScreen() {
|
|||||||
@Composable
|
@Composable
|
||||||
fun HomeView(callback: (NavDestination) -> Unit) {
|
fun HomeView(callback: (NavDestination) -> Unit) {
|
||||||
Column {
|
Column {
|
||||||
TopAppBar(title = { Text(text = stringResource(id = R.string.app_name)) })
|
val context = LocalContext.current
|
||||||
|
CloseIconAppBar(stringResource(id = R.string.app_name)) {
|
||||||
|
(context as? Activity)?.finish()
|
||||||
|
}
|
||||||
|
|
||||||
FeatureButton(R.drawable.ic_csc, R.string.csc_module) { callback(NavDestination.CSC) }
|
FeatureButton(R.drawable.ic_csc, R.string.csc_module) { callback(NavDestination.CSC) }
|
||||||
|
Spacer(modifier = Modifier.height(1.dp))
|
||||||
FeatureButton(R.drawable.ic_hrs, R.string.hrs_module) { callback(NavDestination.HRS) }
|
FeatureButton(R.drawable.ic_hrs, R.string.hrs_module) { callback(NavDestination.HRS) }
|
||||||
|
Spacer(modifier = Modifier.height(1.dp))
|
||||||
FeatureButton(R.drawable.ic_gls, R.string.gls_module) { callback(NavDestination.GLS) }
|
FeatureButton(R.drawable.ic_gls, R.string.gls_module) { callback(NavDestination.GLS) }
|
||||||
|
Spacer(modifier = Modifier.height(1.dp))
|
||||||
|
FeatureButton(R.drawable.ic_hts, R.string.hts_module) { callback(NavDestination.HTS) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +114,6 @@ private fun BackHandler(enabled: Boolean = true, onBack: () -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun DefaultPreview() {
|
fun DefaultPreview() {
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
package no.nordicsemi.android.nrftoolbox
|
package no.nordicsemi.android.nrftoolbox
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Surface
|
import androidx.compose.material.Surface
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import no.nordicsemi.android.theme.TestTheme
|
import no.nordicsemi.android.theme.TestTheme
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|||||||
@@ -4,5 +4,4 @@ import android.app.Application
|
|||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class NrfToolboxApplication : Application() {
|
class NrfToolboxApplication : Application()
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,4 +2,5 @@
|
|||||||
<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>
|
<string name="gls_module">GLS</string>
|
||||||
|
<string name="hts_module">HTS</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
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.data.CSCData
|
|
||||||
import no.nordicsemi.android.service.BluetoothDataReadBroadcast
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
internal class CSCDataReadBroadcast @Inject constructor() : BluetoothDataReadBroadcast<CSCData>() {
|
|
||||||
|
|
||||||
private val _wheelSize = MutableSharedFlow<Int>(
|
|
||||||
replay = 1,
|
|
||||||
extraBufferCapacity = 1,
|
|
||||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
|
||||||
)
|
|
||||||
val wheelSize: SharedFlow<Int> = _wheelSize
|
|
||||||
|
|
||||||
fun setWheelSize(size: Int) {
|
|
||||||
_wheelSize.tryEmit(size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.csc.service
|
|
||||||
|
|
||||||
import no.nordicsemi.android.ble.common.profile.csc.CyclingSpeedAndCadenceCallback
|
|
||||||
import no.nordicsemi.android.service.BatteryManagerCallbacks
|
|
||||||
|
|
||||||
internal interface CSCManagerCallbacks : BatteryManagerCallbacks, CyclingSpeedAndCadenceCallback
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package no.nordicsemi.android.csc.service
|
|
||||||
|
|
||||||
import android.bluetooth.BluetoothDevice
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import no.nordicsemi.android.csc.data.CSCData
|
|
||||||
import no.nordicsemi.android.service.ForegroundBleService
|
|
||||||
import no.nordicsemi.android.service.LoggableBleManager
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
internal class CSCService : ForegroundBleService<CSCManager>(), CSCManagerCallbacks {
|
|
||||||
|
|
||||||
private var data = CSCData()
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var localBroadcast: CSCDataReadBroadcast
|
|
||||||
|
|
||||||
override val manager: CSCManager by lazy {
|
|
||||||
CSCManager(this).apply {
|
|
||||||
setGattCallbacks(this@CSCService)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun initializeManager(): LoggableBleManager<CSCManagerCallbacks> {
|
|
||||||
return manager
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
|
|
||||||
localBroadcast.wheelSize.onEach {
|
|
||||||
manager.setWheelSize(it)
|
|
||||||
}.launchIn(lifecycleScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDistanceChanged(
|
|
||||||
device: BluetoothDevice,
|
|
||||||
totalDistance: Float,
|
|
||||||
distance: Float,
|
|
||||||
speed: Float
|
|
||||||
) {
|
|
||||||
localBroadcast.offer(data.copy(speed = speed, distance = distance, totalDistance = totalDistance))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCrankDataChanged(
|
|
||||||
device: BluetoothDevice,
|
|
||||||
crankCadence: Float,
|
|
||||||
gearRatio: Float
|
|
||||||
) {
|
|
||||||
localBroadcast.offer(data.copy(cadence = crankCadence.toInt(), gearRatio = gearRatio))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBatteryLevelChanged(device: BluetoothDevice, batteryLevel: Int) {
|
|
||||||
localBroadcast.offer(data.copy(batteryLevel = batteryLevel))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
package="no.nordicsemi.android.gls">
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package no.nordicsemi.android.gls.data
|
|
||||||
|
|
||||||
internal data class GLSData(
|
|
||||||
val record: List<GLSRecord> = emptyList(),
|
|
||||||
val batteryLevel: Int = 0,
|
|
||||||
val requestStatus: RequestStatus = RequestStatus.IDLE,
|
|
||||||
val isDeviceBonded: Boolean = false
|
|
||||||
)
|
|
||||||
|
|
||||||
internal enum class RequestStatus {
|
|
||||||
IDLE, PENDING, SUCCESS, ABORTED, FAILED, NOT_SUPPORTED
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package no.nordicsemi.android.gls.viewmodel
|
|
||||||
|
|
||||||
sealed class GLSScreenViewEvent
|
|
||||||
|
|
||||||
object DisconnectEvent : GLSScreenViewEvent()
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
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() {
|
|
||||||
if (deviceHolder.isDeviceBonded()) {
|
|
||||||
deviceHolder.bondDevice()
|
|
||||||
} else {
|
|
||||||
//start work
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package no.nordicsemi.android.hrs.service
|
|
||||||
|
|
||||||
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<HRSData>()
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.hrs.service
|
|
||||||
|
|
||||||
import no.nordicsemi.android.ble.common.profile.hr.BodySensorLocationCallback
|
|
||||||
import no.nordicsemi.android.ble.common.profile.hr.HeartRateMeasurementCallback
|
|
||||||
import no.nordicsemi.android.service.BatteryManagerCallbacks
|
|
||||||
|
|
||||||
interface HRSManagerCallbacks
|
|
||||||
: BatteryManagerCallbacks, BodySensorLocationCallback, HeartRateMeasurementCallback
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
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.data.HRSData
|
|
||||||
import no.nordicsemi.android.service.ForegroundBleService
|
|
||||||
import no.nordicsemi.android.service.LoggableBleManager
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
internal class HRSService : ForegroundBleService<HRSManager>(), HRSManagerCallbacks {
|
|
||||||
|
|
||||||
private var data = HRSData()
|
|
||||||
private val points = mutableListOf<Int>()
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var localBroadcast: HRSDataBroadcast
|
|
||||||
|
|
||||||
override val manager: HRSManager by lazy {
|
|
||||||
HRSManager(this).apply {
|
|
||||||
setGattCallbacks(this@HRSService)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun initializeManager(): LoggableBleManager<out BleManagerCallbacks> {
|
|
||||||
return manager
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBatteryLevelChanged(device: BluetoothDevice, batteryLevel: Int) {
|
|
||||||
sendNewData(data.copy(batteryLevel = batteryLevel))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBodySensorLocationReceived(device: BluetoothDevice, sensorLocation: Int) {
|
|
||||||
sendNewData(data.copy(sensorLocation = sensorLocation))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onHeartRateMeasurementReceived(
|
|
||||||
device: BluetoothDevice,
|
|
||||||
heartRate: Int,
|
|
||||||
contactDetected: Boolean?,
|
|
||||||
energyExpanded: Int?,
|
|
||||||
rrIntervals: MutableList<Int>?
|
|
||||||
) {
|
|
||||||
points.add(heartRate)
|
|
||||||
sendNewData(data.copy(heartRates = points))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sendNewData(newData: HRSData) {
|
|
||||||
data = newData
|
|
||||||
localBroadcast.offer(newData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package no.nordicsemi.android.hrs.viewmodel
|
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
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
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
internal class HRSViewModel @Inject constructor(
|
|
||||||
private val localBroadcast: HRSDataBroadcast
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
val state = MutableStateFlow(HRSViewState())
|
|
||||||
|
|
||||||
init {
|
|
||||||
localBroadcast.events.onEach {
|
|
||||||
withContext(Dispatchers.Main) { consumeEvent(it) }
|
|
||||||
}.launchIn(viewModelScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun consumeEvent(event: HRSData) {
|
|
||||||
state.value = state.value.copy(
|
|
||||||
points = event.heartRates,
|
|
||||||
batteryLevel = event.batteryLevel,
|
|
||||||
sensorLocation = event.sensorLocation
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onEvent(event: HRSScreenViewEvent) {
|
|
||||||
(event as? DisconnectEvent)?.let {
|
|
||||||
onDisconnectButtonClick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onDisconnectButtonClick() {
|
|
||||||
state.tryEmit(state.value.copy(isScreenActive = false))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package no.nordicsemi.android.hrs.viewmodel
|
|
||||||
|
|
||||||
data class HRSViewState(
|
|
||||||
val points: List<Int> = listOf(1, 2, 3),
|
|
||||||
val batteryLevel: Int = 0,
|
|
||||||
val sensorLocation: Int = 0,
|
|
||||||
val isScreenActive: Boolean = true
|
|
||||||
)
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<gradient
|
|
||||||
android:angle="90"
|
|
||||||
android:startColor="#00ff0000"
|
|
||||||
android:endColor="#ffff0000" />
|
|
||||||
</shape>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package no.nordicsemi.android.hts.data
|
|
||||||
|
|
||||||
internal data class HTSData(
|
|
||||||
val heartRates: List<Int> = emptyList(),
|
|
||||||
val temperature: Temperature = Temperature.CELSIUS,
|
|
||||||
val batteryLevel: Int = 0,
|
|
||||||
val sensorLocation: Int = 0,
|
|
||||||
val isScreenActive: Boolean = true
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun displayTemperature() {
|
|
||||||
val value = when (temperature) {
|
|
||||||
Temperature.CELSIUS -> TODO()
|
|
||||||
Temperature.FAHRENHEIT -> TODO()
|
|
||||||
Temperature.KELVIN -> TODO()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal enum class Temperature {
|
|
||||||
CELSIUS, FAHRENHEIT, KELVIN
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package no.nordicsemi.android.hts.service
|
|
||||||
|
|
||||||
import no.nordicsemi.android.hts.data.HTSData
|
|
||||||
import no.nordicsemi.android.service.BluetoothDataReadBroadcast
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
internal class HTSDataBroadcast @Inject constructor() : BluetoothDataReadBroadcast<HTSData>()
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.hts.service
|
|
||||||
|
|
||||||
import no.nordicsemi.android.ble.common.profile.ht.TemperatureMeasurementCallback
|
|
||||||
import no.nordicsemi.android.service.BatteryManagerCallbacks
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface [HTSManagerCallbacks] must be implemented by [HTActivity] in order
|
|
||||||
* to receive callbacks from [HTSManager].
|
|
||||||
*/
|
|
||||||
interface HTSManagerCallbacks : BatteryManagerCallbacks, TemperatureMeasurementCallback
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package no.nordicsemi.android.hts.service
|
|
||||||
|
|
||||||
import android.bluetooth.BluetoothDevice
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
|
||||||
import no.nordicsemi.android.ble.BleManagerCallbacks
|
|
||||||
import no.nordicsemi.android.hts.data.HTSData
|
|
||||||
import no.nordicsemi.android.service.ForegroundBleService
|
|
||||||
import no.nordicsemi.android.service.LoggableBleManager
|
|
||||||
import java.util.*
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
internal class HTSService : ForegroundBleService<HTSManager>(), HTSManagerCallbacks {
|
|
||||||
|
|
||||||
private var data = HTSData()
|
|
||||||
private val points = mutableListOf<Int>()
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var localBroadcast: HTSDataBroadcast
|
|
||||||
|
|
||||||
override val manager: HTSManager by lazy {
|
|
||||||
HTSManager(this).apply {
|
|
||||||
setGattCallbacks(this@HTSService)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun initializeManager(): LoggableBleManager<out BleManagerCallbacks> {
|
|
||||||
return manager
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBatteryLevelChanged(device: BluetoothDevice, batteryLevel: Int) {
|
|
||||||
sendNewData(data.copy(batteryLevel = batteryLevel))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTemperatureMeasurementReceived(
|
|
||||||
device: BluetoothDevice,
|
|
||||||
temperature: Float,
|
|
||||||
unit: Int,
|
|
||||||
calendar: Calendar?,
|
|
||||||
type: Int?
|
|
||||||
) {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sendNewData(newData: HTSData) {
|
|
||||||
data = newData
|
|
||||||
localBroadcast.offer(newData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package no.nordicsemi.android.hts.view
|
|
||||||
|
|
||||||
sealed class HTSScreenViewEvent
|
|
||||||
|
|
||||||
object DisconnectEvent : HTSScreenViewEvent()
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package no.nordicsemi.android.hts.viewmodel
|
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import no.nordicsemi.android.hts.data.HTSData
|
|
||||||
import no.nordicsemi.android.hts.service.HTSDataBroadcast
|
|
||||||
import no.nordicsemi.android.hts.view.DisconnectEvent
|
|
||||||
import no.nordicsemi.android.hts.view.HTSScreenViewEvent
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
internal class HTSViewModel @Inject constructor(
|
|
||||||
private val localBroadcast: HTSDataBroadcast
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
val state = MutableStateFlow(HTSData())
|
|
||||||
|
|
||||||
init {
|
|
||||||
localBroadcast.events.onEach {
|
|
||||||
withContext(Dispatchers.Main) { consumeEvent(it) }
|
|
||||||
}.launchIn(viewModelScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun consumeEvent(event: HTSData) {
|
|
||||||
state.value = state.value.copy(
|
|
||||||
|
|
||||||
batteryLevel = event.batteryLevel,
|
|
||||||
sensorLocation = event.sensorLocation
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onEvent(event: HTSScreenViewEvent) {
|
|
||||||
(event as? DisconnectEvent)?.let {
|
|
||||||
onDisconnectButtonClick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onDisconnectButtonClick() {
|
|
||||||
state.tryEmit(state.value.copy(isScreenActive = false))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,6 +6,7 @@ import android.bluetooth.BluetoothGattCharacteristic
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.IntRange
|
import androidx.annotation.IntRange
|
||||||
|
import no.nordicsemi.android.ble.BleManager
|
||||||
import no.nordicsemi.android.ble.callback.DataReceivedCallback
|
import no.nordicsemi.android.ble.callback.DataReceivedCallback
|
||||||
import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelDataCallback
|
import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelDataCallback
|
||||||
import no.nordicsemi.android.ble.data.Data
|
import no.nordicsemi.android.ble.data.Data
|
||||||
@@ -18,17 +19,10 @@ import java.util.*
|
|||||||
* @param <T> The profile callbacks type.
|
* @param <T> The profile callbacks type.
|
||||||
* @see BleManager
|
* @see BleManager
|
||||||
</T> */
|
</T> */
|
||||||
abstract class BatteryManager<T : BatteryManagerCallbacks?>(context: Context) : LoggableBleManager<T>(context) {
|
abstract class BatteryManager(context: Context) : BleManager(context) {
|
||||||
|
|
||||||
private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null
|
private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null
|
||||||
/**
|
|
||||||
* Returns the last received Battery Level value.
|
|
||||||
* The value is set to null when the device disconnects.
|
|
||||||
* @return Battery Level value, in percent.
|
|
||||||
*/
|
|
||||||
/** Last received Battery Level value. */
|
|
||||||
var batteryLevel: Int? = null
|
|
||||||
private set
|
|
||||||
private val batteryLevelDataCallback: DataReceivedCallback =
|
private val batteryLevelDataCallback: DataReceivedCallback =
|
||||||
object : BatteryLevelDataCallback() {
|
object : BatteryLevelDataCallback() {
|
||||||
override fun onBatteryLevelChanged(
|
override fun onBatteryLevelChanged(
|
||||||
@@ -36,8 +30,7 @@ abstract class BatteryManager<T : BatteryManagerCallbacks?>(context: Context) :
|
|||||||
@IntRange(from = 0, to = 100) batteryLevel: Int
|
@IntRange(from = 0, to = 100) batteryLevel: Int
|
||||||
) {
|
) {
|
||||||
log(LogContract.Log.Level.APPLICATION, "Battery Level received: $batteryLevel%")
|
log(LogContract.Log.Level.APPLICATION, "Battery Level received: $batteryLevel%")
|
||||||
this@BatteryManager.batteryLevel = batteryLevel
|
onBatteryLevelChanged(batteryLevel)
|
||||||
mCallbacks?.onBatteryLevelChanged(device, batteryLevel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onInvalidDataReceived(device: BluetoothDevice, data: Data) {
|
override fun onInvalidDataReceived(device: BluetoothDevice, data: Data) {
|
||||||
@@ -45,15 +38,14 @@ abstract class BatteryManager<T : BatteryManagerCallbacks?>(context: Context) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected abstract fun onBatteryLevelChanged(batteryLevel: Int)
|
||||||
|
|
||||||
fun readBatteryLevelCharacteristic() {
|
fun readBatteryLevelCharacteristic() {
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
readCharacteristic(batteryLevelCharacteristic)
|
readCharacteristic(batteryLevelCharacteristic)
|
||||||
.with(batteryLevelDataCallback)
|
.with(batteryLevelDataCallback)
|
||||||
.fail { device: BluetoothDevice?, status: Int ->
|
.fail { device: BluetoothDevice?, status: Int ->
|
||||||
log(
|
log(Log.WARN, "Battery Level characteristic not found")
|
||||||
Log.WARN,
|
|
||||||
"Battery Level characteristic not found"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.enqueue()
|
.enqueue()
|
||||||
}
|
}
|
||||||
@@ -66,32 +58,10 @@ abstract class BatteryManager<T : BatteryManagerCallbacks?>(context: Context) :
|
|||||||
.with(batteryLevelDataCallback)
|
.with(batteryLevelDataCallback)
|
||||||
enableNotifications(batteryLevelCharacteristic)
|
enableNotifications(batteryLevelCharacteristic)
|
||||||
.done { device: BluetoothDevice? ->
|
.done { device: BluetoothDevice? ->
|
||||||
log(
|
log(Log.INFO, "Battery Level notifications enabled")
|
||||||
Log.INFO,
|
|
||||||
"Battery Level notifications enabled"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.fail { device: BluetoothDevice?, status: Int ->
|
.fail { device: BluetoothDevice?, status: Int ->
|
||||||
log(
|
log(Log.WARN, "Battery Level characteristic not found")
|
||||||
Log.WARN,
|
|
||||||
"Battery Level characteristic not found"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.enqueue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disables Battery Level notifications on the Server.
|
|
||||||
*/
|
|
||||||
fun disableBatteryLevelCharacteristicNotifications() {
|
|
||||||
if (isConnected) {
|
|
||||||
disableNotifications(batteryLevelCharacteristic)
|
|
||||||
.done { device: BluetoothDevice? ->
|
|
||||||
log(
|
|
||||||
Log.INFO,
|
|
||||||
"Battery Level notifications disabled"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.enqueue()
|
.enqueue()
|
||||||
}
|
}
|
||||||
@@ -106,16 +76,14 @@ abstract class BatteryManager<T : BatteryManagerCallbacks?>(context: Context) :
|
|||||||
override fun isOptionalServiceSupported(gatt: BluetoothGatt): Boolean {
|
override fun isOptionalServiceSupported(gatt: BluetoothGatt): Boolean {
|
||||||
val service = gatt.getService(BATTERY_SERVICE_UUID)
|
val service = gatt.getService(BATTERY_SERVICE_UUID)
|
||||||
if (service != null) {
|
if (service != null) {
|
||||||
batteryLevelCharacteristic = service.getCharacteristic(
|
batteryLevelCharacteristic = service.getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)
|
||||||
BATTERY_LEVEL_CHARACTERISTIC_UUID
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return batteryLevelCharacteristic != null
|
return batteryLevelCharacteristic != null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDeviceDisconnected() {
|
override fun onDeviceDisconnected() {
|
||||||
batteryLevelCharacteristic = null
|
batteryLevelCharacteristic = null
|
||||||
batteryLevel = null
|
onBatteryLevelChanged(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
package no.nordicsemi.android.service
|
|
||||||
|
|
||||||
import no.nordicsemi.android.ble.BleManagerCallbacks
|
|
||||||
import no.nordicsemi.android.ble.common.profile.battery.BatteryLevelCallback
|
|
||||||
|
|
||||||
interface BatteryManagerCallbacks : BleManagerCallbacks, BatteryLevelCallback
|
|
||||||
@@ -21,33 +21,21 @@
|
|||||||
*/
|
*/
|
||||||
package no.nordicsemi.android.service
|
package no.nordicsemi.android.service
|
||||||
|
|
||||||
import android.bluetooth.BluetoothAdapter
|
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
import android.bluetooth.BluetoothGatt
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Binder
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.IBinder
|
|
||||||
import android.util.Log
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.lifecycle.LifecycleService
|
import androidx.lifecycle.LifecycleService
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import no.nordicsemi.android.ble.BleManagerCallbacks
|
import no.nordicsemi.android.ble.BleManager
|
||||||
import no.nordicsemi.android.ble.utils.ILogger
|
|
||||||
import no.nordicsemi.android.log.ILogSession
|
import no.nordicsemi.android.log.ILogSession
|
||||||
import no.nordicsemi.android.log.Logger
|
import no.nordicsemi.android.log.Logger
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
abstract class BleProfileService : LifecycleService(), BleManagerCallbacks {
|
abstract class BleProfileService : LifecycleService() {
|
||||||
|
|
||||||
private var bleManager: LoggableBleManager<out BleManagerCallbacks>? = null
|
protected abstract val manager: BleManager
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var bluetoothDeviceHolder: SelectedBluetoothDeviceHolder
|
lateinit var bluetoothDeviceHolder: SelectedBluetoothDeviceHolder
|
||||||
@@ -56,9 +44,8 @@ abstract class BleProfileService : LifecycleService(), BleManagerCallbacks {
|
|||||||
* Returns a handler that is created in onCreate().
|
* Returns a handler that is created in onCreate().
|
||||||
* The handler may be used to postpone execution of some operations or to run them in UI thread.
|
* The handler may be used to postpone execution of some operations or to run them in UI thread.
|
||||||
*/
|
*/
|
||||||
protected var handler: Handler? = null
|
private var handler: Handler? = null
|
||||||
private set
|
|
||||||
protected var bound = false
|
|
||||||
private var activityIsChangingConfiguration = false
|
private var activityIsChangingConfiguration = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,256 +53,45 @@ abstract class BleProfileService : LifecycleService(), BleManagerCallbacks {
|
|||||||
*
|
*
|
||||||
* @return bluetooth device
|
* @return bluetooth device
|
||||||
*/
|
*/
|
||||||
protected val bluetoothDevice: BluetoothDevice by lazy {
|
private val bluetoothDevice: BluetoothDevice by lazy {
|
||||||
bluetoothDeviceHolder.device ?: throw IllegalArgumentException(
|
bluetoothDeviceHolder.device ?: throw IllegalArgumentException(
|
||||||
"No device address at EXTRA_DEVICE_ADDRESS key"
|
"No device associated with the application."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the device name
|
|
||||||
*
|
|
||||||
* @return the device name
|
|
||||||
*/
|
|
||||||
protected var deviceName: String? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the log session that can be used to append log entries. The method returns `null` if the nRF Logger app was not installed. It is safe to use logger when
|
* Returns the log session that can be used to append log entries. The method returns `null` if the nRF Logger app was not installed. It is safe to use logger when
|
||||||
* [.onServiceStarted] has been called.
|
* [.onServiceStarted] has been called.
|
||||||
*
|
*
|
||||||
* @return the log session
|
* @return the log session
|
||||||
*/
|
*/
|
||||||
protected var logSession: ILogSession? = null
|
private var logSession: ILogSession? = null
|
||||||
private set
|
private set
|
||||||
private val bluetoothStateBroadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.STATE_OFF)
|
|
||||||
val logger: ILogger = binder
|
|
||||||
val stateString =
|
|
||||||
"[Broadcast] Action received: " + BluetoothAdapter.ACTION_STATE_CHANGED + ", state changed to " + state2String(
|
|
||||||
state
|
|
||||||
)
|
|
||||||
logger.log(Log.DEBUG, stateString)
|
|
||||||
when (state) {
|
|
||||||
BluetoothAdapter.STATE_ON -> onBluetoothEnabled()
|
|
||||||
BluetoothAdapter.STATE_TURNING_OFF, BluetoothAdapter.STATE_OFF -> onBluetoothDisabled()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun state2String(state: Int): String {
|
|
||||||
return when (state) {
|
|
||||||
BluetoothAdapter.STATE_TURNING_ON -> "TURNING ON"
|
|
||||||
BluetoothAdapter.STATE_ON -> "ON"
|
|
||||||
BluetoothAdapter.STATE_TURNING_OFF -> "TURNING OFF"
|
|
||||||
BluetoothAdapter.STATE_OFF -> "OFF"
|
|
||||||
else -> "UNKNOWN ($state)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class LocalBinder : Binder(), ILogger {
|
|
||||||
/**
|
|
||||||
* Disconnects from the sensor.
|
|
||||||
*/
|
|
||||||
fun disconnect() {
|
|
||||||
val state = bleManager!!.connectionState
|
|
||||||
if (state == BluetoothGatt.STATE_DISCONNECTED || state == BluetoothGatt.STATE_DISCONNECTING) {
|
|
||||||
bleManager!!.close()
|
|
||||||
onDeviceDisconnected(bluetoothDevice!!)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
bleManager!!.disconnect().enqueue()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets whether the bound activity if changing configuration or not.
|
|
||||||
* If `false`, we will turn off battery level notifications in onUnbind(..) method below.
|
|
||||||
*
|
|
||||||
* @param changing true if the bound activity is finishing
|
|
||||||
*/
|
|
||||||
fun setActivityIsChangingConfiguration(changing: Boolean) {
|
|
||||||
activityIsChangingConfiguration = changing
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the device address
|
|
||||||
*
|
|
||||||
* @return device address
|
|
||||||
*/
|
|
||||||
val deviceAddress: String
|
|
||||||
get() = bluetoothDevice!!.address
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the device name
|
|
||||||
*
|
|
||||||
* @return the device name
|
|
||||||
*/
|
|
||||||
fun getDeviceName(): String? {
|
|
||||||
return deviceName
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the Bluetooth device
|
|
||||||
*
|
|
||||||
* @return the Bluetooth device
|
|
||||||
*/
|
|
||||||
fun getBluetoothDevice(): BluetoothDevice? {
|
|
||||||
return bluetoothDevice
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns `true` if the device is connected to the sensor.
|
|
||||||
*
|
|
||||||
* @return `true` if device is connected to the sensor, `false` otherwise
|
|
||||||
*/
|
|
||||||
val isConnected: Boolean
|
|
||||||
get() = bleManager!!.isConnected
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the connection state of given device.
|
|
||||||
*
|
|
||||||
* @return the connection state, as in [BleManager.getConnectionState].
|
|
||||||
*/
|
|
||||||
val connectionState: Int
|
|
||||||
get() = bleManager!!.connectionState
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the log session that can be used to append log entries.
|
|
||||||
* The log session is created when the service is being created.
|
|
||||||
* The method returns `null` if the nRF Logger app was not installed.
|
|
||||||
*
|
|
||||||
* @return the log session
|
|
||||||
*/
|
|
||||||
fun getLogSession(): ILogSession? {
|
|
||||||
return logSession
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun log(level: Int, message: String) {
|
|
||||||
Logger.log(logSession, level, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun log(level: Int, @StringRes messageRes: Int, vararg params: Any) {
|
|
||||||
Logger.log(logSession, level, messageRes, *params)
|
|
||||||
}
|
|
||||||
}// default implementation returns the basic binder. You can overwrite the LocalBinder with your own, wider implementation
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the binder implementation. This must return class implementing the additional manager interface that may be used in the bound activity.
|
|
||||||
*
|
|
||||||
* @return the service binder
|
|
||||||
*/
|
|
||||||
protected val binder: LocalBinder
|
|
||||||
protected get() =// default implementation returns the basic binder. You can overwrite the LocalBinder with your own, wider implementation
|
|
||||||
LocalBinder()
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
|
||||||
super.onBind(intent)
|
|
||||||
bound = true
|
|
||||||
return binder
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRebind(intent: Intent) {
|
|
||||||
bound = true
|
|
||||||
if (!activityIsChangingConfiguration) onRebind()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the activity has rebound to the service after being recreated.
|
|
||||||
* This method is not called when the activity was killed to be recreated when the phone orientation changed
|
|
||||||
* if prior to being killed called [LocalBinder.setActivityIsChangingConfiguration] with parameter true.
|
|
||||||
*/
|
|
||||||
protected open fun onRebind() {
|
|
||||||
// empty default implementation
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUnbind(intent: Intent): Boolean {
|
|
||||||
bound = false
|
|
||||||
if (!activityIsChangingConfiguration) onUnbind()
|
|
||||||
|
|
||||||
// We want the onRebind method be called if anything else binds to it again
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the activity has unbound from the service before being finished.
|
|
||||||
* This method is not called when the activity is killed to be recreated when the phone orientation changed.
|
|
||||||
*/
|
|
||||||
protected open fun onUnbind() {
|
|
||||||
// empty default implementation
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
handler = Handler()
|
handler = Handler()
|
||||||
|
|
||||||
// Initialize the manager
|
|
||||||
bleManager = initializeManager()
|
|
||||||
|
|
||||||
// Register broadcast receivers
|
|
||||||
registerReceiver(
|
|
||||||
bluetoothStateBroadcastReceiver,
|
|
||||||
IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Service has now been created
|
|
||||||
onServiceCreated()
|
|
||||||
|
|
||||||
// Call onBluetoothEnabled if Bluetooth enabled
|
|
||||||
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
|
|
||||||
if (bluetoothAdapter.isEnabled) {
|
|
||||||
onBluetoothEnabled()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the service has been created, before the [.onBluetoothEnabled] is called.
|
|
||||||
*/
|
|
||||||
protected fun onServiceCreated() {
|
|
||||||
// empty default implementation
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the Ble Manager responsible for connecting to a single device.
|
|
||||||
*
|
|
||||||
* @return a new BleManager object
|
|
||||||
*/
|
|
||||||
protected abstract fun initializeManager(): LoggableBleManager<out BleManagerCallbacks>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method returns whether autoConnect option should be used.
|
* This method returns whether autoConnect option should be used.
|
||||||
*
|
*
|
||||||
* @return true to use autoConnect feature, false (default) otherwise.
|
* @return true to use autoConnect feature, false (default) otherwise.
|
||||||
*/
|
*/
|
||||||
protected fun shouldAutoConnect(): Boolean {
|
private fun shouldAutoConnect(): Boolean {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
super.onStartCommand(intent, flags, startId)
|
super.onStartCommand(intent, flags, startId)
|
||||||
|
|
||||||
val logUri = intent?.getParcelableExtra<Uri>(EXTRA_LOG_URI)
|
manager.connect(bluetoothDevice)
|
||||||
logSession = Logger.openSession(applicationContext, logUri)
|
|
||||||
deviceName = intent?.getStringExtra(EXTRA_DEVICE_NAME)
|
|
||||||
Logger.i(logSession, "Service started")
|
|
||||||
val adapter = BluetoothAdapter.getDefaultAdapter()
|
|
||||||
bleManager!!.setLogger(logSession)
|
|
||||||
onServiceStarted()
|
|
||||||
bleManager!!.connect(bluetoothDevice)
|
|
||||||
.useAutoConnect(shouldAutoConnect())
|
.useAutoConnect(shouldAutoConnect())
|
||||||
.retry(3, 100)
|
.retry(3, 100)
|
||||||
.enqueue()
|
.enqueue()
|
||||||
return START_REDELIVER_INTENT
|
return START_REDELIVER_INTENT
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the service has been started. The device name and address are set.
|
|
||||||
* The BLE Manager will try to connect to the device after this method finishes.
|
|
||||||
*/
|
|
||||||
protected fun onServiceStarted() {
|
|
||||||
// empty default implementation
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTaskRemoved(rootIntent: Intent) {
|
override fun onTaskRemoved(rootIntent: Intent) {
|
||||||
super.onTaskRemoved(rootIntent)
|
super.onTaskRemoved(rootIntent)
|
||||||
// This method is called when user removed the app from Recents.
|
// This method is called when user removed the app from Recents.
|
||||||
@@ -326,58 +102,15 @@ abstract class BleProfileService : LifecycleService(), BleManagerCallbacks {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
// Unregister broadcast receivers
|
|
||||||
unregisterReceiver(bluetoothStateBroadcastReceiver)
|
|
||||||
|
|
||||||
// shutdown the manager
|
// shutdown the manager
|
||||||
bleManager!!.close()
|
manager.close()
|
||||||
Logger.i(logSession, "Service destroyed")
|
Logger.i(logSession, "Service destroyed")
|
||||||
bleManager = null
|
|
||||||
bluetoothDeviceHolder.forgetDevice()
|
bluetoothDeviceHolder.forgetDevice()
|
||||||
deviceName = null
|
|
||||||
logSession = null
|
logSession = null
|
||||||
handler = null
|
handler = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method called when Bluetooth Adapter has been disabled.
|
|
||||||
*/
|
|
||||||
protected fun onBluetoothDisabled() {
|
|
||||||
// empty default implementation
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method is called when Bluetooth Adapter has been enabled and
|
|
||||||
* after the service was created if Bluetooth Adapter was enabled at that moment.
|
|
||||||
* This method could initialize all Bluetooth related features, for example open the GATT server.
|
|
||||||
*/
|
|
||||||
protected fun onBluetoothEnabled() {
|
|
||||||
// empty default implementation
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDeviceConnecting(device: BluetoothDevice) {
|
|
||||||
val broadcast = Intent(BROADCAST_CONNECTION_STATE)
|
|
||||||
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice)
|
|
||||||
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTING)
|
|
||||||
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDeviceConnected(device: BluetoothDevice) {
|
|
||||||
val broadcast = Intent(BROADCAST_CONNECTION_STATE)
|
|
||||||
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_CONNECTED)
|
|
||||||
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice)
|
|
||||||
broadcast.putExtra(EXTRA_DEVICE_NAME, deviceName)
|
|
||||||
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDeviceDisconnecting(device: BluetoothDevice) {
|
|
||||||
// Notify user about changing the state to DISCONNECTING
|
|
||||||
val broadcast = Intent(BROADCAST_CONNECTION_STATE)
|
|
||||||
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice)
|
|
||||||
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTING)
|
|
||||||
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method should return false if the service needs to do some asynchronous work after if has disconnected from the device.
|
* This method should return false if the service needs to do some asynchronous work after if has disconnected from the device.
|
||||||
* In that case the [.stopService] method must be called when done.
|
* In that case the [.stopService] method must be called when done.
|
||||||
@@ -388,102 +121,19 @@ abstract class BleProfileService : LifecycleService(), BleManagerCallbacks {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDeviceDisconnected(device: BluetoothDevice) {
|
private fun stopService() {
|
||||||
// Note 1: Do not use the device argument here unless you change calling onDeviceDisconnected from the binder above
|
|
||||||
|
|
||||||
// Note 2: if BleManager#shouldAutoConnect() for this device returned true, this callback will be
|
|
||||||
// invoked ONLY when user requested disconnection (using Disconnect button). If the device
|
|
||||||
// disconnects due to a link loss, the onLinkLossOccurred(BluetoothDevice) method will be called instead.
|
|
||||||
val broadcast = Intent(BROADCAST_CONNECTION_STATE)
|
|
||||||
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice)
|
|
||||||
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_DISCONNECTED)
|
|
||||||
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast)
|
|
||||||
if (stopWhenDisconnected()) stopService()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun stopService() {
|
|
||||||
// user requested disconnection. We must stop the service
|
// user requested disconnection. We must stop the service
|
||||||
Logger.v(logSession, "Stopping service...")
|
Logger.v(logSession, "Stopping service...")
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLinkLossOccurred(device: BluetoothDevice) {
|
|
||||||
val broadcast = Intent(BROADCAST_CONNECTION_STATE)
|
|
||||||
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice)
|
|
||||||
broadcast.putExtra(EXTRA_CONNECTION_STATE, STATE_LINK_LOSS)
|
|
||||||
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onServicesDiscovered(device: BluetoothDevice, optionalServicesFound: Boolean) {
|
|
||||||
val broadcast = Intent(BROADCAST_SERVICES_DISCOVERED)
|
|
||||||
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice)
|
|
||||||
broadcast.putExtra(EXTRA_SERVICE_PRIMARY, true)
|
|
||||||
broadcast.putExtra(EXTRA_SERVICE_SECONDARY, optionalServicesFound)
|
|
||||||
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDeviceReady(device: BluetoothDevice) {
|
|
||||||
val broadcast = Intent(BROADCAST_DEVICE_READY)
|
|
||||||
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice)
|
|
||||||
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDeviceNotSupported(device: BluetoothDevice) {
|
|
||||||
val broadcast = Intent(BROADCAST_SERVICES_DISCOVERED)
|
|
||||||
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice)
|
|
||||||
broadcast.putExtra(EXTRA_SERVICE_PRIMARY, false)
|
|
||||||
broadcast.putExtra(EXTRA_SERVICE_SECONDARY, false)
|
|
||||||
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast)
|
|
||||||
|
|
||||||
// no need for disconnecting, it will be disconnected by the manager automatically
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBatteryValueReceived(device: BluetoothDevice, value: Int) {
|
|
||||||
val broadcast = Intent(BROADCAST_BATTERY_LEVEL)
|
|
||||||
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice)
|
|
||||||
broadcast.putExtra(EXTRA_BATTERY_LEVEL, value)
|
|
||||||
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBondingRequired(device: BluetoothDevice) {
|
|
||||||
showToast(R.string.csc_bonding)
|
|
||||||
val broadcast = Intent(BROADCAST_BOND_STATE)
|
|
||||||
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice)
|
|
||||||
broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDING)
|
|
||||||
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBonded(device: BluetoothDevice) {
|
|
||||||
showToast(R.string.csc_bonded)
|
|
||||||
val broadcast = Intent(BROADCAST_BOND_STATE)
|
|
||||||
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice)
|
|
||||||
broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_BONDED)
|
|
||||||
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBondingFailed(device: BluetoothDevice) {
|
|
||||||
showToast(R.string.csc_bonding_failed)
|
|
||||||
val broadcast = Intent(BROADCAST_BOND_STATE)
|
|
||||||
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice)
|
|
||||||
broadcast.putExtra(EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE)
|
|
||||||
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(device: BluetoothDevice, message: String, errorCode: Int) {
|
|
||||||
val broadcast = Intent(BROADCAST_ERROR)
|
|
||||||
broadcast.putExtra(EXTRA_DEVICE, bluetoothDevice)
|
|
||||||
broadcast.putExtra(EXTRA_ERROR_MESSAGE, message)
|
|
||||||
broadcast.putExtra(EXTRA_ERROR_CODE, errorCode)
|
|
||||||
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows a message as a Toast notification. This method is thread safe, you can call it from any thread
|
* Shows a message as a Toast notification. This method is thread safe, you can call it from any thread
|
||||||
*
|
*
|
||||||
* @param messageResId an resource id of the message to be shown
|
* @param messageResId an resource id of the message to be shown
|
||||||
*/
|
*/
|
||||||
protected fun showToast(messageResId: Int) {
|
protected fun showToast(messageResId: Int) {
|
||||||
handler!!.post {
|
handler?.post {
|
||||||
Toast.makeText(this@BleProfileService, messageResId, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this@BleProfileService, messageResId, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -494,7 +144,7 @@ abstract class BleProfileService : LifecycleService(), BleManagerCallbacks {
|
|||||||
* @param message a message to be shown
|
* @param message a message to be shown
|
||||||
*/
|
*/
|
||||||
protected fun showToast(message: String?) {
|
protected fun showToast(message: String?) {
|
||||||
handler!!.post {
|
handler?.post {
|
||||||
Toast.makeText(this@BleProfileService, message, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this@BleProfileService, message, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -505,7 +155,7 @@ abstract class BleProfileService : LifecycleService(), BleManagerCallbacks {
|
|||||||
* @return device address
|
* @return device address
|
||||||
*/
|
*/
|
||||||
protected val deviceAddress: String
|
protected val deviceAddress: String
|
||||||
protected get() = bluetoothDevice!!.address
|
get() = bluetoothDevice.address
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns `true` if the device is connected to the sensor.
|
* Returns `true` if the device is connected to the sensor.
|
||||||
@@ -513,41 +163,5 @@ abstract class BleProfileService : LifecycleService(), BleManagerCallbacks {
|
|||||||
* @return `true` if device is connected to the sensor, `false` otherwise
|
* @return `true` if device is connected to the sensor, `false` otherwise
|
||||||
*/
|
*/
|
||||||
protected val isConnected: Boolean
|
protected val isConnected: Boolean
|
||||||
protected get() = bleManager != null && bleManager!!.isConnected
|
get() = manager.isConnected
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "BleProfileService"
|
|
||||||
const val BROADCAST_CONNECTION_STATE =
|
|
||||||
"no.nordicsemi.android.nrftoolbox.BROADCAST_CONNECTION_STATE"
|
|
||||||
const val BROADCAST_SERVICES_DISCOVERED =
|
|
||||||
"no.nordicsemi.android.nrftoolbox.BROADCAST_SERVICES_DISCOVERED"
|
|
||||||
const val BROADCAST_DEVICE_READY = "no.nordicsemi.android.nrftoolbox.DEVICE_READY"
|
|
||||||
const val BROADCAST_BOND_STATE = "no.nordicsemi.android.nrftoolbox.BROADCAST_BOND_STATE"
|
|
||||||
|
|
||||||
@Deprecated("")
|
|
||||||
val BROADCAST_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.BROADCAST_BATTERY_LEVEL"
|
|
||||||
const val BROADCAST_ERROR = "no.nordicsemi.android.nrftoolbox.BROADCAST_ERROR"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The key for the device name that is returned in [.BROADCAST_CONNECTION_STATE] with state [.STATE_CONNECTED].
|
|
||||||
*/
|
|
||||||
const val EXTRA_DEVICE_NAME = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE_NAME"
|
|
||||||
const val EXTRA_DEVICE = "no.nordicsemi.android.nrftoolbox.EXTRA_DEVICE"
|
|
||||||
const val EXTRA_LOG_URI = "no.nordicsemi.android.nrftoolbox.EXTRA_LOG_URI"
|
|
||||||
const val EXTRA_CONNECTION_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_CONNECTION_STATE"
|
|
||||||
const val EXTRA_BOND_STATE = "no.nordicsemi.android.nrftoolbox.EXTRA_BOND_STATE"
|
|
||||||
const val EXTRA_SERVICE_PRIMARY = "no.nordicsemi.android.nrftoolbox.EXTRA_SERVICE_PRIMARY"
|
|
||||||
const val EXTRA_SERVICE_SECONDARY =
|
|
||||||
"no.nordicsemi.android.nrftoolbox.EXTRA_SERVICE_SECONDARY"
|
|
||||||
|
|
||||||
@Deprecated("")
|
|
||||||
val EXTRA_BATTERY_LEVEL = "no.nordicsemi.android.nrftoolbox.EXTRA_BATTERY_LEVEL"
|
|
||||||
const val EXTRA_ERROR_MESSAGE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_MESSAGE"
|
|
||||||
const val EXTRA_ERROR_CODE = "no.nordicsemi.android.nrftoolbox.EXTRA_ERROR_CODE"
|
|
||||||
const val STATE_LINK_LOSS = -1
|
|
||||||
const val STATE_DISCONNECTED = 0
|
|
||||||
const val STATE_CONNECTED = 1
|
|
||||||
const val STATE_CONNECTING = 2
|
|
||||||
const val STATE_DISCONNECTING = 3
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package no.nordicsemi.android.service
|
|
||||||
|
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
|
||||||
|
|
||||||
abstract class BluetoothDataReadBroadcast<T> {
|
|
||||||
|
|
||||||
private val _event = MutableSharedFlow<T>(
|
|
||||||
replay = 1,
|
|
||||||
extraBufferCapacity = 1,
|
|
||||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
|
||||||
)
|
|
||||||
val events: SharedFlow<T> = _event
|
|
||||||
|
|
||||||
fun offer(newEvent: T) {
|
|
||||||
_event.tryEmit(newEvent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -31,9 +31,7 @@ import androidx.core.app.NotificationCompat
|
|||||||
|
|
||||||
private const val CHANNEL_ID = "FOREGROUND_BLE_SERVICE"
|
private const val CHANNEL_ID = "FOREGROUND_BLE_SERVICE"
|
||||||
|
|
||||||
abstract class ForegroundBleService<T : BatteryManager<out BatteryManagerCallbacks>> : BleProfileService() {
|
abstract class ForegroundBleService : BleProfileService() {
|
||||||
|
|
||||||
protected abstract val manager: T
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
val result = super.onStartCommand(intent, flags, startId)
|
val result = super.onStartCommand(intent, flags, startId)
|
||||||
@@ -48,22 +46,6 @@ abstract class ForegroundBleService<T : BatteryManager<out BatteryManagerCallbac
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRebind() {
|
|
||||||
stopForegroundService()
|
|
||||||
if (isConnected) {
|
|
||||||
// This method will read the Battery Level value, if possible and then try to enable battery notifications (if it has NOTIFY property).
|
|
||||||
// If the Battery Level characteristic has only the NOTIFY property, it will only try to enable notifications.
|
|
||||||
manager.readBatteryLevelCharacteristic()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUnbind() {
|
|
||||||
// When we are connected, but the application is not open, we are not really interested in battery level notifications.
|
|
||||||
// But we will still be receiving other values, if enabled.
|
|
||||||
if (isConnected) manager.disableBatteryLevelCharacteristicNotifications()
|
|
||||||
startForegroundService()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the service as a foreground service
|
* Sets the service as a foreground service
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
package no.nordicsemi.android.service
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.Log
|
|
||||||
import no.nordicsemi.android.ble.BleManagerCallbacks
|
|
||||||
import no.nordicsemi.android.ble.LegacyBleManager
|
|
||||||
import no.nordicsemi.android.log.ILogSession
|
|
||||||
import no.nordicsemi.android.log.LogContract
|
|
||||||
import no.nordicsemi.android.log.Logger
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The manager that logs to nRF Logger. If nRF Logger is not installed, logs are ignored.
|
|
||||||
*
|
|
||||||
* @param <T> the callbacks class.
|
|
||||||
</T> */
|
|
||||||
abstract class LoggableBleManager<T : BleManagerCallbacks?>(context: Context) : LegacyBleManager<T>(context) {
|
|
||||||
private var logSession: ILogSession? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the log session to log into.
|
|
||||||
*
|
|
||||||
* @param session nRF Logger log session to log inti, or null, if nRF Logger is not installed.
|
|
||||||
*/
|
|
||||||
fun setLogger(session: ILogSession?) {
|
|
||||||
logSession = session
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun log(priority: Int, message: String) {
|
|
||||||
Logger.log(logSession, LogContract.Log.Level.fromPriority(priority), message)
|
|
||||||
Log.println(priority, "BleManager", message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,7 @@ class SelectedBluetoothDeviceHolder constructor(
|
|||||||
return deviceManager.associations.firstOrNull()?.let { bluetoothAdapter?.getRemoteDevice(it) }
|
return deviceManager.associations.firstOrNull()?.let { bluetoothAdapter?.getRemoteDevice(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isDeviceBonded(): Boolean {
|
fun isBondingRequired(): Boolean {
|
||||||
return device?.bondState == BluetoothDevice.BOND_NONE
|
return device?.bondState == BluetoothDevice.BOND_NONE
|
||||||
}
|
}
|
||||||
fun bondDevice() {
|
fun bondDevice() {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest package="no.nordicsemi.android.theme">
|
||||||
package="no.nordicsemi.android.theme">
|
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -28,16 +28,17 @@ object NordicColors {
|
|||||||
val TableViewBackground = NeutralColor(Color(0xFFF2F2F6))
|
val TableViewBackground = NeutralColor(Color(0xFFF2F2F6))
|
||||||
val TableViewSeparator = NeutralColor(Color(0xFFD2D2D6))
|
val TableViewSeparator = NeutralColor(Color(0xFFD2D2D6))
|
||||||
|
|
||||||
val Primary = ThemedColor(Color(0xFF00A9CE), Color(0xFF212121))
|
val Primary = ThemedColor(Color(0xFF00A9CE), Color(0xFF00A9CE))
|
||||||
val PrimaryVariant = ThemedColor(Color(0xFF008CD2), Color.Black)
|
val PrimaryVariant = ThemedColor(Color(0xFF008CD2), Color(0xFF00A9CE))
|
||||||
val Secondary = ThemedColor(Color(0xFF00A9CE), Color(0xFF008CD2))
|
val Secondary = ThemedColor(Color(0xFF00A9CE), Color(0xFF00A9CE))
|
||||||
val SecondaryVariant = ThemedColor(Color(0xFF008CD2), Color(0xFF008CD2))
|
val SecondaryVariant = ThemedColor(Color(0xFF008CD2), Color(0xFF00A9CE))
|
||||||
val OnPrimary = ThemedColor(Color.White, Color.White)
|
val OnPrimary = ThemedColor(Color.White, Color.White)
|
||||||
val OnSecondary = ThemedColor(Color.White, Color.White)
|
val OnSecondary = ThemedColor(Color.White, Color.White)
|
||||||
val OnBackground = ThemedColor(Color.Black, Color.White)
|
val OnBackground = ThemedColor(Color.Black, Color.White)
|
||||||
val OnSurface = ThemedColor(Color.Black, Color.White)
|
val OnSurface = ThemedColor(Color.Black, Color.White)
|
||||||
val Background = ThemedColor(Color(0xFFDADADA), Color.Black)
|
val ItemHighlight = ThemedColor(Color.White, Color(0xFF1E1E1E))
|
||||||
val Surface = ThemedColor(Color(0xFFDADADA), Color.Black)
|
val Background = ThemedColor(Color(0xFFF5F5F5), Color(0xFF121212))
|
||||||
|
val Surface = ThemedColor(Color(0xFFF5F5F5), Color(0xFF121212))
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class NordicColor {
|
sealed class NordicColor {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import androidx.compose.material.lightColors
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TestTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
|
fun TestTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
|
||||||
|
|
||||||
val darkColorPalette = darkColors(
|
val darkColorPalette = darkColors(
|
||||||
primary = NordicColors.Primary.value(),
|
primary = NordicColors.Primary.value(),
|
||||||
|
|||||||
@@ -1,28 +1,15 @@
|
|||||||
package no.nordicsemi.android.theme.view
|
package no.nordicsemi.android.theme.view
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.Card
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import no.nordicsemi.android.theme.NordicColors
|
|
||||||
import no.nordicsemi.android.theme.R
|
import no.nordicsemi.android.theme.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BatteryLevelView(batteryLevel: Int) {
|
fun BatteryLevelView(batteryLevel: Int) {
|
||||||
Card(
|
ScreenSection {
|
||||||
backgroundColor = NordicColors.NordicGray4.value(),
|
|
||||||
shape = RoundedCornerShape(10.dp),
|
|
||||||
elevation = 0.dp
|
|
||||||
) {
|
|
||||||
Box(modifier = Modifier.padding(16.dp)) {
|
|
||||||
KeyValueField(
|
KeyValueField(
|
||||||
stringResource(id = R.string.field_battery),
|
stringResource(id = R.string.field_battery),
|
||||||
"$batteryLevel%"
|
"$batteryLevel%"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import no.nordicsemi.android.theme.NordicColors
|
import no.nordicsemi.android.theme.NordicColors
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun KeyValueField(key: String, value: String) {
|
fun KeyValueField(key: String, value: String) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
package no.nordicsemi.android.theme.view
|
package no.nordicsemi.android.theme.view
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.Card
|
import androidx.compose.material.Card
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import no.nordicsemi.android.theme.NordicColors
|
import no.nordicsemi.android.theme.NordicColors
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SensorRecordCard(content: @Composable () -> Unit) {
|
fun ScreenSection(content: @Composable () -> Unit) {
|
||||||
Card(
|
Card(
|
||||||
backgroundColor = NordicColors.NordicGray4.value(),
|
backgroundColor = NordicColors.ItemHighlight.value(),
|
||||||
shape = RoundedCornerShape(10.dp),
|
shape = RoundedCornerShape(4.dp),
|
||||||
elevation = 0.dp
|
elevation = 0.dp
|
||||||
) {
|
) {
|
||||||
|
Box(modifier = Modifier.padding(16.dp)) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package no.nordicsemi.android.csc.view
|
package no.nordicsemi.android.theme.view
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -12,10 +12,10 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SpeedUnitRadioGroup(
|
fun <T> SpeedUnitRadioGroup(
|
||||||
currentItem: RadioGroupItem,
|
currentItem: T,
|
||||||
items: List<RadioGroupItem>,
|
items: List<RadioGroupItem<T>>,
|
||||||
onEvent: (RadioGroupItem) -> Unit
|
onEvent: (RadioGroupItem<T>) -> Unit
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -28,14 +28,14 @@ internal fun SpeedUnitRadioGroup(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SpeedUnitRadioButton(
|
internal fun <T> SpeedUnitRadioButton(
|
||||||
selectedItem: RadioGroupItem,
|
selectedItem: T,
|
||||||
displayedItem: RadioGroupItem,
|
displayedItem: RadioGroupItem<T>,
|
||||||
onEvent: (RadioGroupItem) -> Unit
|
onEvent: (RadioGroupItem<T>) -> Unit
|
||||||
) {
|
) {
|
||||||
Row {
|
Row {
|
||||||
RadioButton(
|
RadioButton(
|
||||||
selected = (selectedItem == displayedItem),
|
selected = (selectedItem == displayedItem.unit),
|
||||||
onClick = { onEvent(displayedItem) }
|
onClick = { onEvent(displayedItem) }
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
@@ -43,12 +43,4 @@ internal fun SpeedUnitRadioButton(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun createSpeedUnitLabel(unit: SpeedUnit): String {
|
data class RadioGroupItem<T>(val unit: T, val label: String)
|
||||||
return when (unit) {
|
|
||||||
SpeedUnit.M_S -> "m/s"
|
|
||||||
SpeedUnit.KM_H -> "km/h"
|
|
||||||
SpeedUnit.MPH -> "mph"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class RadioGroupItem(val label: String)
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package no.nordicsemi.android.theme.view
|
||||||
|
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.IconButton
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TopAppBar
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import no.nordicsemi.android.theme.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CloseIconAppBar(text: String, onClick: () -> Unit) {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(text) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { onClick() }) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Close,
|
||||||
|
contentDescription = stringResource(id = R.string.close_app),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BackIconAppBar(text: String, onClick: () -> Unit) {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(text) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { onClick() }) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.ArrowBack,
|
||||||
|
contentDescription = stringResource(id = R.string.back_screen),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package no.nordicsemi.android.theme.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
||||||
|
abstract class CloseableViewModel : ViewModel() {
|
||||||
|
|
||||||
|
var isActive = MutableStateFlow(true)
|
||||||
|
|
||||||
|
protected fun finish() {
|
||||||
|
isActive.tryEmit(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,6 @@
|
|||||||
<color name="colorSecondary">#FF0077c8</color>
|
<color name="colorSecondary">#FF0077c8</color>
|
||||||
<color name="colorSecondaryDark">#FF004c97</color>
|
<color name="colorSecondaryDark">#FF004c97</color>
|
||||||
<color name="colorOnSecondary">#FFFFFFFF</color>
|
<color name="colorOnSecondary">#FFFFFFFF</color>
|
||||||
<color name="background">#FFDADADA</color>
|
<color name="background">#FFF5F5F5</color>
|
||||||
<color name="actionBarColor">#FF0090B0</color>
|
<color name="actionBarColor">#FF0090B0</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">nRF Toolbox</string>
|
<string name="app_name">nRF Toolbox</string>
|
||||||
|
|
||||||
<string name="disconnect">Disconnect</string>
|
<string name="close_app">Close the application.</string>
|
||||||
|
<string name="back_screen">Close the current screen.</string>
|
||||||
|
|
||||||
|
<string name="disconnect">DISCONNECT</string>
|
||||||
<string name="field_battery">Battery</string>
|
<string name="field_battery">Battery</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest package="no.nordicsemi.android.utils">
|
||||||
package="no.nordicsemi.android.utils">
|
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
package no.nordicsemi.android.csc.data
|
package no.nordicsemi.android.csc.data
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
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 no.nordicsemi.android.theme.view.RadioGroupItem
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
internal data class CSCData(
|
internal data class CSCData(
|
||||||
@@ -9,15 +11,21 @@ internal data class CSCData(
|
|||||||
val scanDevices: Boolean = false,
|
val scanDevices: Boolean = false,
|
||||||
val selectedSpeedUnit: SpeedUnit = SpeedUnit.M_S,
|
val selectedSpeedUnit: SpeedUnit = SpeedUnit.M_S,
|
||||||
val speed: Float = 0f,
|
val speed: Float = 0f,
|
||||||
val cadence: Int = 0,
|
val cadence: Float = 0f,
|
||||||
val distance: Float = 0f,
|
val distance: Float = 0f,
|
||||||
val totalDistance: Float = 0f,
|
val totalDistance: Float = 0f,
|
||||||
val gearRatio: Float = 0f,
|
val gearRatio: Float = 0f,
|
||||||
val batteryLevel: Int = 0,
|
val batteryLevel: Int = 0,
|
||||||
val wheelSize: String = CSCSettings.DefaultWheelSize.NAME,
|
val wheelSize: Int = CSCSettings.DefaultWheelSize.VALUE,
|
||||||
val isScreenActive: Boolean = true
|
val wheelSizeDisplay: String = CSCSettings.DefaultWheelSize.NAME
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun drawItself() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private val speedWithUnit = when (selectedSpeedUnit) {
|
private val speedWithUnit = when (selectedSpeedUnit) {
|
||||||
SpeedUnit.M_S -> speed
|
SpeedUnit.M_S -> speed
|
||||||
SpeedUnit.KM_H -> speed * 3.6f
|
SpeedUnit.KM_H -> speed * 3.6f
|
||||||
@@ -33,7 +41,7 @@ internal data class CSCData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun displayCadence(): String {
|
fun displayCadence(): String {
|
||||||
return String.format("%d RPM", cadence)
|
return String.format("%.0f RPM", cadence)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun displayDistance(): String {
|
fun displayDistance(): String {
|
||||||
@@ -56,7 +64,11 @@ internal data class CSCData(
|
|||||||
return String.format(Locale.US, "%.1f", gearRatio)
|
return String.format(Locale.US, "%.1f", gearRatio)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun items(): List<> {
|
fun items(): List<RadioGroupItem<SpeedUnit>> {
|
||||||
|
return listOf(
|
||||||
|
RadioGroupItem(SpeedUnit.M_S,"m/s"),
|
||||||
|
RadioGroupItem(SpeedUnit.KM_H, "km/h"),
|
||||||
|
RadioGroupItem(SpeedUnit.MPH, "mph")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package no.nordicsemi.android.csc.data
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import no.nordicsemi.android.csc.view.SpeedUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
internal class CSCDataHolder @Inject constructor() {
|
||||||
|
|
||||||
|
private val _data = MutableStateFlow(CSCData())
|
||||||
|
val data: StateFlow<CSCData> = _data
|
||||||
|
|
||||||
|
fun setWheelSize(wheelSize: Int, wheelSizeDisplay: String) {
|
||||||
|
_data.tryEmit(_data.value.copy(
|
||||||
|
wheelSize = wheelSize,
|
||||||
|
wheelSizeDisplay = wheelSizeDisplay,
|
||||||
|
showDialog = false
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSpeedUnit(selectedSpeedUnit: SpeedUnit) {
|
||||||
|
_data.tryEmit(_data.value.copy(selectedSpeedUnit = selectedSpeedUnit))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDisplayWheelSizeDialog() {
|
||||||
|
_data.tryEmit(_data.value.copy(showDialog = true))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setNewDistance(totalDistance: Float, distance: Float, speed: Float) {
|
||||||
|
_data.tryEmit(_data.value.copy(totalDistance = totalDistance, distance = distance, speed = speed))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setNewCrankCadence(crankCadence: Float, gearRatio: Float) {
|
||||||
|
_data.tryEmit(_data.value.copy(cadence = crankCadence, gearRatio = gearRatio))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBatteryLevel(batteryLevel: Int) {
|
||||||
|
_data.tryEmit(_data.value.copy(batteryLevel = batteryLevel))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
_data.tryEmit(CSCData())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ import android.util.Log
|
|||||||
import androidx.annotation.FloatRange
|
import androidx.annotation.FloatRange
|
||||||
import no.nordicsemi.android.ble.common.callback.csc.CyclingSpeedAndCadenceMeasurementDataCallback
|
import no.nordicsemi.android.ble.common.callback.csc.CyclingSpeedAndCadenceMeasurementDataCallback
|
||||||
import no.nordicsemi.android.ble.data.Data
|
import no.nordicsemi.android.ble.data.Data
|
||||||
|
import no.nordicsemi.android.csc.data.CSCDataHolder
|
||||||
import no.nordicsemi.android.csc.service.CSCMeasurementParser.parse
|
import no.nordicsemi.android.csc.service.CSCMeasurementParser.parse
|
||||||
import no.nordicsemi.android.csc.view.CSCSettings
|
import no.nordicsemi.android.csc.view.CSCSettings
|
||||||
import no.nordicsemi.android.log.LogContract
|
import no.nordicsemi.android.log.LogContract
|
||||||
@@ -41,11 +42,15 @@ private val CYCLING_SPEED_AND_CADENCE_SERVICE_UUID = UUID.fromString("00001816-0
|
|||||||
/** Cycling Speed and Cadence Measurement characteristic UUID. */
|
/** Cycling Speed and Cadence Measurement characteristic UUID. */
|
||||||
private val CSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A5B-0000-1000-8000-00805f9b34fb")
|
private val CSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A5B-0000-1000-8000-00805f9b34fb")
|
||||||
|
|
||||||
internal class CSCManager(context: Context) : BatteryManager<CSCManagerCallbacks>(context) {
|
internal class CSCManager(context: Context, private val dataHolder: CSCDataHolder) : BatteryManager(context) {
|
||||||
|
|
||||||
private var cscMeasurementCharacteristic: BluetoothGattCharacteristic? = null
|
private var cscMeasurementCharacteristic: BluetoothGattCharacteristic? = null
|
||||||
private var wheelSize = CSCSettings.DefaultWheelSize.VALUE
|
private var wheelSize = CSCSettings.DefaultWheelSize.VALUE
|
||||||
|
|
||||||
|
override fun onBatteryLevelChanged(batteryLevel: Int) {
|
||||||
|
dataHolder.setBatteryLevel(batteryLevel)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getGattCallback(): BatteryManagerGattCallback {
|
override fun getGattCallback(): BatteryManagerGattCallback {
|
||||||
return CSCManagerGattCallback()
|
return CSCManagerGattCallback()
|
||||||
}
|
}
|
||||||
@@ -82,7 +87,7 @@ internal class CSCManager(context: Context) : BatteryManager<CSCManagerCallbacks
|
|||||||
@FloatRange(from = 0.0) distance: Float,
|
@FloatRange(from = 0.0) distance: Float,
|
||||||
@FloatRange(from = 0.0) speed: Float
|
@FloatRange(from = 0.0) speed: Float
|
||||||
) {
|
) {
|
||||||
mCallbacks?.onDistanceChanged(device, totalDistance, distance, speed)
|
dataHolder.setNewDistance(totalDistance, distance, speed)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCrankDataChanged(
|
override fun onCrankDataChanged(
|
||||||
@@ -90,7 +95,7 @@ internal class CSCManager(context: Context) : BatteryManager<CSCManagerCallbacks
|
|||||||
@FloatRange(from = 0.0) crankCadence: Float,
|
@FloatRange(from = 0.0) crankCadence: Float,
|
||||||
gearRatio: Float
|
gearRatio: Float
|
||||||
) {
|
) {
|
||||||
mCallbacks?.onCrankDataChanged(device, crankCadence, gearRatio)
|
dataHolder.setNewCrankCadence(crankCadence, gearRatio)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onInvalidDataReceived(
|
override fun onInvalidDataReceived(
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package no.nordicsemi.android.csc.service
|
||||||
|
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import no.nordicsemi.android.csc.data.CSCDataHolder
|
||||||
|
import no.nordicsemi.android.service.ForegroundBleService
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
internal class CSCService : ForegroundBleService() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var dataHolder: CSCDataHolder
|
||||||
|
|
||||||
|
override val manager: CSCManager by lazy { CSCManager(this, dataHolder) }
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ package no.nordicsemi.android.csc.view
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material.Button
|
import androidx.compose.material.Button
|
||||||
import androidx.compose.material.ButtonDefaults
|
import androidx.compose.material.ButtonDefaults
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
@@ -16,7 +15,8 @@ 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.data.CSCData
|
import no.nordicsemi.android.csc.data.CSCData
|
||||||
import no.nordicsemi.android.theme.view.SensorRecordCard
|
import no.nordicsemi.android.theme.view.ScreenSection
|
||||||
|
import no.nordicsemi.android.theme.view.SpeedUnitRadioGroup
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
||||||
@@ -25,9 +25,10 @@ internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
SettingsSection(state, onEvent)
|
SettingsSection(state, onEvent)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
@@ -47,16 +48,17 @@ internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SettingsSection(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
private fun SettingsSection(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
||||||
SensorRecordCard {
|
ScreenSection {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
WheelSizeView(state, onEvent)
|
WheelSizeView(state, onEvent)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
SpeedUnitRadioGroup(state.selectedSpeedUnit) { onEvent(it) }
|
SpeedUnitRadioGroup(state.selectedSpeedUnit, state.items()) {
|
||||||
|
onEvent(OnSelectedSpeedUnitSelected(it.unit))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,8 +2,6 @@ package no.nordicsemi.android.csc.view
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.compose.foundation.layout.Column
|
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.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -11,19 +9,21 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
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.data.CSCData
|
import no.nordicsemi.android.csc.data.CSCData
|
||||||
import no.nordicsemi.android.csc.viewmodel.CscViewModel
|
import no.nordicsemi.android.csc.service.CSCService
|
||||||
|
import no.nordicsemi.android.csc.viewmodel.CSCViewModel
|
||||||
|
import no.nordicsemi.android.theme.view.BackIconAppBar
|
||||||
import no.nordicsemi.android.utils.isServiceRunning
|
import no.nordicsemi.android.utils.isServiceRunning
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CscScreen(finishAction: () -> Unit) {
|
fun CSCScreen(finishAction: () -> Unit) {
|
||||||
val viewModel: CscViewModel = hiltViewModel()
|
val viewModel: CSCViewModel = hiltViewModel()
|
||||||
val state = viewModel.state.collectAsState().value
|
val state = viewModel.state.collectAsState().value
|
||||||
|
val isScreenActive = viewModel.isActive.collectAsState().value
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
LaunchedEffect(state.isScreenActive) {
|
LaunchedEffect(isScreenActive) {
|
||||||
if (!state.isScreenActive) {
|
if (!isScreenActive) {
|
||||||
finishAction()
|
finishAction()
|
||||||
}
|
}
|
||||||
if (context.isServiceRunning(CSCService::class.java.name)) {
|
if (context.isServiceRunning(CSCService::class.java.name)) {
|
||||||
@@ -45,7 +45,9 @@ fun CscScreen(finishAction: () -> Unit) {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun CSCView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
private fun CSCView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
||||||
Column {
|
Column {
|
||||||
TopAppBar(title = { Text(text = stringResource(id = R.string.csc_title)) })
|
BackIconAppBar(stringResource(id = R.string.csc_title)) {
|
||||||
|
onEvent(OnDisconnectButtonClick)
|
||||||
|
}
|
||||||
|
|
||||||
CSCContentView(state) { onEvent(it) }
|
CSCContentView(state) { onEvent(it) }
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package no.nordicsemi.android.csc.view
|
package no.nordicsemi.android.csc.view
|
||||||
|
|
||||||
object CSCSettings {
|
internal object CSCSettings {
|
||||||
|
|
||||||
object DefaultWheelSize {
|
object DefaultWheelSize {
|
||||||
const val NAME = "60-622"
|
const val NAME = "60-622"
|
||||||
@@ -3,7 +3,6 @@ package no.nordicsemi.android.csc.view
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -13,12 +12,12 @@ import no.nordicsemi.android.csc.R
|
|||||||
import no.nordicsemi.android.csc.data.CSCData
|
import no.nordicsemi.android.csc.data.CSCData
|
||||||
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
|
||||||
import no.nordicsemi.android.theme.view.SensorRecordCard
|
import no.nordicsemi.android.theme.view.ScreenSection
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SensorsReadingView(state: CSCData) {
|
internal fun SensorsReadingView(state: CSCData) {
|
||||||
SensorRecordCard {
|
ScreenSection {
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
Column {
|
||||||
KeyValueField(stringResource(id = R.string.scs_field_speed), state.displaySpeed())
|
KeyValueField(stringResource(id = R.string.scs_field_speed), state.displaySpeed())
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
KeyValueField(stringResource(id = R.string.scs_field_cadence), state.displayCadence())
|
KeyValueField(stringResource(id = R.string.scs_field_cadence), state.displayCadence())
|
||||||
@@ -18,7 +18,7 @@ import no.nordicsemi.android.csc.data.CSCData
|
|||||||
internal fun WheelSizeView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
internal fun WheelSizeView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
value = state.wheelSize,
|
value = state.wheelSizeDisplay,
|
||||||
onValueChange = { },
|
onValueChange = { },
|
||||||
enabled = false,
|
enabled = false,
|
||||||
label = { Text(text = stringResource(id = R.string.scs_field_wheel_size)) },
|
label = { Text(text = stringResource(id = R.string.scs_field_wheel_size)) },
|
||||||
@@ -1,35 +1,22 @@
|
|||||||
package no.nordicsemi.android.csc.viewmodel
|
package no.nordicsemi.android.csc.viewmodel
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import no.nordicsemi.android.csc.data.CSCDataHolder
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
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.CSCViewEvent
|
||||||
import no.nordicsemi.android.csc.view.OnDisconnectButtonClick
|
import no.nordicsemi.android.csc.view.OnDisconnectButtonClick
|
||||||
import no.nordicsemi.android.csc.view.OnSelectedSpeedUnitSelected
|
import no.nordicsemi.android.csc.view.OnSelectedSpeedUnitSelected
|
||||||
import no.nordicsemi.android.csc.view.OnShowEditWheelSizeDialogButtonClick
|
import no.nordicsemi.android.csc.view.OnShowEditWheelSizeDialogButtonClick
|
||||||
import no.nordicsemi.android.csc.view.OnWheelSizeSelected
|
import no.nordicsemi.android.csc.view.OnWheelSizeSelected
|
||||||
|
import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
|
||||||
import no.nordicsemi.android.utils.exhaustive
|
import no.nordicsemi.android.utils.exhaustive
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
internal class CscViewModel @Inject constructor(
|
internal class CSCViewModel @Inject constructor(
|
||||||
private val localBroadcast: CSCDataReadBroadcast
|
private val dataHolder: CSCDataHolder
|
||||||
) : ViewModel() {
|
) : CloseableViewModel() {
|
||||||
|
|
||||||
val state = MutableStateFlow(CSCData())
|
val state = dataHolder.data
|
||||||
|
|
||||||
init {
|
|
||||||
localBroadcast.events.onEach {
|
|
||||||
withContext(Dispatchers.Main) { state.value = it }
|
|
||||||
}.launchIn(viewModelScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onEvent(event: CSCViewEvent) {
|
fun onEvent(event: CSCViewEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
@@ -41,22 +28,19 @@ internal class CscViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onSelectedSpeedUnit(event: OnSelectedSpeedUnitSelected) {
|
private fun onSelectedSpeedUnit(event: OnSelectedSpeedUnitSelected) {
|
||||||
state.tryEmit(state.value.copy(selectedSpeedUnit = event.selectedSpeedUnit))
|
dataHolder.setSpeedUnit(event.selectedSpeedUnit)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onShowDialogEvent() {
|
private fun onShowDialogEvent() {
|
||||||
state.tryEmit(state.value.copy(showDialog = true))
|
dataHolder.setDisplayWheelSizeDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onWheelSizeChanged(event: OnWheelSizeSelected) {
|
private fun onWheelSizeChanged(event: OnWheelSizeSelected) {
|
||||||
localBroadcast.setWheelSize(event.wheelSize)
|
dataHolder.setWheelSize(event.wheelSize, event.wheelSizeDisplayInfo)
|
||||||
state.tryEmit(state.value.copy(
|
|
||||||
showDialog = false,
|
|
||||||
wheelSize = event.wheelSizeDisplayInfo
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onDisconnectButtonClick() {
|
private fun onDisconnectButtonClick() {
|
||||||
state.tryEmit(state.value.copy(isScreenActive = false))
|
finish()
|
||||||
|
dataHolder.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
4
profile_gls/src/main/AndroidManifest.xml
Normal file
4
profile_gls/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="no.nordicsemi.android.gls">
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package no.nordicsemi.android.gls.data
|
||||||
|
|
||||||
|
import no.nordicsemi.android.theme.view.RadioGroupItem
|
||||||
|
|
||||||
|
internal data class GLSData(
|
||||||
|
val records: List<GLSRecord> = emptyList(),
|
||||||
|
val batteryLevel: Int = 0,
|
||||||
|
val requestStatus: RequestStatus = RequestStatus.IDLE,
|
||||||
|
val isDeviceBonded: Boolean = false,
|
||||||
|
val selectedMode: WorkingMode = WorkingMode.ALL
|
||||||
|
) {
|
||||||
|
fun modeItems(): List<RadioGroupItem<WorkingMode>> {
|
||||||
|
return listOf(
|
||||||
|
RadioGroupItem(WorkingMode.ALL, "All"),
|
||||||
|
RadioGroupItem(WorkingMode.FIRST, "First"),
|
||||||
|
RadioGroupItem(WorkingMode.LAST, "Last")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum class WorkingMode {
|
||||||
|
ALL, LAST, FIRST
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum class RequestStatus {
|
||||||
|
IDLE, PENDING, SUCCESS, ABORTED, FAILED, NOT_SUPPORTED
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package no.nordicsemi.android.gls.data
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
internal class GLSDataHolder @Inject constructor() {
|
||||||
|
|
||||||
|
private val _data = MutableStateFlow(GLSData())
|
||||||
|
val data: StateFlow<GLSData> = _data
|
||||||
|
|
||||||
|
fun addNewRecord(record: GLSRecord) {
|
||||||
|
val newRecords = _data.value.records.toMutableList().apply {
|
||||||
|
add(record)
|
||||||
|
}
|
||||||
|
_data.tryEmit(_data.value.copy(records = newRecords))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addNewContext(context: MeasurementContext) {
|
||||||
|
_data.value.records.find { context.sequenceNumber == it.sequenceNumber }?.let {
|
||||||
|
it.context = context
|
||||||
|
}
|
||||||
|
_data.tryEmit(_data.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setRequestStatus(requestStatus: RequestStatus) {
|
||||||
|
_data.tryEmit(_data.value.copy(requestStatus = requestStatus))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun records() = _data.value.records
|
||||||
|
|
||||||
|
fun clearRecords() {
|
||||||
|
_data.tryEmit(_data.value.copy(records = emptyList()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setNewWorkingMode(workingMode: WorkingMode) {
|
||||||
|
_data.tryEmit(_data.value.copy(selectedMode = workingMode))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setNewBatteryLevel(batteryLevel: Int) {
|
||||||
|
_data.tryEmit(_data.value.copy(batteryLevel = batteryLevel))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
_data.tryEmit(GLSData())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@ internal data class GLSRecord(
|
|||||||
/** The glucose concentration. 0 if not present */
|
/** The glucose concentration. 0 if not present */
|
||||||
val glucoseConcentration: Float = 0f,
|
val glucoseConcentration: Float = 0f,
|
||||||
|
|
||||||
/** Concentration unit. One of the following: [GLSRecord.UNIT_kgpl], [GLSRecord.UNIT_molpl] */
|
/** Concentration unit. One of the following: [ConcentrationUnit.UNIT_KGPL], [ConcentrationUnit.UNIT_MOLPL] */
|
||||||
val unit: ConcentrationUnit = ConcentrationUnit.UNIT_KGPL,
|
val unit: ConcentrationUnit = ConcentrationUnit.UNIT_KGPL,
|
||||||
|
|
||||||
/** The type of the record. 0 if not present */
|
/** The type of the record. 0 if not present */
|
||||||
@@ -49,6 +49,8 @@ internal data class GLSRecord(
|
|||||||
)
|
)
|
||||||
|
|
||||||
internal data class MeasurementContext(
|
internal data class MeasurementContext(
|
||||||
|
/** Record sequence number */
|
||||||
|
val sequenceNumber: Int = 0,
|
||||||
|
|
||||||
val carbohydrateId: CarbohydrateId = CarbohydrateId.NOT_PRESENT,
|
val carbohydrateId: CarbohydrateId = CarbohydrateId.NOT_PRESENT,
|
||||||
|
|
||||||
@@ -28,7 +28,6 @@ import android.bluetooth.BluetoothGattCharacteristic
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import 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.RecordAccessControlPointDataCallback
|
||||||
import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementContextDataCallback
|
import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementContextDataCallback
|
||||||
import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementDataCallback
|
import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementDataCallback
|
||||||
@@ -44,7 +43,7 @@ import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContex
|
|||||||
import no.nordicsemi.android.ble.data.Data
|
import no.nordicsemi.android.ble.data.Data
|
||||||
import no.nordicsemi.android.gls.data.CarbohydrateId
|
import no.nordicsemi.android.gls.data.CarbohydrateId
|
||||||
import no.nordicsemi.android.gls.data.ConcentrationUnit
|
import no.nordicsemi.android.gls.data.ConcentrationUnit
|
||||||
import no.nordicsemi.android.gls.data.GLSData
|
import no.nordicsemi.android.gls.data.GLSDataHolder
|
||||||
import no.nordicsemi.android.gls.data.GLSRecord
|
import no.nordicsemi.android.gls.data.GLSRecord
|
||||||
import no.nordicsemi.android.gls.data.HealthStatus
|
import no.nordicsemi.android.gls.data.HealthStatus
|
||||||
import no.nordicsemi.android.gls.data.MeasurementContext
|
import no.nordicsemi.android.gls.data.MeasurementContext
|
||||||
@@ -55,7 +54,6 @@ import no.nordicsemi.android.gls.data.TestType
|
|||||||
import no.nordicsemi.android.gls.data.TypeOfMeal
|
import no.nordicsemi.android.gls.data.TypeOfMeal
|
||||||
import no.nordicsemi.android.log.LogContract
|
import no.nordicsemi.android.log.LogContract
|
||||||
import no.nordicsemi.android.service.BatteryManager
|
import no.nordicsemi.android.service.BatteryManager
|
||||||
import no.nordicsemi.android.service.BatteryManagerCallbacks
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
@@ -78,16 +76,18 @@ private val RACP_CHARACTERISTIC = UUID.fromString("00002A52-0000-1000-8000-00805
|
|||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
internal class GLSManager @Inject constructor(
|
internal class GLSManager @Inject constructor(
|
||||||
@ApplicationContext context: Context
|
@ApplicationContext context: Context,
|
||||||
) : BatteryManager<BatteryManagerCallbacks?>(context) {
|
private val dataHolder: GLSDataHolder
|
||||||
|
) : BatteryManager(context) {
|
||||||
val data = MutableStateFlow(GLSData())
|
|
||||||
private val records = hashMapOf<Int, GLSRecord>()
|
|
||||||
|
|
||||||
private var glucoseMeasurementCharacteristic: BluetoothGattCharacteristic? = null
|
private var glucoseMeasurementCharacteristic: BluetoothGattCharacteristic? = null
|
||||||
private var glucoseMeasurementContextCharacteristic: BluetoothGattCharacteristic? = null
|
private var glucoseMeasurementContextCharacteristic: BluetoothGattCharacteristic? = null
|
||||||
private var recordAccessControlPointCharacteristic: BluetoothGattCharacteristic? = null
|
private var recordAccessControlPointCharacteristic: BluetoothGattCharacteristic? = null
|
||||||
|
|
||||||
|
override fun onBatteryLevelChanged(batteryLevel: Int) {
|
||||||
|
dataHolder.setNewBatteryLevel(batteryLevel)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getGattCallback(): BatteryManagerGattCallback {
|
override fun getGattCallback(): BatteryManagerGattCallback {
|
||||||
return GlucoseManagerGattCallback()
|
return GlucoseManagerGattCallback()
|
||||||
}
|
}
|
||||||
@@ -121,10 +121,14 @@ internal class GLSManager @Inject constructor(
|
|||||||
.with(object : GlucoseMeasurementDataCallback() {
|
.with(object : GlucoseMeasurementDataCallback() {
|
||||||
|
|
||||||
override fun onGlucoseMeasurementReceived(
|
override fun onGlucoseMeasurementReceived(
|
||||||
device: BluetoothDevice, sequenceNumber: Int,
|
device: BluetoothDevice,
|
||||||
time: Calendar, glucoseConcentration: Float?,
|
sequenceNumber: Int,
|
||||||
unit: Int?, type: Int?,
|
time: Calendar,
|
||||||
sampleLocation: Int?, status: GlucoseStatus?,
|
glucoseConcentration: Float?,
|
||||||
|
unit: Int?,
|
||||||
|
type: Int?,
|
||||||
|
sampleLocation: Int?,
|
||||||
|
status: GlucoseStatus?,
|
||||||
contextInformationFollows: Boolean
|
contextInformationFollows: Boolean
|
||||||
) {
|
) {
|
||||||
val record = GLSRecord(
|
val record = GLSRecord(
|
||||||
@@ -138,27 +142,29 @@ internal class GLSManager @Inject constructor(
|
|||||||
status = status?.value ?: 0
|
status = status?.value ?: 0
|
||||||
)
|
)
|
||||||
|
|
||||||
records[record.sequenceNumber] = record
|
dataHolder.addNewRecord(record)
|
||||||
if (!contextInformationFollows) {
|
|
||||||
data.tryEmit(data.value.copy(record = records.values.toList()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
setNotificationCallback(glucoseMeasurementContextCharacteristic)
|
setNotificationCallback(glucoseMeasurementContextCharacteristic)
|
||||||
.with(object : GlucoseMeasurementContextDataCallback() {
|
.with(object : GlucoseMeasurementContextDataCallback() {
|
||||||
|
|
||||||
override fun onGlucoseMeasurementContextReceived(
|
override fun onGlucoseMeasurementContextReceived(
|
||||||
device: BluetoothDevice, sequenceNumber: Int,
|
device: BluetoothDevice,
|
||||||
carbohydrate: Carbohydrate?, carbohydrateAmount: Float?,
|
sequenceNumber: Int,
|
||||||
meal: Meal?, tester: Tester?,
|
carbohydrate: Carbohydrate?,
|
||||||
health: Health?, exerciseDuration: Int?,
|
carbohydrateAmount: Float?,
|
||||||
exerciseIntensity: Int?, medication: Medication?,
|
meal: Meal?,
|
||||||
medicationAmount: Float?, medicationUnit: Int?,
|
tester: Tester?,
|
||||||
|
health: Health?,
|
||||||
|
exerciseDuration: Int?,
|
||||||
|
exerciseIntensity: Int?,
|
||||||
|
medication: Medication?,
|
||||||
|
medicationAmount: Float?,
|
||||||
|
medicationUnit: Int?,
|
||||||
HbA1c: Float?
|
HbA1c: Float?
|
||||||
) {
|
) {
|
||||||
val record = records[sequenceNumber] ?: return
|
|
||||||
|
|
||||||
val context = MeasurementContext(
|
val context = MeasurementContext(
|
||||||
|
sequenceNumber = sequenceNumber,
|
||||||
carbohydrateId = carbohydrate?.value?.let { CarbohydrateId.create(it) }
|
carbohydrateId = carbohydrate?.value?.let { CarbohydrateId.create(it) }
|
||||||
?: CarbohydrateId.NOT_PRESENT,
|
?: CarbohydrateId.NOT_PRESENT,
|
||||||
carbohydrateUnits = carbohydrateAmount ?: 0f,
|
carbohydrateUnits = carbohydrateAmount ?: 0f,
|
||||||
@@ -177,9 +183,8 @@ internal class GLSManager @Inject constructor(
|
|||||||
?: MedicationUnit.UNIT_KG,
|
?: MedicationUnit.UNIT_KG,
|
||||||
HbA1c = HbA1c ?: 0f
|
HbA1c = HbA1c ?: 0f
|
||||||
)
|
)
|
||||||
record.context = context
|
|
||||||
|
|
||||||
data.tryEmit(data.value)
|
dataHolder.addNewContext(context)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
setIndicationCallback(recordAccessControlPointCharacteristic)
|
setIndicationCallback(recordAccessControlPointCharacteristic)
|
||||||
@@ -194,14 +199,14 @@ internal class GLSManager @Inject constructor(
|
|||||||
RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED
|
RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED
|
||||||
else -> RequestStatus.SUCCESS
|
else -> RequestStatus.SUCCESS
|
||||||
}
|
}
|
||||||
data.tryEmit(data.value.copy(requestStatus = status))
|
dataHolder.setRequestStatus(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRecordAccessOperationCompletedWithNoRecordsFound(
|
override fun onRecordAccessOperationCompletedWithNoRecordsFound(
|
||||||
device: BluetoothDevice,
|
device: BluetoothDevice,
|
||||||
@RACPOpCode requestCode: Int
|
@RACPOpCode requestCode: Int
|
||||||
) {
|
) {
|
||||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.SUCCESS))
|
dataHolder.setRequestStatus(RequestStatus.SUCCESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNumberOfRecordsReceived(
|
override fun onNumberOfRecordsReceived(
|
||||||
@@ -211,8 +216,8 @@ internal class GLSManager @Inject constructor(
|
|||||||
//TODO("Probably not needed")
|
//TODO("Probably not needed")
|
||||||
// mCallbacks!!.onNumberOfRecordsRequested(device, numberOfRecords)
|
// mCallbacks!!.onNumberOfRecordsRequested(device, numberOfRecords)
|
||||||
if (numberOfRecords > 0) {
|
if (numberOfRecords > 0) {
|
||||||
if (records.size > 0) {
|
if (dataHolder.records().isNotEmpty()) {
|
||||||
val sequenceNumber = records.keys.last() + 1
|
val sequenceNumber = dataHolder.records().last().sequenceNumber + 1 //TODO check if correct
|
||||||
writeCharacteristic(
|
writeCharacteristic(
|
||||||
recordAccessControlPointCharacteristic,
|
recordAccessControlPointCharacteristic,
|
||||||
RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(
|
RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(
|
||||||
@@ -228,7 +233,7 @@ internal class GLSManager @Inject constructor(
|
|||||||
.enqueue()
|
.enqueue()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.SUCCESS))
|
dataHolder.setRequestStatus(RequestStatus.SUCCESS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,9 +244,9 @@ internal class GLSManager @Inject constructor(
|
|||||||
) {
|
) {
|
||||||
log(Log.WARN, "Record Access operation failed (error $errorCode)")
|
log(Log.WARN, "Record Access operation failed (error $errorCode)")
|
||||||
if (errorCode == RACP_ERROR_OP_CODE_NOT_SUPPORTED) {
|
if (errorCode == RACP_ERROR_OP_CODE_NOT_SUPPORTED) {
|
||||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.NOT_SUPPORTED))
|
dataHolder.setRequestStatus(RequestStatus.NOT_SUPPORTED)
|
||||||
} else {
|
} else {
|
||||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.FAILED))
|
dataHolder.setRequestStatus(RequestStatus.FAILED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -271,9 +276,7 @@ internal class GLSManager @Inject constructor(
|
|||||||
return glucoseMeasurementCharacteristic != null && recordAccessControlPointCharacteristic != null
|
return glucoseMeasurementCharacteristic != null && recordAccessControlPointCharacteristic != null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onServicesInvalidated() {
|
override fun onServicesInvalidated() { }
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isOptionalServiceSupported(gatt: BluetoothGatt): Boolean {
|
override fun isOptionalServiceSupported(gatt: BluetoothGatt): Boolean {
|
||||||
super.isOptionalServiceSupported(gatt)
|
super.isOptionalServiceSupported(gatt)
|
||||||
@@ -290,11 +293,11 @@ internal class GLSManager @Inject constructor(
|
|||||||
/**
|
/**
|
||||||
* Clears the records list locally.
|
* Clears the records list locally.
|
||||||
*/
|
*/
|
||||||
fun clear() {
|
private fun clear() {
|
||||||
records.clear()
|
dataHolder.clearRecords()
|
||||||
val target = bluetoothDevice
|
val target = bluetoothDevice
|
||||||
if (target != null) {
|
if (target != null) {
|
||||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.SUCCESS))
|
dataHolder.setRequestStatus(RequestStatus.SUCCESS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,11 +306,11 @@ internal class GLSManager @Inject constructor(
|
|||||||
* be returned to Glucose Measurement characteristic as a notification followed by Record Access
|
* 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.
|
* Control Point indication with status code Success or other in case of error.
|
||||||
*/
|
*/
|
||||||
fun lastRecord(): Unit {
|
fun requestLastRecord() {
|
||||||
if (recordAccessControlPointCharacteristic == null) return
|
if (recordAccessControlPointCharacteristic == null) return
|
||||||
val target = bluetoothDevice ?: return
|
val target = bluetoothDevice ?: return
|
||||||
clear()
|
clear()
|
||||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
|
dataHolder.setRequestStatus(RequestStatus.PENDING)
|
||||||
writeCharacteristic(
|
writeCharacteristic(
|
||||||
recordAccessControlPointCharacteristic,
|
recordAccessControlPointCharacteristic,
|
||||||
RecordAccessControlPointData.reportLastStoredRecord()
|
RecordAccessControlPointData.reportLastStoredRecord()
|
||||||
@@ -326,11 +329,11 @@ internal class GLSManager @Inject constructor(
|
|||||||
* returned to Glucose Measurement characteristic as a notification followed by Record Access
|
* 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.
|
* Control Point indication with status code Success or other in case of error.
|
||||||
*/
|
*/
|
||||||
fun requestFirstRecord(): Unit {
|
fun requestFirstRecord() {
|
||||||
if (recordAccessControlPointCharacteristic == null) return
|
if (recordAccessControlPointCharacteristic == null) return
|
||||||
val target = bluetoothDevice ?: return
|
val target = bluetoothDevice ?: return
|
||||||
clear()
|
clear()
|
||||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
|
dataHolder.setRequestStatus(RequestStatus.PENDING)
|
||||||
writeCharacteristic(
|
writeCharacteristic(
|
||||||
recordAccessControlPointCharacteristic,
|
recordAccessControlPointCharacteristic,
|
||||||
RecordAccessControlPointData.reportFirstStoredRecord()
|
RecordAccessControlPointData.reportFirstStoredRecord()
|
||||||
@@ -350,11 +353,11 @@ internal class GLSManager @Inject constructor(
|
|||||||
* will be returned to Glucose Measurement characteristic as a notification followed by
|
* 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.
|
* Record Access Control Point indication with status code Success or other in case of error.
|
||||||
*/
|
*/
|
||||||
fun requestAllRecords(): Unit {
|
fun requestAllRecords() {
|
||||||
if (recordAccessControlPointCharacteristic == null) return
|
if (recordAccessControlPointCharacteristic == null) return
|
||||||
val target = bluetoothDevice ?: return
|
val target = bluetoothDevice ?: return
|
||||||
clear()
|
clear()
|
||||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
|
dataHolder.setRequestStatus(RequestStatus.PENDING)
|
||||||
writeCharacteristic(
|
writeCharacteristic(
|
||||||
recordAccessControlPointCharacteristic,
|
recordAccessControlPointCharacteristic,
|
||||||
RecordAccessControlPointData.reportNumberOfAllStoredRecords()
|
RecordAccessControlPointData.reportNumberOfAllStoredRecords()
|
||||||
@@ -382,13 +385,13 @@ internal class GLSManager @Inject constructor(
|
|||||||
fun refreshRecords() {
|
fun refreshRecords() {
|
||||||
if (recordAccessControlPointCharacteristic == null) return
|
if (recordAccessControlPointCharacteristic == null) return
|
||||||
val target = bluetoothDevice ?: return
|
val target = bluetoothDevice ?: return
|
||||||
if (records.size == 0) {
|
if (dataHolder.records().isEmpty()) {
|
||||||
requestAllRecords()
|
requestAllRecords()
|
||||||
} else {
|
} else {
|
||||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
|
dataHolder.setRequestStatus(RequestStatus.PENDING)
|
||||||
|
|
||||||
// obtain the last sequence number
|
// obtain the last sequence number
|
||||||
val sequenceNumber = records.keys.last() + 1
|
val sequenceNumber = dataHolder.records().last().sequenceNumber + 1 //TODO check if correct
|
||||||
writeCharacteristic(
|
writeCharacteristic(
|
||||||
recordAccessControlPointCharacteristic,
|
recordAccessControlPointCharacteristic,
|
||||||
RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber)
|
RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber)
|
||||||
@@ -432,7 +435,7 @@ internal class GLSManager @Inject constructor(
|
|||||||
if (recordAccessControlPointCharacteristic == null) return
|
if (recordAccessControlPointCharacteristic == null) return
|
||||||
val target = bluetoothDevice ?: return
|
val target = bluetoothDevice ?: return
|
||||||
clear()
|
clear()
|
||||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
|
dataHolder.setRequestStatus(RequestStatus.PENDING)
|
||||||
writeCharacteristic(
|
writeCharacteristic(
|
||||||
recordAccessControlPointCharacteristic,
|
recordAccessControlPointCharacteristic,
|
||||||
RecordAccessControlPointData.deleteAllStoredRecords()
|
RecordAccessControlPointData.deleteAllStoredRecords()
|
||||||
@@ -445,7 +448,7 @@ internal class GLSManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
.enqueue()
|
.enqueue()
|
||||||
|
|
||||||
val elements = listOf<Int>(1, 2, 3)
|
val elements = listOf(1, 2, 3)
|
||||||
val result = elements.all { it > 3 }
|
val result = elements.all { it > 3 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,8 +3,8 @@ package no.nordicsemi.android.gls.view
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material.Button
|
import androidx.compose.material.Button
|
||||||
import androidx.compose.material.ButtonDefaults
|
import androidx.compose.material.ButtonDefaults
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
@@ -18,18 +18,27 @@ import no.nordicsemi.android.gls.R
|
|||||||
import no.nordicsemi.android.gls.data.GLSData
|
import no.nordicsemi.android.gls.data.GLSData
|
||||||
import no.nordicsemi.android.gls.viewmodel.DisconnectEvent
|
import no.nordicsemi.android.gls.viewmodel.DisconnectEvent
|
||||||
import no.nordicsemi.android.gls.viewmodel.GLSScreenViewEvent
|
import no.nordicsemi.android.gls.viewmodel.GLSScreenViewEvent
|
||||||
|
import no.nordicsemi.android.gls.viewmodel.OnWorkingModeSelected
|
||||||
import no.nordicsemi.android.theme.view.BatteryLevelView
|
import no.nordicsemi.android.theme.view.BatteryLevelView
|
||||||
|
import no.nordicsemi.android.theme.view.ScreenSection
|
||||||
|
import no.nordicsemi.android.theme.view.SpeedUnitRadioGroup
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun GLSContentView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) {
|
internal fun GLSContentView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize(),
|
||||||
.fillMaxSize()
|
|
||||||
.padding(16.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
SettingsView(state, onEvent)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
RecordsView(state)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
BatteryLevelView(state.batteryLevel)
|
BatteryLevelView(state.batteryLevel)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
@@ -43,3 +52,22 @@ internal fun GLSContentView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Uni
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SettingsView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) {
|
||||||
|
ScreenSection {
|
||||||
|
SpeedUnitRadioGroup(state.selectedMode, state.modeItems()) {
|
||||||
|
onEvent(OnWorkingModeSelected(it.unit))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RecordsView(state: GLSData) {
|
||||||
|
ScreenSection {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
state.records.forEach {
|
||||||
|
Text(text = String.format("Glocose concentration: ", it.glucoseConcentration))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
package no.nordicsemi.android.gls.view
|
package no.nordicsemi.android.gls.view
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
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.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -10,23 +8,38 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import no.nordicsemi.android.gls.R
|
import no.nordicsemi.android.gls.R
|
||||||
import no.nordicsemi.android.gls.data.GLSData
|
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.gls.viewmodel.GLSScreenViewEvent
|
||||||
import no.nordicsemi.android.gls.viewmodel.GLSViewModel
|
import no.nordicsemi.android.gls.viewmodel.GLSViewModel
|
||||||
|
import no.nordicsemi.android.theme.view.BackIconAppBar
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GLSScreen(finishAction: () -> Unit) {
|
fun GLSScreen(finishAction: () -> Unit) {
|
||||||
val viewModel: GLSViewModel = hiltViewModel()
|
val viewModel: GLSViewModel = hiltViewModel()
|
||||||
val state = viewModel.state.collectAsState().value
|
val state = viewModel.state.collectAsState().value
|
||||||
|
val isScreenActive = viewModel.isActive.collectAsState().value
|
||||||
|
|
||||||
LaunchedEffect(state.isDeviceBonded) {
|
LaunchedEffect(state.isDeviceBonded) {
|
||||||
// viewModel.bondDevice()
|
viewModel.bondDevice()
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(isScreenActive) {
|
||||||
|
if (!isScreenActive) {
|
||||||
|
finishAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GLSView(state) {
|
||||||
|
viewModel.onEvent(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun GLSView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) {
|
private fun GLSView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) {
|
||||||
Column {
|
Column {
|
||||||
TopAppBar(title = { Text(text = stringResource(id = R.string.gls_title)) })
|
BackIconAppBar(stringResource(id = R.string.gls_title)) {
|
||||||
|
onEvent(DisconnectEvent)
|
||||||
|
}
|
||||||
|
|
||||||
GLSContentView(state, onEvent)
|
GLSContentView(state, onEvent)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package no.nordicsemi.android.gls.viewmodel
|
||||||
|
|
||||||
|
import no.nordicsemi.android.gls.data.WorkingMode
|
||||||
|
|
||||||
|
internal sealed class GLSScreenViewEvent
|
||||||
|
|
||||||
|
internal data class OnWorkingModeSelected(val workingMode: WorkingMode) : GLSScreenViewEvent()
|
||||||
|
|
||||||
|
internal object DisconnectEvent : GLSScreenViewEvent()
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package no.nordicsemi.android.gls.viewmodel
|
||||||
|
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import no.nordicsemi.android.gls.data.GLSDataHolder
|
||||||
|
import no.nordicsemi.android.gls.data.WorkingMode
|
||||||
|
import no.nordicsemi.android.gls.repository.GLSManager
|
||||||
|
import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
|
||||||
|
import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
|
||||||
|
import no.nordicsemi.android.utils.exhaustive
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
internal class GLSViewModel @Inject constructor(
|
||||||
|
private val glsManager: GLSManager,
|
||||||
|
private val deviceHolder: SelectedBluetoothDeviceHolder,
|
||||||
|
private val dataHolder: GLSDataHolder
|
||||||
|
) : CloseableViewModel() {
|
||||||
|
|
||||||
|
val state = dataHolder.data
|
||||||
|
private var lastSelectedMode = state.value.selectedMode
|
||||||
|
|
||||||
|
init {
|
||||||
|
dataHolder.data.onEach {
|
||||||
|
if (lastSelectedMode == it.selectedMode) {
|
||||||
|
return@onEach
|
||||||
|
}
|
||||||
|
lastSelectedMode = it.selectedMode
|
||||||
|
when (it.selectedMode) {
|
||||||
|
WorkingMode.ALL -> glsManager.requestAllRecords()
|
||||||
|
WorkingMode.LAST -> glsManager.requestLastRecord()
|
||||||
|
WorkingMode.FIRST -> glsManager.requestFirstRecord()
|
||||||
|
}.exhaustive
|
||||||
|
}.launchIn(GlobalScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEvent(event: GLSScreenViewEvent) {
|
||||||
|
when (event) {
|
||||||
|
DisconnectEvent -> disconnect()
|
||||||
|
is OnWorkingModeSelected -> dataHolder.setNewWorkingMode(event.workingMode)
|
||||||
|
}.exhaustive
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bondDevice() {
|
||||||
|
if (deviceHolder.isBondingRequired()) {
|
||||||
|
deviceHolder.bondDevice()
|
||||||
|
} else {
|
||||||
|
connectDevice()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun connectDevice() {
|
||||||
|
deviceHolder.device?.let {
|
||||||
|
glsManager.connect(it)
|
||||||
|
.useAutoConnect(false)
|
||||||
|
.retry(3, 100)
|
||||||
|
.enqueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun disconnect() {
|
||||||
|
finish()
|
||||||
|
deviceHolder.forgetDevice()
|
||||||
|
dataHolder.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,5 +3,5 @@ package no.nordicsemi.android.hrs.data
|
|||||||
internal data class HRSData(
|
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,
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package no.nordicsemi.android.hrs.data
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
internal class HRSDataHolder @Inject constructor() {
|
||||||
|
|
||||||
|
private val _data = MutableStateFlow(HRSData())
|
||||||
|
val data: StateFlow<HRSData> = _data
|
||||||
|
|
||||||
|
fun addNewHeartRate(heartRate: Int) {
|
||||||
|
val result = _data.value.heartRates.toMutableList().apply {
|
||||||
|
add(heartRate)
|
||||||
|
}
|
||||||
|
_data.tryEmit(_data.value.copy(heartRates = result))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSensorLocation(sensorLocation: Int) {
|
||||||
|
_data.tryEmit(_data.value.copy(sensorLocation = sensorLocation))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBatteryLevel(batteryLevel: Int) {
|
||||||
|
_data.tryEmit(_data.value.copy(batteryLevel = batteryLevel))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
_data.tryEmit(HRSData())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,8 +25,7 @@ import no.nordicsemi.android.ble.data.Data
|
|||||||
|
|
||||||
internal object BodySensorLocationParser {
|
internal object BodySensorLocationParser {
|
||||||
fun parse(data: Data): String {
|
fun parse(data: Data): String {
|
||||||
val value = data.getIntValue(Data.FORMAT_UINT8, 0)!!
|
return when (data.getIntValue(Data.FORMAT_UINT8, 0)!!) {
|
||||||
return when (value) {
|
|
||||||
6 -> "Foot"
|
6 -> "Foot"
|
||||||
5 -> "Ear Lobe"
|
5 -> "Ear Lobe"
|
||||||
4 -> "Hand"
|
4 -> "Hand"
|
||||||
@@ -31,21 +31,63 @@ import no.nordicsemi.android.ble.common.callback.hr.BodySensorLocationDataCallba
|
|||||||
import no.nordicsemi.android.ble.common.callback.hr.HeartRateMeasurementDataCallback
|
import no.nordicsemi.android.ble.common.callback.hr.HeartRateMeasurementDataCallback
|
||||||
import no.nordicsemi.android.ble.common.profile.hr.BodySensorLocation
|
import no.nordicsemi.android.ble.common.profile.hr.BodySensorLocation
|
||||||
import no.nordicsemi.android.ble.data.Data
|
import no.nordicsemi.android.ble.data.Data
|
||||||
|
import no.nordicsemi.android.hrs.data.HRSDataHolder
|
||||||
import no.nordicsemi.android.log.LogContract
|
import no.nordicsemi.android.log.LogContract
|
||||||
import no.nordicsemi.android.service.BatteryManager
|
import no.nordicsemi.android.service.BatteryManager
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
private val HR_SERVICE_UUID = UUID.fromString("0000180D-0000-1000-8000-00805f9b34fb")
|
||||||
|
private val BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID = UUID.fromString("00002A38-0000-1000-8000-00805f9b34fb")
|
||||||
|
private val HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A37-0000-1000-8000-00805f9b34fb")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HRSManager class performs BluetoothGatt operations for connection, service discovery,
|
* HRSManager class performs BluetoothGatt operations for connection, service discovery,
|
||||||
* enabling notification and reading characteristics.
|
* enabling notification and reading characteristics.
|
||||||
* All operations required to connect to device with BLE Heart Rate Service and reading
|
* All operations required to connect to device with BLE Heart Rate Service and reading
|
||||||
* heart rate values are performed here.
|
* heart rate values are performed here.
|
||||||
*/
|
*/
|
||||||
class HRSManager(context: Context) : BatteryManager<HRSManagerCallbacks>(context) {
|
internal class HRSManager(context: Context, private val dataHolder: HRSDataHolder) : BatteryManager(context) {
|
||||||
|
|
||||||
private var heartRateCharacteristic: BluetoothGattCharacteristic? = null
|
private var heartRateCharacteristic: BluetoothGattCharacteristic? = null
|
||||||
private var bodySensorLocationCharacteristic: BluetoothGattCharacteristic? = null
|
private var bodySensorLocationCharacteristic: BluetoothGattCharacteristic? = null
|
||||||
|
|
||||||
|
private val bodySensorLocationDataCallback = object : BodySensorLocationDataCallback() {
|
||||||
|
|
||||||
|
override fun onDataReceived(device: BluetoothDevice, data: Data) {
|
||||||
|
log(LogContract.Log.Level.APPLICATION, "\"" + BodySensorLocationParser.parse(data) + "\" received")
|
||||||
|
super.onDataReceived(device, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBodySensorLocationReceived(
|
||||||
|
device: BluetoothDevice,
|
||||||
|
@BodySensorLocation sensorLocation: Int
|
||||||
|
) {
|
||||||
|
dataHolder.setSensorLocation(sensorLocation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val heartRateMeasurementDataCallback = object : HeartRateMeasurementDataCallback() {
|
||||||
|
|
||||||
|
override fun onDataReceived(device: BluetoothDevice, data: Data) {
|
||||||
|
log(LogContract.Log.Level.APPLICATION, "\"" + HeartRateMeasurementParser.parse(data) + "\" received")
|
||||||
|
super.onDataReceived(device, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onHeartRateMeasurementReceived(
|
||||||
|
device: BluetoothDevice,
|
||||||
|
@IntRange(from = 0) heartRate: Int,
|
||||||
|
contactDetected: Boolean?,
|
||||||
|
@IntRange(from = 0) energyExpanded: Int?,
|
||||||
|
rrIntervals: List<Int>?
|
||||||
|
) {
|
||||||
|
dataHolder.addNewHeartRate(heartRate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBatteryLevelChanged(batteryLevel: Int) {
|
||||||
|
dataHolder.setBatteryLevel(batteryLevel)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getGattCallback(): BatteryManagerGattCallback {
|
override fun getGattCallback(): BatteryManagerGattCallback {
|
||||||
return HeartRateManagerCallback()
|
return HeartRateManagerCallback()
|
||||||
}
|
}
|
||||||
@@ -58,56 +100,14 @@ class HRSManager(context: Context) : BatteryManager<HRSManagerCallbacks>(context
|
|||||||
override fun initialize() {
|
override fun initialize() {
|
||||||
super.initialize()
|
super.initialize()
|
||||||
readCharacteristic(bodySensorLocationCharacteristic)
|
readCharacteristic(bodySensorLocationCharacteristic)
|
||||||
.with(object : BodySensorLocationDataCallback() {
|
.with(bodySensorLocationDataCallback)
|
||||||
|
|
||||||
override fun onDataReceived(device: BluetoothDevice, data: Data) {
|
|
||||||
log(
|
|
||||||
LogContract.Log.Level.APPLICATION,
|
|
||||||
"\"" + BodySensorLocationParser.parse(data) + "\" received"
|
|
||||||
)
|
|
||||||
super.onDataReceived(device, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBodySensorLocationReceived(
|
|
||||||
device: BluetoothDevice,
|
|
||||||
@BodySensorLocation sensorLocation: Int
|
|
||||||
) {
|
|
||||||
mCallbacks?.onBodySensorLocationReceived(device, sensorLocation)
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
.fail { device: BluetoothDevice?, status: Int ->
|
.fail { device: BluetoothDevice?, status: Int ->
|
||||||
log(Log.WARN, "Body Sensor Location characteristic not found")
|
log(Log.WARN, "Body Sensor Location characteristic not found")
|
||||||
}
|
}
|
||||||
.enqueue()
|
.enqueue()
|
||||||
|
|
||||||
setNotificationCallback(heartRateCharacteristic)
|
setNotificationCallback(heartRateCharacteristic)
|
||||||
.with(object : HeartRateMeasurementDataCallback() {
|
.with(heartRateMeasurementDataCallback)
|
||||||
|
|
||||||
override fun onDataReceived(device: BluetoothDevice, data: Data) {
|
|
||||||
log(
|
|
||||||
LogContract.Log.Level.APPLICATION,
|
|
||||||
"\"" + HeartRateMeasurementParser.parse(data) + "\" received"
|
|
||||||
)
|
|
||||||
super.onDataReceived(device, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onHeartRateMeasurementReceived(
|
|
||||||
device: BluetoothDevice,
|
|
||||||
@IntRange(from = 0) heartRate: Int,
|
|
||||||
contactDetected: Boolean?,
|
|
||||||
@IntRange(from = 0) energyExpanded: Int?,
|
|
||||||
rrIntervals: List<Int>?
|
|
||||||
) {
|
|
||||||
mCallbacks?.onHeartRateMeasurementReceived(
|
|
||||||
device,
|
|
||||||
heartRate,
|
|
||||||
contactDetected,
|
|
||||||
energyExpanded,
|
|
||||||
rrIntervals
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
enableNotifications(heartRateCharacteristic).enqueue()
|
enableNotifications(heartRateCharacteristic).enqueue()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,23 +140,4 @@ class HRSManager(context: Context) : BatteryManager<HRSManagerCallbacks>(context
|
|||||||
|
|
||||||
override fun onServicesInvalidated() {}
|
override fun onServicesInvalidated() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
val HR_SERVICE_UUID = UUID.fromString("0000180D-0000-1000-8000-00805f9b34fb")
|
|
||||||
private val BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID = UUID.fromString("00002A38-0000-1000-8000-00805f9b34fb")
|
|
||||||
private val HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A37-0000-1000-8000-00805f9b34fb")
|
|
||||||
private var managerInstance: HRSManager? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Singleton implementation of HRSManager class.
|
|
||||||
*/
|
|
||||||
@Synchronized
|
|
||||||
fun getInstance(context: Context): HRSManager? {
|
|
||||||
if (managerInstance == null) {
|
|
||||||
managerInstance = HRSManager(context)
|
|
||||||
}
|
|
||||||
return managerInstance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package no.nordicsemi.android.hrs.service
|
||||||
|
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import no.nordicsemi.android.hrs.data.HRSDataHolder
|
||||||
|
import no.nordicsemi.android.service.ForegroundBleService
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
internal class HRSService : ForegroundBleService() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var dataHolder: HRSDataHolder
|
||||||
|
|
||||||
|
override val manager: HRSManager by lazy { HRSManager(this, dataHolder) }
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
package no.nordicsemi.android.hts.view
|
package no.nordicsemi.android.hrs.view
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.Button
|
import androidx.compose.material.Button
|
||||||
@@ -16,22 +15,21 @@ 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 androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import no.nordicsemi.android.hts.R
|
import no.nordicsemi.android.hrs.R
|
||||||
import no.nordicsemi.android.hts.data.HTSData
|
import no.nordicsemi.android.hrs.data.HRSData
|
||||||
import no.nordicsemi.android.theme.view.BatteryLevelView
|
import no.nordicsemi.android.theme.view.BatteryLevelView
|
||||||
import no.nordicsemi.android.theme.view.SensorRecordCard
|
import no.nordicsemi.android.theme.view.ScreenSection
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun HTSContentView(state: HTSData, onEvent: (HTSScreenViewEvent) -> Unit) {
|
internal fun HRSContentView(state: HRSData, onEvent: (HRSScreenViewEvent) -> Unit) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(16.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
SensorRecordCard {
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Box(modifier = Modifier.padding(16.dp)) {
|
|
||||||
|
|
||||||
|
ScreenSection {
|
||||||
|
Box(modifier = Modifier.padding(16.dp)) {
|
||||||
|
LineChartView(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,5 +51,5 @@ internal fun HTSContentView(state: HTSData, onEvent: (HTSScreenViewEvent) -> Uni
|
|||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
private fun Preview() {
|
private fun Preview() {
|
||||||
HTSContentView(state = HTSData()) { }
|
HRSContentView(state = HRSData()) { }
|
||||||
}
|
}
|
||||||
@@ -2,8 +2,6 @@ package no.nordicsemi.android.hrs.view
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.compose.foundation.layout.Column
|
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.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -11,19 +9,21 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import no.nordicsemi.android.hrs.R
|
import no.nordicsemi.android.hrs.R
|
||||||
|
import no.nordicsemi.android.hrs.data.HRSData
|
||||||
import no.nordicsemi.android.hrs.service.HRSService
|
import no.nordicsemi.android.hrs.service.HRSService
|
||||||
import no.nordicsemi.android.hrs.viewmodel.HRSViewModel
|
import no.nordicsemi.android.hrs.viewmodel.HRSViewModel
|
||||||
import no.nordicsemi.android.hrs.viewmodel.HRSViewState
|
import no.nordicsemi.android.theme.view.BackIconAppBar
|
||||||
import no.nordicsemi.android.utils.isServiceRunning
|
import no.nordicsemi.android.utils.isServiceRunning
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HRSScreen(finishAction: () -> Unit) {
|
fun HRSScreen(finishAction: () -> Unit) {
|
||||||
val viewModel: HRSViewModel = hiltViewModel()
|
val viewModel: HRSViewModel = hiltViewModel()
|
||||||
val state = viewModel.state.collectAsState().value
|
val state = viewModel.state.collectAsState().value
|
||||||
|
val isActive = viewModel.isActive.collectAsState().value
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
LaunchedEffect(state.isScreenActive) {
|
LaunchedEffect(isActive) {
|
||||||
if (!state.isScreenActive) {
|
if (!isActive) {
|
||||||
finishAction()
|
finishAction()
|
||||||
}
|
}
|
||||||
if (context.isServiceRunning(HRSService::class.java.name)) {
|
if (context.isServiceRunning(HRSService::class.java.name)) {
|
||||||
@@ -43,9 +43,11 @@ fun HRSScreen(finishAction: () -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun HRSView(state: HRSViewState, onEvent: (HRSScreenViewEvent) -> Unit) {
|
private fun HRSView(state: HRSData, onEvent: (HRSScreenViewEvent) -> Unit) {
|
||||||
Column {
|
Column {
|
||||||
TopAppBar(title = { Text(text = stringResource(id = R.string.hrs_title)) })
|
BackIconAppBar(stringResource(id = R.string.hrs_title)) {
|
||||||
|
onEvent(DisconnectEvent)
|
||||||
|
}
|
||||||
|
|
||||||
HRSContentView(state) { onEvent(it) }
|
HRSContentView(state) { onEvent(it) }
|
||||||
}
|
}
|
||||||
@@ -3,101 +3,72 @@ package no.nordicsemi.android.hrs.view
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.DashPathEffect
|
import android.graphics.DashPathEffect
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.Button
|
|
||||||
import androidx.compose.material.ButtonDefaults
|
|
||||||
import androidx.compose.material.Card
|
|
||||||
import androidx.compose.material.MaterialTheme
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import com.github.mikephil.charting.charts.LineChart
|
import com.github.mikephil.charting.charts.LineChart
|
||||||
|
import com.github.mikephil.charting.components.XAxis
|
||||||
import com.github.mikephil.charting.data.Entry
|
import com.github.mikephil.charting.data.Entry
|
||||||
import com.github.mikephil.charting.data.LineData
|
import com.github.mikephil.charting.data.LineData
|
||||||
import com.github.mikephil.charting.data.LineDataSet
|
import com.github.mikephil.charting.data.LineDataSet
|
||||||
import com.github.mikephil.charting.formatter.IFillFormatter
|
|
||||||
import com.github.mikephil.charting.interfaces.datasets.ILineDataSet
|
import com.github.mikephil.charting.interfaces.datasets.ILineDataSet
|
||||||
import com.github.mikephil.charting.utils.Utils
|
import no.nordicsemi.android.hrs.data.HRSData
|
||||||
import no.nordicsemi.android.hrs.R
|
|
||||||
import no.nordicsemi.android.hrs.viewmodel.HRSViewState
|
|
||||||
import no.nordicsemi.android.theme.NordicColors
|
|
||||||
import no.nordicsemi.android.theme.view.BatteryLevelView
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@Composable
|
private const val X_AXIS_ELEMENTS_COUNT = 40f
|
||||||
internal fun HRSContentView(state: HRSViewState, onEvent: (HRSScreenViewEvent) -> Unit) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(16.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Card(
|
|
||||||
backgroundColor = NordicColors.NordicGray4.value(),
|
|
||||||
shape = RoundedCornerShape(10.dp),
|
|
||||||
elevation = 0.dp
|
|
||||||
) {
|
|
||||||
Box(modifier = Modifier.padding(16.dp)) {
|
|
||||||
LineChartView(state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun LineChartView(state: HRSViewState) {
|
internal fun LineChartView(state: HRSData) {
|
||||||
|
val items = state.heartRates.takeLast(X_AXIS_ELEMENTS_COUNT.toInt()).reversed()
|
||||||
|
val isSystemInDarkTheme = isSystemInDarkTheme()
|
||||||
AndroidView(
|
AndroidView(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(300.dp),
|
.height(300.dp),
|
||||||
factory = { createLineChartView(it, state) },
|
factory = { createLineChartView(isSystemInDarkTheme, it, items) },
|
||||||
update = { updateData(state.points, it) }
|
update = { updateData(items, it) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun createLineChartView(context: Context, state: HRSViewState): LineChart {
|
internal fun createLineChartView(isDarkTheme: Boolean, context: Context, points: List<Int>): LineChart {
|
||||||
return LineChart(context).apply {
|
return LineChart(context).apply {
|
||||||
setBackgroundColor(Color.WHITE)
|
|
||||||
|
|
||||||
description.isEnabled = false
|
description.isEnabled = false
|
||||||
|
|
||||||
setTouchEnabled(true)
|
legend.isEnabled = false
|
||||||
|
|
||||||
|
setTouchEnabled(false)
|
||||||
|
|
||||||
// setOnChartValueSelectedListener(this)
|
|
||||||
setDrawGridBackground(false)
|
setDrawGridBackground(false)
|
||||||
|
|
||||||
isDragEnabled = true
|
isDragEnabled = true
|
||||||
setScaleEnabled(true)
|
setScaleEnabled(false)
|
||||||
setPinchZoom(true)
|
setPinchZoom(false)
|
||||||
|
|
||||||
|
if (isDarkTheme) {
|
||||||
|
setBackgroundColor(Color.TRANSPARENT)
|
||||||
|
xAxis.gridColor = Color.WHITE
|
||||||
|
xAxis.textColor = Color.WHITE
|
||||||
|
axisLeft.gridColor = Color.WHITE
|
||||||
|
axisLeft.textColor = Color.WHITE
|
||||||
|
} else {
|
||||||
|
setBackgroundColor(Color.WHITE)
|
||||||
|
xAxis.gridColor = Color.BLACK
|
||||||
|
xAxis.textColor = Color.BLACK
|
||||||
|
axisLeft.gridColor = Color.BLACK
|
||||||
|
axisLeft.textColor = Color.BLACK
|
||||||
|
}
|
||||||
|
|
||||||
xAxis.apply {
|
xAxis.apply {
|
||||||
xAxis.enableGridDashedLine(10f, 10f, 0f)
|
xAxis.enableGridDashedLine(10f, 10f, 0f)
|
||||||
|
|
||||||
|
axisMinimum = -X_AXIS_ELEMENTS_COUNT
|
||||||
|
axisMaximum = 0f
|
||||||
|
setAvoidFirstLastClipping(true)
|
||||||
|
position = XAxis.XAxisPosition.BOTTOM
|
||||||
}
|
}
|
||||||
axisLeft.apply {
|
axisLeft.apply {
|
||||||
enableGridDashedLine(10f, 10f, 0f)
|
enableGridDashedLine(10f, 10f, 0f)
|
||||||
@@ -107,11 +78,9 @@ internal fun createLineChartView(context: Context, state: HRSViewState): LineCha
|
|||||||
}
|
}
|
||||||
axisRight.isEnabled = false
|
axisRight.isEnabled = false
|
||||||
|
|
||||||
//---
|
val entries = points.mapIndexed { i, v ->
|
||||||
|
Entry(-i.toFloat(), v.toFloat())
|
||||||
val entries = state.points.mapIndexed { i, v ->
|
}.reversed()
|
||||||
Entry(i.toFloat(), v.toFloat())
|
|
||||||
}
|
|
||||||
// create a dataset and give it a type
|
// create a dataset and give it a type
|
||||||
|
|
||||||
if (data != null && data.dataSetCount > 0) {
|
if (data != null && data.dataSetCount > 0) {
|
||||||
@@ -124,6 +93,7 @@ internal fun createLineChartView(context: Context, state: HRSViewState): LineCha
|
|||||||
val set1 = LineDataSet(entries, "DataSet 1")
|
val set1 = LineDataSet(entries, "DataSet 1")
|
||||||
|
|
||||||
set1.setDrawIcons(false)
|
set1.setDrawIcons(false)
|
||||||
|
set1.setDrawValues(false)
|
||||||
|
|
||||||
// draw dashed line
|
// draw dashed line
|
||||||
|
|
||||||
@@ -133,8 +103,13 @@ internal fun createLineChartView(context: Context, state: HRSViewState): LineCha
|
|||||||
// black lines and points
|
// black lines and points
|
||||||
|
|
||||||
// black lines and points
|
// black lines and points
|
||||||
|
if (isDarkTheme) {
|
||||||
|
set1.color = Color.WHITE
|
||||||
|
set1.setCircleColor(Color.WHITE)
|
||||||
|
} else {
|
||||||
set1.color = Color.BLACK
|
set1.color = Color.BLACK
|
||||||
set1.setCircleColor(Color.BLACK)
|
set1.setCircleColor(Color.BLACK)
|
||||||
|
}
|
||||||
|
|
||||||
// line thickness and point size
|
// line thickness and point size
|
||||||
|
|
||||||
@@ -164,31 +139,9 @@ internal fun createLineChartView(context: Context, state: HRSViewState): LineCha
|
|||||||
// draw selection line as dashed
|
// draw selection line as dashed
|
||||||
set1.enableDashedHighlightLine(10f, 5f, 0f)
|
set1.enableDashedHighlightLine(10f, 5f, 0f)
|
||||||
|
|
||||||
// set the filled area
|
|
||||||
|
|
||||||
// set the filled area
|
|
||||||
set1.setDrawFilled(true)
|
|
||||||
set1.fillFormatter = IFillFormatter { _, _ ->
|
|
||||||
axisLeft.axisMinimum
|
|
||||||
}
|
|
||||||
|
|
||||||
// set color of filled area
|
|
||||||
|
|
||||||
// set color of filled area
|
|
||||||
if (Utils.getSDKInt() >= 18) {
|
|
||||||
// drawables only supported on api level 18 and above
|
|
||||||
val drawable = ContextCompat.getDrawable(context, R.drawable.fade_red)
|
|
||||||
set1.fillDrawable = drawable
|
|
||||||
} else {
|
|
||||||
set1.fillColor = Color.BLACK
|
|
||||||
}
|
|
||||||
|
|
||||||
val dataSets = ArrayList<ILineDataSet>()
|
val dataSets = ArrayList<ILineDataSet>()
|
||||||
dataSets.add(set1) // add the data sets
|
dataSets.add(set1) // add the data sets
|
||||||
|
|
||||||
|
|
||||||
// create a data object with the data sets
|
|
||||||
|
|
||||||
// create a data object with the data sets
|
// create a data object with the data sets
|
||||||
val data = LineData(dataSets)
|
val data = LineData(dataSets)
|
||||||
|
|
||||||
@@ -202,8 +155,8 @@ internal fun createLineChartView(context: Context, state: HRSViewState): LineCha
|
|||||||
|
|
||||||
private fun updateData(points: List<Int>, chart: LineChart) {
|
private fun updateData(points: List<Int>, chart: LineChart) {
|
||||||
val entries = points.mapIndexed { i, v ->
|
val entries = points.mapIndexed { i, v ->
|
||||||
Entry(i.toFloat(), v.toFloat())
|
Entry(-i.toFloat(), v.toFloat())
|
||||||
}
|
}.reversed()
|
||||||
|
|
||||||
with(chart) {
|
with(chart) {
|
||||||
if (data != null && data.dataSetCount > 0) {
|
if (data != null && data.dataSetCount > 0) {
|
||||||
@@ -216,9 +169,3 @@ private fun updateData(points: List<Int>, chart: LineChart) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
private fun Preview() {
|
|
||||||
HRSContentView(state = HRSViewState()) { }
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package no.nordicsemi.android.hrs.viewmodel
|
||||||
|
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import no.nordicsemi.android.hrs.data.HRSDataHolder
|
||||||
|
import no.nordicsemi.android.hrs.view.DisconnectEvent
|
||||||
|
import no.nordicsemi.android.hrs.view.HRSScreenViewEvent
|
||||||
|
import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
internal class HRSViewModel @Inject constructor(
|
||||||
|
private val dataHolder: HRSDataHolder
|
||||||
|
) : CloseableViewModel() {
|
||||||
|
|
||||||
|
val state = dataHolder.data
|
||||||
|
|
||||||
|
fun onEvent(event: HRSScreenViewEvent) {
|
||||||
|
(event as? DisconnectEvent)?.let {
|
||||||
|
onDisconnectButtonClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onDisconnectButtonClick() {
|
||||||
|
finish()
|
||||||
|
dataHolder.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package no.nordicsemi.android.hts.data
|
||||||
|
|
||||||
|
import no.nordicsemi.android.theme.view.RadioGroupItem
|
||||||
|
|
||||||
|
internal data class HTSData(
|
||||||
|
val temperatureValue: Float = 0f,
|
||||||
|
val temperatureUnit: TemperatureUnit = TemperatureUnit.CELSIUS,
|
||||||
|
val batteryLevel: Int = 0,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun displayTemperature(): String {
|
||||||
|
return when (temperatureUnit) {
|
||||||
|
TemperatureUnit.CELSIUS -> String.format("%.1f °C", temperatureValue)
|
||||||
|
TemperatureUnit.FAHRENHEIT -> String.format("%.1f °F", temperatureValue * 1.8f + 32f)
|
||||||
|
TemperatureUnit.KELVIN -> String.format("%.1f °K", temperatureValue + 273.15f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun temperatureSettingsItems(): List<RadioGroupItem<TemperatureUnit>> {
|
||||||
|
return listOf(
|
||||||
|
RadioGroupItem(TemperatureUnit.CELSIUS,"°C"),
|
||||||
|
RadioGroupItem(TemperatureUnit.FAHRENHEIT, "°F"),
|
||||||
|
RadioGroupItem(TemperatureUnit.KELVIN, "°K")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum class TemperatureUnit {
|
||||||
|
CELSIUS, FAHRENHEIT, KELVIN
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package no.nordicsemi.android.hts.data
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
internal class HTSDataHolder @Inject constructor() {
|
||||||
|
|
||||||
|
private val _data = MutableStateFlow(HTSData())
|
||||||
|
val data: StateFlow<HTSData> = _data
|
||||||
|
|
||||||
|
fun setNewTemperature(temperature: Float) {
|
||||||
|
_data.tryEmit(_data.value.copy(temperatureValue = temperature))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBatteryLevel(batteryLevel: Int) {
|
||||||
|
_data.tryEmit(_data.value.copy(batteryLevel = batteryLevel))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTemperatureUnit(unit: TemperatureUnit) {
|
||||||
|
_data.tryEmit(_data.value.copy(temperatureUnit = unit))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
_data.tryEmit(HTSData())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ import no.nordicsemi.android.ble.common.callback.ht.TemperatureMeasurementDataCa
|
|||||||
import no.nordicsemi.android.ble.common.profile.ht.TemperatureType
|
import no.nordicsemi.android.ble.common.profile.ht.TemperatureType
|
||||||
import no.nordicsemi.android.ble.common.profile.ht.TemperatureUnit
|
import no.nordicsemi.android.ble.common.profile.ht.TemperatureUnit
|
||||||
import no.nordicsemi.android.ble.data.Data
|
import no.nordicsemi.android.ble.data.Data
|
||||||
|
import no.nordicsemi.android.hts.data.HTSDataHolder
|
||||||
import no.nordicsemi.android.log.LogContract
|
import no.nordicsemi.android.log.LogContract
|
||||||
import no.nordicsemi.android.service.BatteryManager
|
import no.nordicsemi.android.service.BatteryManager
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -41,23 +42,11 @@ private val HT_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A1C-0000-
|
|||||||
* enabling indication and reading characteristics. All operations required to connect to device
|
* enabling indication and reading characteristics. All operations required to connect to device
|
||||||
* with BLE HT Service and reading health thermometer values are performed here.
|
* with BLE HT Service and reading health thermometer values are performed here.
|
||||||
*/
|
*/
|
||||||
class HTSManager internal constructor(context: Context) : BatteryManager<HTSManagerCallbacks>(context) {
|
class HTSManager internal constructor(context: Context, private val dataHolder: HTSDataHolder) : BatteryManager(context) {
|
||||||
|
|
||||||
private var htCharacteristic: BluetoothGattCharacteristic? = null
|
private var htCharacteristic: BluetoothGattCharacteristic? = null
|
||||||
|
|
||||||
override fun getGattCallback(): BatteryManagerGattCallback {
|
private val temperatureMeasurementDataCallback = object : TemperatureMeasurementDataCallback() {
|
||||||
return HTManagerGattCallback()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* BluetoothGatt callbacks for connection/disconnection, service discovery,
|
|
||||||
* receiving indication, etc..
|
|
||||||
*/
|
|
||||||
private inner class HTManagerGattCallback : BatteryManagerGattCallback() {
|
|
||||||
override fun initialize() {
|
|
||||||
super.initialize()
|
|
||||||
setIndicationCallback(htCharacteristic)
|
|
||||||
.with(object : TemperatureMeasurementDataCallback() {
|
|
||||||
override fun onDataReceived(device: BluetoothDevice, data: Data) {
|
override fun onDataReceived(device: BluetoothDevice, data: Data) {
|
||||||
log(
|
log(
|
||||||
LogContract.Log.Level.APPLICATION,
|
LogContract.Log.Level.APPLICATION,
|
||||||
@@ -73,15 +62,27 @@ class HTSManager internal constructor(context: Context) : BatteryManager<HTSMana
|
|||||||
calendar: Calendar?,
|
calendar: Calendar?,
|
||||||
@TemperatureType type: Int?
|
@TemperatureType type: Int?
|
||||||
) {
|
) {
|
||||||
mCallbacks!!.onTemperatureMeasurementReceived(
|
dataHolder.setNewTemperature(temperature)
|
||||||
device,
|
|
||||||
temperature,
|
|
||||||
unit,
|
|
||||||
calendar,
|
|
||||||
type
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
override fun onBatteryLevelChanged(batteryLevel: Int) {
|
||||||
|
dataHolder.setBatteryLevel(batteryLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getGattCallback(): BatteryManagerGattCallback {
|
||||||
|
return HTManagerGattCallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BluetoothGatt callbacks for connection/disconnection, service discovery,
|
||||||
|
* receiving indication, etc..
|
||||||
|
*/
|
||||||
|
private inner class HTManagerGattCallback : BatteryManagerGattCallback() {
|
||||||
|
override fun initialize() {
|
||||||
|
super.initialize()
|
||||||
|
setIndicationCallback(htCharacteristic)
|
||||||
|
.with(temperatureMeasurementDataCallback)
|
||||||
enableIndications(htCharacteristic).enqueue()
|
enableIndications(htCharacteristic).enqueue()
|
||||||
}
|
}
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user