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 {
//Hilt requires to implement every module in the main app module
//https://github.com/google/dagger/issues/2123
implementation project(":feature_csc")
implementation project(":feature_hrs")
implementation project(":feature_hts")
implementation project(":feature_gls")
implementation project(':feature_scanner')
implementation project(':profile_csc')
implementation project(':profile_hrs')
implementation project(':profile_hts')
implementation project(':profile_gls')
implementation project(':profile_scanner')
implementation project(":lib_theme")
implementation project(":lib_utils")

View File

@@ -4,16 +4,16 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
@@ -25,10 +25,13 @@ import no.nordicsemi.android.theme.NordicColors
@Composable
fun FeatureButton(@DrawableRes iconId: Int, @StringRes nameId: Int, onClick: () -> Unit) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = { onClick() },
colors = ButtonDefaults.buttonColors(backgroundColor = NordicColors.NordicGray4.value()),
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.background(NordicColors.ItemHighlight.value())
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(iconId),
@@ -41,14 +44,10 @@ fun FeatureButton(@DrawableRes iconId: Int, @StringRes nameId: Int, onClick: ()
)
Row(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
Text(
text = stringResource(id = nameId),
modifier = Modifier.padding(16.dp),
)
Text(text = stringResource(id = nameId))
}
}
}

View File

@@ -1,10 +1,11 @@
package no.nordicsemi.android.nrftoolbox
import android.app.Activity
import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.layout.Column
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@@ -12,14 +13,17 @@ import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
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.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import no.nordicsemi.android.csc.view.CscScreen
import no.nordicsemi.android.csc.view.CSCScreen
import no.nordicsemi.android.gls.view.GLSScreen
import no.nordicsemi.android.hrs.view.HRSScreen
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.ScanDeviceScreen
import no.nordicsemi.android.scanner.view.ScanDeviceScreenResult
import no.nordicsemi.android.theme.view.CloseIconAppBar
import no.nordicsemi.android.utils.exhaustive
@Composable
@@ -42,12 +47,12 @@ internal fun HomeScreen() {
NavHost(navController = navController, startDestination = NavDestination.HOME.id) {
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.HTS.id) { HTSScreen { viewModel.navigateUp() } }
composable(NavDestination.GLS.id) { GLSScreen { viewModel.navigateUp() } }
composable(NavDestination.REQUEST_PERMISSION.id) { RequestPermissionScreen(continueAction) }
composable(NavDestination.BLUETOOTH_NOT_AVAILABLE.id) { BluetoothNotAvailableScreen() }
composable(NavDestination.BLUETOOTH_NOT_AVAILABLE.id) { BluetoothNotAvailableScreen{ viewModel.finish() } }
composable(NavDestination.BLUETOOTH_NOT_ENABLED.id) {
BluetoothNotEnabledScreen(continueAction)
}
@@ -69,11 +74,18 @@ internal fun HomeScreen() {
@Composable
fun HomeView(callback: (NavDestination) -> Unit) {
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) }
Spacer(modifier = Modifier.height(1.dp))
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) }
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)
@Composable
fun DefaultPreview() {

View File

@@ -1,15 +1,15 @@
package no.nordicsemi.android.nrftoolbox
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import dagger.hilt.android.AndroidEntryPoint
import no.nordicsemi.android.theme.TestTheme
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View File

@@ -4,5 +4,4 @@ import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class NrfToolboxApplication : Application() {
}
class NrfToolboxApplication : Application()

View File

@@ -2,4 +2,5 @@
<string name="csc_module">CSC</string>
<string name="hrs_module">HRS</string>
<string name="gls_module">GLS</string>
<string name="hts_module">HTS</string>
</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.util.Log
import androidx.annotation.IntRange
import no.nordicsemi.android.ble.BleManager
import no.nordicsemi.android.ble.callback.DataReceivedCallback
import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelDataCallback
import no.nordicsemi.android.ble.data.Data
@@ -18,17 +19,10 @@ import java.util.*
* @param <T> The profile callbacks type.
* @see BleManager
</T> */
abstract class BatteryManager<T : BatteryManagerCallbacks?>(context: Context) : LoggableBleManager<T>(context) {
abstract class BatteryManager(context: Context) : BleManager(context) {
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 =
object : BatteryLevelDataCallback() {
override fun onBatteryLevelChanged(
@@ -36,8 +30,7 @@ abstract class BatteryManager<T : BatteryManagerCallbacks?>(context: Context) :
@IntRange(from = 0, to = 100) batteryLevel: Int
) {
log(LogContract.Log.Level.APPLICATION, "Battery Level received: $batteryLevel%")
this@BatteryManager.batteryLevel = batteryLevel
mCallbacks?.onBatteryLevelChanged(device, batteryLevel)
onBatteryLevelChanged(batteryLevel)
}
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() {
if (isConnected) {
readCharacteristic(batteryLevelCharacteristic)
.with(batteryLevelDataCallback)
.fail { device: BluetoothDevice?, status: Int ->
log(
Log.WARN,
"Battery Level characteristic not found"
)
log(Log.WARN, "Battery Level characteristic not found")
}
.enqueue()
}
@@ -66,32 +58,10 @@ abstract class BatteryManager<T : BatteryManagerCallbacks?>(context: Context) :
.with(batteryLevelDataCallback)
enableNotifications(batteryLevelCharacteristic)
.done { device: BluetoothDevice? ->
log(
Log.INFO,
"Battery Level notifications enabled"
)
log(Log.INFO, "Battery Level notifications enabled")
}
.fail { device: BluetoothDevice?, status: Int ->
log(
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"
)
log(Log.WARN, "Battery Level characteristic not found")
}
.enqueue()
}
@@ -106,16 +76,14 @@ abstract class BatteryManager<T : BatteryManagerCallbacks?>(context: Context) :
override fun isOptionalServiceSupported(gatt: BluetoothGatt): Boolean {
val service = gatt.getService(BATTERY_SERVICE_UUID)
if (service != null) {
batteryLevelCharacteristic = service.getCharacteristic(
BATTERY_LEVEL_CHARACTERISTIC_UUID
)
batteryLevelCharacteristic = service.getCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)
}
return batteryLevelCharacteristic != null
}
override fun onDeviceDisconnected() {
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
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Binder
import android.os.Handler
import android.os.IBinder
import android.util.Log
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.lifecycle.LifecycleService
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import dagger.hilt.android.AndroidEntryPoint
import no.nordicsemi.android.ble.BleManagerCallbacks
import no.nordicsemi.android.ble.utils.ILogger
import no.nordicsemi.android.ble.BleManager
import no.nordicsemi.android.log.ILogSession
import no.nordicsemi.android.log.Logger
import javax.inject.Inject
@AndroidEntryPoint
abstract class BleProfileService : LifecycleService(), BleManagerCallbacks {
abstract class BleProfileService : LifecycleService() {
private var bleManager: LoggableBleManager<out BleManagerCallbacks>? = null
protected abstract val manager: BleManager
@Inject
lateinit var bluetoothDeviceHolder: SelectedBluetoothDeviceHolder
@@ -56,9 +44,8 @@ abstract class BleProfileService : LifecycleService(), BleManagerCallbacks {
* 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.
*/
protected var handler: Handler? = null
private set
protected var bound = false
private var handler: Handler? = null
private var activityIsChangingConfiguration = false
/**
@@ -66,256 +53,45 @@ abstract class BleProfileService : LifecycleService(), BleManagerCallbacks {
*
* @return bluetooth device
*/
protected val bluetoothDevice: BluetoothDevice by lazy {
private val bluetoothDevice: BluetoothDevice by lazy {
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
* [.onServiceStarted] has been called.
*
* @return the log session
*/
protected var logSession: ILogSession? = null
private var logSession: ILogSession? = null
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() {
super.onCreate()
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.
*
* @return true to use autoConnect feature, false (default) otherwise.
*/
protected fun shouldAutoConnect(): Boolean {
private fun shouldAutoConnect(): Boolean {
return false
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
val logUri = intent?.getParcelableExtra<Uri>(EXTRA_LOG_URI)
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)
manager.connect(bluetoothDevice)
.useAutoConnect(shouldAutoConnect())
.retry(3, 100)
.enqueue()
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) {
super.onTaskRemoved(rootIntent)
// This method is called when user removed the app from Recents.
@@ -326,58 +102,15 @@ abstract class BleProfileService : LifecycleService(), BleManagerCallbacks {
override fun onDestroy() {
super.onDestroy()
// Unregister broadcast receivers
unregisterReceiver(bluetoothStateBroadcastReceiver)
// shutdown the manager
bleManager!!.close()
manager.close()
Logger.i(logSession, "Service destroyed")
bleManager = null
bluetoothDeviceHolder.forgetDevice()
deviceName = null
logSession = 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.
* In that case the [.stopService] method must be called when done.
@@ -388,102 +121,19 @@ abstract class BleProfileService : LifecycleService(), BleManagerCallbacks {
return true
}
override fun onDeviceDisconnected(device: BluetoothDevice) {
// 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() {
private fun stopService() {
// user requested disconnection. We must stop the service
Logger.v(logSession, "Stopping service...")
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
*
* @param messageResId an resource id of the message to be shown
*/
protected fun showToast(messageResId: Int) {
handler!!.post {
handler?.post {
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
*/
protected fun showToast(message: String?) {
handler!!.post {
handler?.post {
Toast.makeText(this@BleProfileService, message, Toast.LENGTH_SHORT).show()
}
}
@@ -505,7 +155,7 @@ abstract class BleProfileService : LifecycleService(), BleManagerCallbacks {
* @return device address
*/
protected val deviceAddress: String
protected get() = bluetoothDevice!!.address
get() = bluetoothDevice.address
/**
* 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
*/
protected val isConnected: Boolean
protected get() = bleManager != null && bleManager!!.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
}
get() = manager.isConnected
}

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"
abstract class ForegroundBleService<T : BatteryManager<out BatteryManagerCallbacks>> : BleProfileService() {
protected abstract val manager: T
abstract class ForegroundBleService : BleProfileService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val result = super.onStartCommand(intent, flags, startId)
@@ -48,22 +46,6 @@ abstract class ForegroundBleService<T : BatteryManager<out BatteryManagerCallbac
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
*/

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) }
}
fun isDeviceBonded(): Boolean {
fun isBondingRequired(): Boolean {
return device?.bondState == BluetoothDevice.BOND_NONE
}
fun bondDevice() {

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="no.nordicsemi.android.theme">
<manifest package="no.nordicsemi.android.theme">
</manifest>

View File

@@ -28,16 +28,17 @@ object NordicColors {
val TableViewBackground = NeutralColor(Color(0xFFF2F2F6))
val TableViewSeparator = NeutralColor(Color(0xFFD2D2D6))
val Primary = ThemedColor(Color(0xFF00A9CE), Color(0xFF212121))
val PrimaryVariant = ThemedColor(Color(0xFF008CD2), Color.Black)
val Secondary = ThemedColor(Color(0xFF00A9CE), Color(0xFF008CD2))
val SecondaryVariant = ThemedColor(Color(0xFF008CD2), Color(0xFF008CD2))
val Primary = ThemedColor(Color(0xFF00A9CE), Color(0xFF00A9CE))
val PrimaryVariant = ThemedColor(Color(0xFF008CD2), Color(0xFF00A9CE))
val Secondary = ThemedColor(Color(0xFF00A9CE), Color(0xFF00A9CE))
val SecondaryVariant = ThemedColor(Color(0xFF008CD2), Color(0xFF00A9CE))
val OnPrimary = ThemedColor(Color.White, Color.White)
val OnSecondary = ThemedColor(Color.White, Color.White)
val OnBackground = ThemedColor(Color.Black, Color.White)
val OnSurface = ThemedColor(Color.Black, Color.White)
val Background = ThemedColor(Color(0xFFDADADA), Color.Black)
val Surface = ThemedColor(Color(0xFFDADADA), Color.Black)
val ItemHighlight = ThemedColor(Color.White, Color(0xFF1E1E1E))
val Background = ThemedColor(Color(0xFFF5F5F5), Color(0xFF121212))
val Surface = ThemedColor(Color(0xFFF5F5F5), Color(0xFF121212))
}
sealed class NordicColor {

View File

@@ -7,7 +7,7 @@ import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
@Composable
fun TestTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
fun TestTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
val darkColorPalette = darkColors(
primary = NordicColors.Primary.value(),

View File

@@ -1,28 +1,15 @@
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.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.theme.NordicColors
import no.nordicsemi.android.theme.R
@Composable
fun BatteryLevelView(batteryLevel: Int) {
Card(
backgroundColor = NordicColors.NordicGray4.value(),
shape = RoundedCornerShape(10.dp),
elevation = 0.dp
) {
Box(modifier = Modifier.padding(16.dp)) {
KeyValueField(
stringResource(id = R.string.field_battery),
"$batteryLevel%"
)
}
ScreenSection {
KeyValueField(
stringResource(id = R.string.field_battery),
"$batteryLevel%"
)
}
}

View File

@@ -9,7 +9,7 @@ import androidx.compose.ui.Modifier
import no.nordicsemi.android.theme.NordicColors
@Composable
fun KeyValueField(key: String, value: String) {
fun KeyValueField(key: String, value: String) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween

View File

@@ -1,18 +1,23 @@
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.ui.Modifier
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.theme.NordicColors
@Composable
fun SensorRecordCard(content: @Composable () -> Unit) {
fun ScreenSection(content: @Composable () -> Unit) {
Card(
backgroundColor = NordicColors.NordicGray4.value(),
shape = RoundedCornerShape(10.dp),
backgroundColor = NordicColors.ItemHighlight.value(),
shape = RoundedCornerShape(4.dp),
elevation = 0.dp
) {
content()
Box(modifier = Modifier.padding(16.dp)) {
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.Row
@@ -12,10 +12,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
internal fun SpeedUnitRadioGroup(
currentItem: RadioGroupItem,
items: List<RadioGroupItem>,
onEvent: (RadioGroupItem) -> Unit
fun <T> SpeedUnitRadioGroup(
currentItem: T,
items: List<RadioGroupItem<T>>,
onEvent: (RadioGroupItem<T>) -> Unit
) {
Row(
modifier = Modifier.fillMaxWidth(),
@@ -28,14 +28,14 @@ internal fun SpeedUnitRadioGroup(
}
@Composable
internal fun SpeedUnitRadioButton(
selectedItem: RadioGroupItem,
displayedItem: RadioGroupItem,
onEvent: (RadioGroupItem) -> Unit
internal fun <T> SpeedUnitRadioButton(
selectedItem: T,
displayedItem: RadioGroupItem<T>,
onEvent: (RadioGroupItem<T>) -> Unit
) {
Row {
RadioButton(
selected = (selectedItem == displayedItem),
selected = (selectedItem == displayedItem.unit),
onClick = { onEvent(displayedItem) }
)
Spacer(modifier = Modifier.width(4.dp))
@@ -43,12 +43,4 @@ internal fun SpeedUnitRadioButton(
}
}
internal fun createSpeedUnitLabel(unit: SpeedUnit): String {
return when (unit) {
SpeedUnit.M_S -> "m/s"
SpeedUnit.KM_H -> "km/h"
SpeedUnit.MPH -> "mph"
}
}
data class RadioGroupItem(val label: String)
data class RadioGroupItem<T>(val unit: T, 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="colorSecondaryDark">#FF004c97</color>
<color name="colorOnSecondary">#FFFFFFFF</color>
<color name="background">#FFDADADA</color>
<color name="background">#FFF5F5F5</color>
<color name="actionBarColor">#FF0090B0</color>
</resources>

View File

@@ -2,6 +2,9 @@
<resources>
<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>
</resources>

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="no.nordicsemi.android.utils">
<manifest package="no.nordicsemi.android.utils">
</manifest>

View File

@@ -1,7 +1,9 @@
package no.nordicsemi.android.csc.data
import androidx.compose.runtime.Composable
import no.nordicsemi.android.csc.view.CSCSettings
import no.nordicsemi.android.csc.view.SpeedUnit
import no.nordicsemi.android.theme.view.RadioGroupItem
import java.util.*
internal data class CSCData(
@@ -9,15 +11,21 @@ internal data class CSCData(
val scanDevices: Boolean = false,
val selectedSpeedUnit: SpeedUnit = SpeedUnit.M_S,
val speed: Float = 0f,
val cadence: Int = 0,
val cadence: Float = 0f,
val distance: Float = 0f,
val totalDistance: Float = 0f,
val gearRatio: Float = 0f,
val batteryLevel: Int = 0,
val wheelSize: String = CSCSettings.DefaultWheelSize.NAME,
val isScreenActive: Boolean = true
val wheelSize: Int = CSCSettings.DefaultWheelSize.VALUE,
val wheelSizeDisplay: String = CSCSettings.DefaultWheelSize.NAME
) {
@Composable
fun drawItself() {
}
private val speedWithUnit = when (selectedSpeedUnit) {
SpeedUnit.M_S -> speed
SpeedUnit.KM_H -> speed * 3.6f
@@ -33,7 +41,7 @@ internal data class CSCData(
}
fun displayCadence(): String {
return String.format("%d RPM", cadence)
return String.format("%.0f RPM", cadence)
}
fun displayDistance(): String {
@@ -56,7 +64,11 @@ internal data class CSCData(
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 no.nordicsemi.android.ble.common.callback.csc.CyclingSpeedAndCadenceMeasurementDataCallback
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.view.CSCSettings
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. */
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 wheelSize = CSCSettings.DefaultWheelSize.VALUE
override fun onBatteryLevelChanged(batteryLevel: Int) {
dataHolder.setBatteryLevel(batteryLevel)
}
override fun getGattCallback(): BatteryManagerGattCallback {
return CSCManagerGattCallback()
}
@@ -82,7 +87,7 @@ internal class CSCManager(context: Context) : BatteryManager<CSCManagerCallbacks
@FloatRange(from = 0.0) distance: Float,
@FloatRange(from = 0.0) speed: Float
) {
mCallbacks?.onDistanceChanged(device, totalDistance, distance, speed)
dataHolder.setNewDistance(totalDistance, distance, speed)
}
override fun onCrankDataChanged(
@@ -90,7 +95,7 @@ internal class CSCManager(context: Context) : BatteryManager<CSCManagerCallbacks
@FloatRange(from = 0.0) crankCadence: Float,
gearRatio: Float
) {
mCallbacks?.onCrankDataChanged(device, crankCadence, gearRatio)
dataHolder.setNewCrankCadence(crankCadence, gearRatio)
}
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.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.MaterialTheme
@@ -16,7 +15,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.csc.R
import no.nordicsemi.android.csc.data.CSCData
import no.nordicsemi.android.theme.view.SensorRecordCard
import no.nordicsemi.android.theme.view.ScreenSection
import no.nordicsemi.android.theme.view.SpeedUnitRadioGroup
@Composable
internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
@@ -25,9 +25,10 @@ internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
}
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(16.dp))
SettingsSection(state, onEvent)
Spacer(modifier = Modifier.height(16.dp))
@@ -47,16 +48,17 @@ internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
@Composable
private fun SettingsSection(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
SensorRecordCard {
ScreenSection {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
WheelSizeView(state, onEvent)
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 androidx.compose.foundation.layout.Column
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@@ -11,19 +9,21 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.csc.R
import no.nordicsemi.android.csc.service.CSCService
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
@Composable
fun CscScreen(finishAction: () -> Unit) {
val viewModel: CscViewModel = hiltViewModel()
fun CSCScreen(finishAction: () -> Unit) {
val viewModel: CSCViewModel = hiltViewModel()
val state = viewModel.state.collectAsState().value
val isScreenActive = viewModel.isActive.collectAsState().value
val context = LocalContext.current
LaunchedEffect(state.isScreenActive) {
if (!state.isScreenActive) {
LaunchedEffect(isScreenActive) {
if (!isScreenActive) {
finishAction()
}
if (context.isServiceRunning(CSCService::class.java.name)) {
@@ -45,7 +45,9 @@ fun CscScreen(finishAction: () -> Unit) {
@Composable
private fun CSCView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
Column {
TopAppBar(title = { Text(text = stringResource(id = R.string.csc_title)) })
BackIconAppBar(stringResource(id = R.string.csc_title)) {
onEvent(OnDisconnectButtonClick)
}
CSCContentView(state) { onEvent(it) }
}

View File

@@ -1,6 +1,6 @@
package no.nordicsemi.android.csc.view
object CSCSettings {
internal object CSCSettings {
object DefaultWheelSize {
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.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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.theme.view.BatteryLevelView
import no.nordicsemi.android.theme.view.KeyValueField
import no.nordicsemi.android.theme.view.SensorRecordCard
import no.nordicsemi.android.theme.view.ScreenSection
@Composable
internal fun SensorsReadingView(state: CSCData) {
SensorRecordCard {
Column(modifier = Modifier.padding(16.dp)) {
ScreenSection {
Column {
KeyValueField(stringResource(id = R.string.scs_field_speed), state.displaySpeed())
Spacer(modifier = Modifier.height(4.dp))
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) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = state.wheelSize,
value = state.wheelSizeDisplay,
onValueChange = { },
enabled = false,
label = { Text(text = stringResource(id = R.string.scs_field_wheel_size)) },

View File

@@ -1,35 +1,22 @@
package no.nordicsemi.android.csc.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.csc.data.CSCData
import no.nordicsemi.android.csc.service.CSCDataReadBroadcast
import no.nordicsemi.android.csc.data.CSCDataHolder
import no.nordicsemi.android.csc.view.CSCViewEvent
import no.nordicsemi.android.csc.view.OnDisconnectButtonClick
import no.nordicsemi.android.csc.view.OnSelectedSpeedUnitSelected
import no.nordicsemi.android.csc.view.OnShowEditWheelSizeDialogButtonClick
import no.nordicsemi.android.csc.view.OnWheelSizeSelected
import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
import no.nordicsemi.android.utils.exhaustive
import javax.inject.Inject
@HiltViewModel
internal class CscViewModel @Inject constructor(
private val localBroadcast: CSCDataReadBroadcast
) : ViewModel() {
internal class CSCViewModel @Inject constructor(
private val dataHolder: CSCDataHolder
) : CloseableViewModel() {
val state = MutableStateFlow(CSCData())
init {
localBroadcast.events.onEach {
withContext(Dispatchers.Main) { state.value = it }
}.launchIn(viewModelScope)
}
val state = dataHolder.data
fun onEvent(event: CSCViewEvent) {
when (event) {
@@ -41,22 +28,19 @@ internal class CscViewModel @Inject constructor(
}
private fun onSelectedSpeedUnit(event: OnSelectedSpeedUnitSelected) {
state.tryEmit(state.value.copy(selectedSpeedUnit = event.selectedSpeedUnit))
dataHolder.setSpeedUnit(event.selectedSpeedUnit)
}
private fun onShowDialogEvent() {
state.tryEmit(state.value.copy(showDialog = true))
dataHolder.setDisplayWheelSizeDialog()
}
private fun onWheelSizeChanged(event: OnWheelSizeSelected) {
localBroadcast.setWheelSize(event.wheelSize)
state.tryEmit(state.value.copy(
showDialog = false,
wheelSize = event.wheelSizeDisplayInfo
))
dataHolder.setWheelSize(event.wheelSize, event.wheelSizeDisplayInfo)
}
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 */
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,
/** The type of the record. 0 if not present */
@@ -49,6 +49,8 @@ internal data class GLSRecord(
)
internal data class MeasurementContext(
/** Record sequence number */
val sequenceNumber: Int = 0,
val carbohydrateId: CarbohydrateId = CarbohydrateId.NOT_PRESENT,

View File

@@ -28,7 +28,6 @@ import android.bluetooth.BluetoothGattCharacteristic
import android.content.Context
import android.util.Log
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointDataCallback
import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementContextDataCallback
import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementDataCallback
@@ -44,7 +43,7 @@ import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContex
import no.nordicsemi.android.ble.data.Data
import no.nordicsemi.android.gls.data.CarbohydrateId
import no.nordicsemi.android.gls.data.ConcentrationUnit
import no.nordicsemi.android.gls.data.GLSData
import no.nordicsemi.android.gls.data.GLSDataHolder
import no.nordicsemi.android.gls.data.GLSRecord
import no.nordicsemi.android.gls.data.HealthStatus
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.log.LogContract
import no.nordicsemi.android.service.BatteryManager
import no.nordicsemi.android.service.BatteryManagerCallbacks
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
@@ -78,16 +76,18 @@ private val RACP_CHARACTERISTIC = UUID.fromString("00002A52-0000-1000-8000-00805
@Singleton
internal class GLSManager @Inject constructor(
@ApplicationContext context: Context
) : BatteryManager<BatteryManagerCallbacks?>(context) {
val data = MutableStateFlow(GLSData())
private val records = hashMapOf<Int, GLSRecord>()
@ApplicationContext context: Context,
private val dataHolder: GLSDataHolder
) : BatteryManager(context) {
private var glucoseMeasurementCharacteristic: BluetoothGattCharacteristic? = null
private var glucoseMeasurementContextCharacteristic: BluetoothGattCharacteristic? = null
private var recordAccessControlPointCharacteristic: BluetoothGattCharacteristic? = null
override fun onBatteryLevelChanged(batteryLevel: Int) {
dataHolder.setNewBatteryLevel(batteryLevel)
}
override fun getGattCallback(): BatteryManagerGattCallback {
return GlucoseManagerGattCallback()
}
@@ -121,10 +121,14 @@ internal class GLSManager @Inject constructor(
.with(object : GlucoseMeasurementDataCallback() {
override fun onGlucoseMeasurementReceived(
device: BluetoothDevice, sequenceNumber: Int,
time: Calendar, glucoseConcentration: Float?,
unit: Int?, type: Int?,
sampleLocation: Int?, status: GlucoseStatus?,
device: BluetoothDevice,
sequenceNumber: Int,
time: Calendar,
glucoseConcentration: Float?,
unit: Int?,
type: Int?,
sampleLocation: Int?,
status: GlucoseStatus?,
contextInformationFollows: Boolean
) {
val record = GLSRecord(
@@ -138,27 +142,29 @@ internal class GLSManager @Inject constructor(
status = status?.value ?: 0
)
records[record.sequenceNumber] = record
if (!contextInformationFollows) {
data.tryEmit(data.value.copy(record = records.values.toList()))
}
dataHolder.addNewRecord(record)
}
})
setNotificationCallback(glucoseMeasurementContextCharacteristic)
.with(object : GlucoseMeasurementContextDataCallback() {
override fun onGlucoseMeasurementContextReceived(
device: BluetoothDevice, sequenceNumber: Int,
carbohydrate: Carbohydrate?, carbohydrateAmount: Float?,
meal: Meal?, tester: Tester?,
health: Health?, exerciseDuration: Int?,
exerciseIntensity: Int?, medication: Medication?,
medicationAmount: Float?, medicationUnit: Int?,
device: BluetoothDevice,
sequenceNumber: Int,
carbohydrate: Carbohydrate?,
carbohydrateAmount: Float?,
meal: Meal?,
tester: Tester?,
health: Health?,
exerciseDuration: Int?,
exerciseIntensity: Int?,
medication: Medication?,
medicationAmount: Float?,
medicationUnit: Int?,
HbA1c: Float?
) {
val record = records[sequenceNumber] ?: return
val context = MeasurementContext(
sequenceNumber = sequenceNumber,
carbohydrateId = carbohydrate?.value?.let { CarbohydrateId.create(it) }
?: CarbohydrateId.NOT_PRESENT,
carbohydrateUnits = carbohydrateAmount ?: 0f,
@@ -177,9 +183,8 @@ internal class GLSManager @Inject constructor(
?: MedicationUnit.UNIT_KG,
HbA1c = HbA1c ?: 0f
)
record.context = context
data.tryEmit(data.value)
dataHolder.addNewContext(context)
}
})
setIndicationCallback(recordAccessControlPointCharacteristic)
@@ -194,14 +199,14 @@ internal class GLSManager @Inject constructor(
RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED
else -> RequestStatus.SUCCESS
}
data.tryEmit(data.value.copy(requestStatus = status))
dataHolder.setRequestStatus(status)
}
override fun onRecordAccessOperationCompletedWithNoRecordsFound(
device: BluetoothDevice,
@RACPOpCode requestCode: Int
) {
data.tryEmit(data.value.copy(requestStatus = RequestStatus.SUCCESS))
dataHolder.setRequestStatus(RequestStatus.SUCCESS)
}
override fun onNumberOfRecordsReceived(
@@ -211,8 +216,8 @@ internal class GLSManager @Inject constructor(
//TODO("Probably not needed")
// mCallbacks!!.onNumberOfRecordsRequested(device, numberOfRecords)
if (numberOfRecords > 0) {
if (records.size > 0) {
val sequenceNumber = records.keys.last() + 1
if (dataHolder.records().isNotEmpty()) {
val sequenceNumber = dataHolder.records().last().sequenceNumber + 1 //TODO check if correct
writeCharacteristic(
recordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(
@@ -228,7 +233,7 @@ internal class GLSManager @Inject constructor(
.enqueue()
}
} 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)")
if (errorCode == RACP_ERROR_OP_CODE_NOT_SUPPORTED) {
data.tryEmit(data.value.copy(requestStatus = RequestStatus.NOT_SUPPORTED))
dataHolder.setRequestStatus(RequestStatus.NOT_SUPPORTED)
} 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
}
override fun onServicesInvalidated() {
TODO("Not yet implemented")
}
override fun onServicesInvalidated() { }
override fun isOptionalServiceSupported(gatt: BluetoothGatt): Boolean {
super.isOptionalServiceSupported(gatt)
@@ -290,11 +293,11 @@ internal class GLSManager @Inject constructor(
/**
* Clears the records list locally.
*/
fun clear() {
records.clear()
private fun clear() {
dataHolder.clearRecords()
val target = bluetoothDevice
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
* Control Point indication with status code Success or other in case of error.
*/
fun lastRecord(): Unit {
fun requestLastRecord() {
if (recordAccessControlPointCharacteristic == null) return
val target = bluetoothDevice ?: return
clear()
data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
dataHolder.setRequestStatus(RequestStatus.PENDING)
writeCharacteristic(
recordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportLastStoredRecord()
@@ -326,11 +329,11 @@ internal class GLSManager @Inject constructor(
* returned to Glucose Measurement characteristic as a notification followed by Record Access
* Control Point indication with status code Success or other in case of error.
*/
fun requestFirstRecord(): Unit {
fun requestFirstRecord() {
if (recordAccessControlPointCharacteristic == null) return
val target = bluetoothDevice ?: return
clear()
data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
dataHolder.setRequestStatus(RequestStatus.PENDING)
writeCharacteristic(
recordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportFirstStoredRecord()
@@ -350,11 +353,11 @@ internal class GLSManager @Inject constructor(
* will be returned to Glucose Measurement characteristic as a notification followed by
* Record Access Control Point indication with status code Success or other in case of error.
*/
fun requestAllRecords(): Unit {
fun requestAllRecords() {
if (recordAccessControlPointCharacteristic == null) return
val target = bluetoothDevice ?: return
clear()
data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
dataHolder.setRequestStatus(RequestStatus.PENDING)
writeCharacteristic(
recordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportNumberOfAllStoredRecords()
@@ -382,13 +385,13 @@ internal class GLSManager @Inject constructor(
fun refreshRecords() {
if (recordAccessControlPointCharacteristic == null) return
val target = bluetoothDevice ?: return
if (records.size == 0) {
if (dataHolder.records().isEmpty()) {
requestAllRecords()
} else {
data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
dataHolder.setRequestStatus(RequestStatus.PENDING)
// obtain the last sequence number
val sequenceNumber = records.keys.last() + 1
val sequenceNumber = dataHolder.records().last().sequenceNumber + 1 //TODO check if correct
writeCharacteristic(
recordAccessControlPointCharacteristic,
RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber)
@@ -432,7 +435,7 @@ internal class GLSManager @Inject constructor(
if (recordAccessControlPointCharacteristic == null) return
val target = bluetoothDevice ?: return
clear()
data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
dataHolder.setRequestStatus(RequestStatus.PENDING)
writeCharacteristic(
recordAccessControlPointCharacteristic,
RecordAccessControlPointData.deleteAllStoredRecords()
@@ -445,7 +448,7 @@ internal class GLSManager @Inject constructor(
}
.enqueue()
val elements = listOf<Int>(1, 2, 3)
val elements = listOf(1, 2, 3)
val result = elements.all { it > 3 }
}
}

View File

@@ -23,7 +23,7 @@ package no.nordicsemi.android.gls.repository
import no.nordicsemi.android.ble.data.Data
object GLSRecordAccessControlPointParser {
object GLSRecordAccessControlPointParser {
private const val OP_CODE_REPORT_STORED_RECORDS = 1
private const val OP_CODE_DELETE_STORED_RECORDS = 2

View File

@@ -3,8 +3,8 @@ package no.nordicsemi.android.gls.view
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.MaterialTheme
@@ -18,18 +18,27 @@ import no.nordicsemi.android.gls.R
import no.nordicsemi.android.gls.data.GLSData
import no.nordicsemi.android.gls.viewmodel.DisconnectEvent
import no.nordicsemi.android.gls.viewmodel.GLSScreenViewEvent
import no.nordicsemi.android.gls.viewmodel.OnWorkingModeSelected
import no.nordicsemi.android.theme.view.BatteryLevelView
import no.nordicsemi.android.theme.view.ScreenSection
import no.nordicsemi.android.theme.view.SpeedUnitRadioGroup
@Composable
internal fun GLSContentView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
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)
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
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.LaunchedEffect
import androidx.compose.runtime.collectAsState
@@ -10,23 +8,38 @@ import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.gls.R
import no.nordicsemi.android.gls.data.GLSData
import no.nordicsemi.android.gls.viewmodel.DisconnectEvent
import no.nordicsemi.android.gls.viewmodel.GLSScreenViewEvent
import no.nordicsemi.android.gls.viewmodel.GLSViewModel
import no.nordicsemi.android.theme.view.BackIconAppBar
@Composable
fun GLSScreen(finishAction: () -> Unit) {
val viewModel: GLSViewModel = hiltViewModel()
val state = viewModel.state.collectAsState().value
val isScreenActive = viewModel.isActive.collectAsState().value
LaunchedEffect(state.isDeviceBonded) {
// viewModel.bondDevice()
viewModel.bondDevice()
}
LaunchedEffect(isScreenActive) {
if (!isScreenActive) {
finishAction()
}
}
GLSView(state) {
viewModel.onEvent(it)
}
}
@Composable
private fun GLSView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) {
Column {
TopAppBar(title = { Text(text = stringResource(id = R.string.gls_title)) })
BackIconAppBar(stringResource(id = R.string.gls_title)) {
onEvent(DisconnectEvent)
}
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(
val heartRates: List<Int> = emptyList(),
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 {
fun parse(data: Data): String {
val value = data.getIntValue(Data.FORMAT_UINT8, 0)!!
return when (value) {
return when (data.getIntValue(Data.FORMAT_UINT8, 0)!!) {
6 -> "Foot"
5 -> "Ear Lobe"
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.profile.hr.BodySensorLocation
import no.nordicsemi.android.ble.data.Data
import no.nordicsemi.android.hrs.data.HRSDataHolder
import no.nordicsemi.android.log.LogContract
import no.nordicsemi.android.service.BatteryManager
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,
* enabling notification and reading characteristics.
* All operations required to connect to device with BLE Heart Rate Service and reading
* 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 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 {
return HeartRateManagerCallback()
}
@@ -58,56 +100,14 @@ class HRSManager(context: Context) : BatteryManager<HRSManagerCallbacks>(context
override fun initialize() {
super.initialize()
readCharacteristic(bodySensorLocationCharacteristic)
.with(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
) {
mCallbacks?.onBodySensorLocationReceived(device, sensorLocation)
}
})
.with(bodySensorLocationDataCallback)
.fail { device: BluetoothDevice?, status: Int ->
log(Log.WARN, "Body Sensor Location characteristic not found")
}
.enqueue()
setNotificationCallback(heartRateCharacteristic)
.with(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>?
) {
mCallbacks?.onHeartRateMeasurementReceived(
device,
heartRate,
contactDetected,
energyExpanded,
rrIntervals
)
}
})
.with(heartRateMeasurementDataCallback)
enableNotifications(heartRateCharacteristic).enqueue()
}
@@ -140,23 +140,4 @@ class HRSManager(context: Context) : BatteryManager<HRSManagerCallbacks>(context
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.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
@@ -16,22 +15,21 @@ 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 no.nordicsemi.android.hts.R
import no.nordicsemi.android.hts.data.HTSData
import no.nordicsemi.android.hrs.R
import no.nordicsemi.android.hrs.data.HRSData
import no.nordicsemi.android.theme.view.BatteryLevelView
import no.nordicsemi.android.theme.view.SensorRecordCard
import no.nordicsemi.android.theme.view.ScreenSection
@Composable
internal fun HTSContentView(state: HTSData, onEvent: (HTSScreenViewEvent) -> Unit) {
internal fun HRSContentView(state: HRSData, onEvent: (HRSScreenViewEvent) -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
SensorRecordCard {
Box(modifier = Modifier.padding(16.dp)) {
Spacer(modifier = Modifier.height(16.dp))
ScreenSection {
Box(modifier = Modifier.padding(16.dp)) {
LineChartView(state)
}
}
@@ -53,5 +51,5 @@ internal fun HTSContentView(state: HTSData, onEvent: (HTSScreenViewEvent) -> Uni
@Preview
@Composable
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 androidx.compose.foundation.layout.Column
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@@ -11,19 +9,21 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
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.viewmodel.HRSViewModel
import no.nordicsemi.android.hrs.viewmodel.HRSViewState
import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.utils.isServiceRunning
@Composable
fun HRSScreen(finishAction: () -> Unit) {
val viewModel: HRSViewModel = hiltViewModel()
val state = viewModel.state.collectAsState().value
val isActive = viewModel.isActive.collectAsState().value
val context = LocalContext.current
LaunchedEffect(state.isScreenActive) {
if (!state.isScreenActive) {
LaunchedEffect(isActive) {
if (!isActive) {
finishAction()
}
if (context.isServiceRunning(HRSService::class.java.name)) {
@@ -43,9 +43,11 @@ fun HRSScreen(finishAction: () -> Unit) {
}
@Composable
private fun HRSView(state: HRSViewState, onEvent: (HRSScreenViewEvent) -> Unit) {
private fun HRSView(state: HRSData, onEvent: (HRSScreenViewEvent) -> Unit) {
Column {
TopAppBar(title = { Text(text = stringResource(id = R.string.hrs_title)) })
BackIconAppBar(stringResource(id = R.string.hrs_title)) {
onEvent(DisconnectEvent)
}
HRSContentView(state) { onEvent(it) }
}

View File

@@ -3,101 +3,72 @@ package no.nordicsemi.android.hrs.view
import android.content.Context
import android.graphics.Color
import android.graphics.DashPathEffect
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxWidth
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.ui.Alignment
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.viewinterop.AndroidView
import androidx.core.content.ContextCompat
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.LineData
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.utils.Utils
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 no.nordicsemi.android.hrs.data.HRSData
import java.util.*
@Composable
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))
}
}
}
private const val X_AXIS_ELEMENTS_COUNT = 40f
@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(
modifier = Modifier
.fillMaxWidth()
.height(300.dp),
factory = { createLineChartView(it, state) },
update = { updateData(state.points, it) }
factory = { createLineChartView(isSystemInDarkTheme, it, items) },
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 {
setBackgroundColor(Color.WHITE)
description.isEnabled = false
setTouchEnabled(true)
legend.isEnabled = false
setTouchEnabled(false)
// setOnChartValueSelectedListener(this)
setDrawGridBackground(false)
isDragEnabled = true
setScaleEnabled(true)
setPinchZoom(true)
setScaleEnabled(false)
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.enableGridDashedLine(10f, 10f, 0f)
axisMinimum = -X_AXIS_ELEMENTS_COUNT
axisMaximum = 0f
setAvoidFirstLastClipping(true)
position = XAxis.XAxisPosition.BOTTOM
}
axisLeft.apply {
enableGridDashedLine(10f, 10f, 0f)
@@ -107,11 +78,9 @@ internal fun createLineChartView(context: Context, state: HRSViewState): LineCha
}
axisRight.isEnabled = false
//---
val entries = state.points.mapIndexed { i, v ->
Entry(i.toFloat(), v.toFloat())
}
val entries = points.mapIndexed { i, v ->
Entry(-i.toFloat(), v.toFloat())
}.reversed()
// create a dataset and give it a type
if (data != null && data.dataSetCount > 0) {
@@ -124,6 +93,7 @@ internal fun createLineChartView(context: Context, state: HRSViewState): LineCha
val set1 = LineDataSet(entries, "DataSet 1")
set1.setDrawIcons(false)
set1.setDrawValues(false)
// draw dashed line
@@ -133,8 +103,13 @@ internal fun createLineChartView(context: Context, state: HRSViewState): LineCha
// black lines and points
// black lines and points
set1.color = Color.BLACK
set1.setCircleColor(Color.BLACK)
if (isDarkTheme) {
set1.color = Color.WHITE
set1.setCircleColor(Color.WHITE)
} else {
set1.color = Color.BLACK
set1.setCircleColor(Color.BLACK)
}
// line thickness and point size
@@ -164,31 +139,9 @@ internal fun createLineChartView(context: Context, state: HRSViewState): LineCha
// draw selection line as dashed
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>()
dataSets.add(set1) // add the data sets
// create a data object with the data sets
// create a data object with the data sets
val data = LineData(dataSets)
@@ -202,8 +155,8 @@ internal fun createLineChartView(context: Context, state: HRSViewState): LineCha
private fun updateData(points: List<Int>, chart: LineChart) {
val entries = points.mapIndexed { i, v ->
Entry(i.toFloat(), v.toFloat())
}
Entry(-i.toFloat(), v.toFloat())
}.reversed()
with(chart) {
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.TemperatureUnit
import no.nordicsemi.android.ble.data.Data
import no.nordicsemi.android.hts.data.HTSDataHolder
import no.nordicsemi.android.log.LogContract
import no.nordicsemi.android.service.BatteryManager
import java.util.*
@@ -41,10 +42,34 @@ private val HT_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A1C-0000-
* enabling indication and reading characteristics. All operations required to connect to device
* 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 val temperatureMeasurementDataCallback = object : TemperatureMeasurementDataCallback() {
override fun onDataReceived(device: BluetoothDevice, data: Data) {
log(
LogContract.Log.Level.APPLICATION,
"\"" + TemperatureMeasurementParser.parse(data) + "\" received"
)
super.onDataReceived(device, data)
}
override fun onTemperatureMeasurementReceived(
device: BluetoothDevice,
temperature: Float,
@TemperatureUnit unit: Int,
calendar: Calendar?,
@TemperatureType type: Int?
) {
dataHolder.setNewTemperature(temperature)
}
}
override fun onBatteryLevelChanged(batteryLevel: Int) {
dataHolder.setBatteryLevel(batteryLevel)
}
override fun getGattCallback(): BatteryManagerGattCallback {
return HTManagerGattCallback()
}
@@ -57,31 +82,7 @@ class HTSManager internal constructor(context: Context) : BatteryManager<HTSMana
override fun initialize() {
super.initialize()
setIndicationCallback(htCharacteristic)
.with(object : TemperatureMeasurementDataCallback() {
override fun onDataReceived(device: BluetoothDevice, data: Data) {
log(
LogContract.Log.Level.APPLICATION,
"\"" + TemperatureMeasurementParser.parse(data) + "\" received"
)
super.onDataReceived(device, data)
}
override fun onTemperatureMeasurementReceived(
device: BluetoothDevice,
temperature: Float,
@TemperatureUnit unit: Int,
calendar: Calendar?,
@TemperatureType type: Int?
) {
mCallbacks!!.onTemperatureMeasurementReceived(
device,
temperature,
unit,
calendar,
type
)
}
})
.with(temperatureMeasurementDataCallback)
enableIndications(htCharacteristic).enqueue()
}

Some files were not shown because too many files have changed in this diff Show More