Refactoring & CR fixes

This commit is contained in:
Sylwester Zieliński
2021-10-05 10:15:14 +02:00
parent 0384b717b6
commit 90fa2db2ad
124 changed files with 986 additions and 1429 deletions

View File

@@ -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")

View File

@@ -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),
)
} }
} }
} }

View File

@@ -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() {

View File

@@ -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)

View File

@@ -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()
}

View File

@@ -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>

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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))
}
}

View File

@@ -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>

View File

@@ -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
}

View File

@@ -1,5 +0,0 @@
package no.nordicsemi.android.gls.viewmodel
sealed class GLSScreenViewEvent
object DisconnectEvent : GLSScreenViewEvent()

View File

@@ -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
}
}
}

View File

@@ -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>()

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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))
}
}

View File

@@ -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
)

View File

@@ -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>

View File

@@ -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
}

View File

@@ -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>()

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -1,5 +0,0 @@
package no.nordicsemi.android.hts.view
sealed class HTSScreenViewEvent
object DisconnectEvent : HTSScreenViewEvent()

View File

@@ -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))
}
}

View File

@@ -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)
} }
} }

View File

@@ -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

View File

@@ -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
}
} }

View File

@@ -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)
}
}

View File

@@ -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
*/ */

View File

@@ -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)
}
}

View File

@@ -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() {

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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(),

View File

@@ -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%"
) )
} }
}
} }

View File

@@ -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

View File

@@ -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()
} }
}
} }

View File

@@ -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)

View File

@@ -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),
)
}
}
)
}

View File

@@ -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)
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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")
)
} }
} }

View File

@@ -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())
}
}

View File

@@ -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(

View File

@@ -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) }
}

View File

@@ -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))
}
} }
} }
} }

View File

@@ -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) }
} }

View File

@@ -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"

View File

@@ -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())

View File

@@ -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)) },

View File

@@ -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()
} }
} }

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="no.nordicsemi.android.gls">
</manifest>

View File

@@ -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
}

View File

@@ -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())
}
}

View File

@@ -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,

View File

@@ -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 }
} }
} }

View File

@@ -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))
}
}
}
}

View File

@@ -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)
} }

View File

@@ -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()

View File

@@ -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()
}
}

View File

@@ -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,
) )

View File

@@ -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())
}
}

View File

@@ -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"

View File

@@ -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
}
}
} }

View File

@@ -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) }
}

View File

@@ -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()) { }
} }

View File

@@ -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) }
} }

View File

@@ -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()) { }
}

View File

@@ -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()
}
}

View File

@@ -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
}

View File

@@ -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())
}
}

View File

@@ -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