diff --git a/app/build.gradle b/app/build.gradle index 1f21139f..5f9029ee 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -52,6 +52,7 @@ dependencies { //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(":lib_theme") diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt index 79456427..aca4b7b3 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt @@ -22,6 +22,7 @@ import androidx.navigation.compose.rememberNavController import no.nordicsemi.android.csc.view.CscScreen import no.nordicsemi.android.gls.view.GLSScreen import no.nordicsemi.android.hrs.view.HRSScreen +import no.nordicsemi.android.hts.view.HTSScreen import no.nordicsemi.android.scanner.view.BluetoothNotAvailableScreen import no.nordicsemi.android.scanner.view.BluetoothNotEnabledScreen import no.nordicsemi.android.scanner.view.RequestPermissionScreen @@ -43,6 +44,7 @@ internal fun HomeScreen() { composable(NavDestination.HOME.id) { HomeView { viewModel.navigate(it) } } composable(NavDestination.CSC.id) { CscScreen { viewModel.navigateUp() } } composable(NavDestination.HRS.id) { HRSScreen { viewModel.navigateUp() } } + composable(NavDestination.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() } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavDestination.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavDestination.kt index 5820a21f..c6513cfc 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavDestination.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavDestination.kt @@ -4,6 +4,7 @@ enum class NavDestination(val id: String) { HOME("home-screen"), CSC("csc-screen"), HRS("hrs-screen"), + HTS("hts-screen"), GLS("gls-screen"), REQUEST_PERMISSION("request-permission"), BLUETOOTH_NOT_AVAILABLE("bluetooth-not-available"), diff --git a/app/src/main/res/drawable/ic_hts.xml b/app/src/main/res/drawable/ic_hts.xml new file mode 100644 index 00000000..b575adf5 --- /dev/null +++ b/app/src/main/res/drawable/ic_hts.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/data/CSCData.kt b/feature_csc/src/main/java/no/nordicsemi/android/csc/data/CSCData.kt index 974acd0f..ccc3727e 100644 --- a/feature_csc/src/main/java/no/nordicsemi/android/csc/data/CSCData.kt +++ b/feature_csc/src/main/java/no/nordicsemi/android/csc/data/CSCData.kt @@ -55,4 +55,8 @@ internal data class CSCData( fun displayGearRatio(): String { return String.format(Locale.US, "%.1f", gearRatio) } + + fun items(): List<> { + + } } diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/ConnectedView.kt b/feature_csc/src/main/java/no/nordicsemi/android/csc/view/CSCConnectedView.kt similarity index 82% rename from feature_csc/src/main/java/no/nordicsemi/android/csc/view/ConnectedView.kt rename to feature_csc/src/main/java/no/nordicsemi/android/csc/view/CSCConnectedView.kt index ff755728..48a3dccc 100644 --- a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/ConnectedView.kt +++ b/feature_csc/src/main/java/no/nordicsemi/android/csc/view/CSCConnectedView.kt @@ -4,10 +4,8 @@ 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.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 @@ -18,10 +16,10 @@ 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.NordicColors +import no.nordicsemi.android.theme.view.SensorRecordCard @Composable -internal fun ContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) { +internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) { if (state.showDialog) { SelectWheelSizeDialog { onEvent(it) } } @@ -49,11 +47,7 @@ internal fun ContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) { @Composable private fun SettingsSection(state: CSCData, onEvent: (CSCViewEvent) -> Unit) { - Card( - backgroundColor = NordicColors.NordicGray4.value(), - shape = RoundedCornerShape(10.dp), - elevation = 0.dp - ) { + SensorRecordCard { Column( modifier = Modifier.padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally @@ -70,5 +64,5 @@ private fun SettingsSection(state: CSCData, onEvent: (CSCViewEvent) -> Unit) { @Preview @Composable private fun ConnectedPreview() { - ContentView(CSCData()) { } + CSCContentView(CSCData()) { } } diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/CscScreen.kt b/feature_csc/src/main/java/no/nordicsemi/android/csc/view/CscScreen.kt index e1431aa3..558d50ac 100644 --- a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/CscScreen.kt +++ b/feature_csc/src/main/java/no/nordicsemi/android/csc/view/CscScreen.kt @@ -47,6 +47,6 @@ private fun CSCView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) { Column { TopAppBar(title = { Text(text = stringResource(id = R.string.csc_title)) }) - ContentView(state) { onEvent(it) } + CSCContentView(state) { onEvent(it) } } } diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/SensorsReadingView.kt b/feature_csc/src/main/java/no/nordicsemi/android/csc/view/SensorsReadingView.kt index 3abd742f..f37a793a 100644 --- a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/SensorsReadingView.kt +++ b/feature_csc/src/main/java/no/nordicsemi/android/csc/view/SensorsReadingView.kt @@ -4,8 +4,6 @@ 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.foundation.shape.RoundedCornerShape -import androidx.compose.material.Card import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -13,17 +11,13 @@ 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.NordicColors import no.nordicsemi.android.theme.view.BatteryLevelView import no.nordicsemi.android.theme.view.KeyValueField +import no.nordicsemi.android.theme.view.SensorRecordCard @Composable internal fun SensorsReadingView(state: CSCData) { - Card( - backgroundColor = NordicColors.NordicGray4.value(), - shape = RoundedCornerShape(10.dp), - elevation = 0.dp - ) { + SensorRecordCard { Column(modifier = Modifier.padding(16.dp)) { KeyValueField(stringResource(id = R.string.scs_field_speed), state.displaySpeed()) Spacer(modifier = Modifier.height(4.dp)) diff --git a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/SpeedUnitRadioGroup.kt b/feature_csc/src/main/java/no/nordicsemi/android/csc/view/SpeedUnitRadioGroup.kt index 36b51e83..964fe107 100644 --- a/feature_csc/src/main/java/no/nordicsemi/android/csc/view/SpeedUnitRadioGroup.kt +++ b/feature_csc/src/main/java/no/nordicsemi/android/csc/view/SpeedUnitRadioGroup.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material.RadioButton import androidx.compose.material.Text @@ -14,32 +13,33 @@ import androidx.compose.ui.unit.dp @Composable internal fun SpeedUnitRadioGroup( - currentUnit: SpeedUnit, - onEvent: (OnSelectedSpeedUnitSelected) -> Unit + currentItem: RadioGroupItem, + items: List, + onEvent: (RadioGroupItem) -> Unit ) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly ) { - SpeedUnitRadioButton(currentUnit, SpeedUnit.KM_H, onEvent) - SpeedUnitRadioButton(currentUnit, SpeedUnit.MPH, onEvent) - SpeedUnitRadioButton(currentUnit, SpeedUnit.M_S, onEvent) + items.forEach { + SpeedUnitRadioButton(currentItem, it, onEvent) + } } } @Composable internal fun SpeedUnitRadioButton( - selectedUnit: SpeedUnit, - displayedUnit: SpeedUnit, - onEvent: (OnSelectedSpeedUnitSelected) -> Unit + selectedItem: RadioGroupItem, + displayedItem: RadioGroupItem, + onEvent: (RadioGroupItem) -> Unit ) { Row { RadioButton( - selected = (selectedUnit == displayedUnit), - onClick = { onEvent(OnSelectedSpeedUnitSelected(displayedUnit)) } + selected = (selectedItem == displayedItem), + onClick = { onEvent(displayedItem) } ) Spacer(modifier = Modifier.width(4.dp)) - Text(text = createSpeedUnitLabel(displayedUnit)) + Text(text = displayedItem.label) } } @@ -50,3 +50,5 @@ internal fun createSpeedUnitLabel(unit: SpeedUnit): String { SpeedUnit.MPH -> "mph" } } + +data class RadioGroupItem(val label: String) diff --git a/feature_gls/src/main/java/no/nordicsemi/android/gls/data/GLSData.kt b/feature_gls/src/main/java/no/nordicsemi/android/gls/data/GLSData.kt index d5abc761..7b429a39 100644 --- a/feature_gls/src/main/java/no/nordicsemi/android/gls/data/GLSData.kt +++ b/feature_gls/src/main/java/no/nordicsemi/android/gls/data/GLSData.kt @@ -3,7 +3,8 @@ package no.nordicsemi.android.gls.data internal data class GLSData( val record: List = emptyList(), val batteryLevel: Int = 0, - val requestStatus: RequestStatus = RequestStatus.IDLE + val requestStatus: RequestStatus = RequestStatus.IDLE, + val isDeviceBonded: Boolean = false ) internal enum class RequestStatus { diff --git a/feature_gls/src/main/java/no/nordicsemi/android/gls/view/GLSScreen.kt b/feature_gls/src/main/java/no/nordicsemi/android/gls/view/GLSScreen.kt index 96071a8a..17f4a35b 100644 --- a/feature_gls/src/main/java/no/nordicsemi/android/gls/view/GLSScreen.kt +++ b/feature_gls/src/main/java/no/nordicsemi/android/gls/view/GLSScreen.kt @@ -4,6 +4,7 @@ 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 import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel @@ -16,6 +17,10 @@ import no.nordicsemi.android.gls.viewmodel.GLSViewModel fun GLSScreen(finishAction: () -> Unit) { val viewModel: GLSViewModel = hiltViewModel() val state = viewModel.state.collectAsState().value + + LaunchedEffect(state.isDeviceBonded) { +// viewModel.bondDevice() + } } @Composable diff --git a/feature_gls/src/main/java/no/nordicsemi/android/gls/viewmodel/GLSViewModel.kt b/feature_gls/src/main/java/no/nordicsemi/android/gls/viewmodel/GLSViewModel.kt index 8a566bc0..11acfcb7 100644 --- a/feature_gls/src/main/java/no/nordicsemi/android/gls/viewmodel/GLSViewModel.kt +++ b/feature_gls/src/main/java/no/nordicsemi/android/gls/viewmodel/GLSViewModel.kt @@ -15,6 +15,10 @@ internal class GLSViewModel @Inject constructor( val state = glsManager.data fun bondDevice() { - + if (deviceHolder.isDeviceBonded()) { + deviceHolder.bondDevice() + } else { + //start work + } } } diff --git a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/service/BodySensorLocationParser.kt b/feature_hrs/src/main/java/no/nordicsemi/android/hrs/service/BodySensorLocationParser.kt index 6b1d706a..10a68c76 100644 --- a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/service/BodySensorLocationParser.kt +++ b/feature_hrs/src/main/java/no/nordicsemi/android/hrs/service/BodySensorLocationParser.kt @@ -23,7 +23,7 @@ package no.nordicsemi.android.hrs.service import no.nordicsemi.android.ble.data.Data -object BodySensorLocationParser { +internal object BodySensorLocationParser { fun parse(data: Data): String { val value = data.getIntValue(Data.FORMAT_UINT8, 0)!! return when (value) { @@ -37,4 +37,4 @@ object BodySensorLocationParser { else -> "Other" } } -} \ No newline at end of file +} diff --git a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/service/HeartRateMeasurementParser.kt b/feature_hrs/src/main/java/no/nordicsemi/android/hrs/service/HeartRateMeasurementParser.kt index c69ad1c5..46e0729a 100644 --- a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/service/HeartRateMeasurementParser.kt +++ b/feature_hrs/src/main/java/no/nordicsemi/android/hrs/service/HeartRateMeasurementParser.kt @@ -24,7 +24,7 @@ package no.nordicsemi.android.hrs.service import no.nordicsemi.android.ble.data.Data import java.util.* -object HeartRateMeasurementParser { +internal object HeartRateMeasurementParser { private const val HEART_RATE_VALUE_FORMAT: Byte = 0x01 // 1 bit private const val SENSOR_CONTACT_STATUS: Byte = 0x06 // 2 bits diff --git a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/view/ContentView.kt b/feature_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSContentView.kt similarity index 97% rename from feature_hrs/src/main/java/no/nordicsemi/android/hrs/view/ContentView.kt rename to feature_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSContentView.kt index a4122d9a..1ca046aa 100644 --- a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/view/ContentView.kt +++ b/feature_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSContentView.kt @@ -38,7 +38,7 @@ import no.nordicsemi.android.theme.view.BatteryLevelView import java.util.* @Composable -internal fun ContentView(state: HRSViewState, onEvent: (HRSScreenViewEvent) -> Unit) { +internal fun HRSContentView(state: HRSViewState, onEvent: (HRSScreenViewEvent) -> Unit) { Column( modifier = Modifier .fillMaxSize() @@ -220,5 +220,5 @@ private fun updateData(points: List, chart: LineChart) { @Preview @Composable private fun Preview() { - ContentView(state = HRSViewState()) { } + HRSContentView(state = HRSViewState()) { } } diff --git a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreen.kt b/feature_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreen.kt index a6dc4d2a..6cfea08e 100644 --- a/feature_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreen.kt +++ b/feature_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreen.kt @@ -47,6 +47,6 @@ private fun HRSView(state: HRSViewState, onEvent: (HRSScreenViewEvent) -> Unit) Column { TopAppBar(title = { Text(text = stringResource(id = R.string.hrs_title)) }) - ContentView(state) { onEvent(it) } + HRSContentView(state) { onEvent(it) } } } diff --git a/feature_hts/build.gradle b/feature_hts/build.gradle new file mode 100644 index 00000000..d397c91b --- /dev/null +++ b/feature_hts/build.gradle @@ -0,0 +1,26 @@ +apply from: rootProject.file("library.gradle") +apply plugin: 'kotlin-parcelize' + +dependencies { + implementation project(":lib_service") + implementation project(":lib_theme") + implementation project(":lib_utils") + + implementation libs.nordic.ble.common + + implementation libs.nordic.log + + implementation libs.bundles.compose + implementation libs.androidx.core + implementation libs.material + implementation libs.lifecycle.activity + implementation libs.lifecycle.service + implementation libs.compose.lifecycle + implementation libs.compose.activity + + testImplementation libs.test.junit + androidTestImplementation libs.android.test.junit + androidTestImplementation libs.android.test.espresso + androidTestImplementation libs.android.test.compose.ui + debugImplementation libs.android.test.compose.tooling +} diff --git a/feature_hts/src/androidTest/java/no/nordicsemi/android/hts/ExampleInstrumentedTest.kt b/feature_hts/src/androidTest/java/no/nordicsemi/android/hts/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..6b416079 --- /dev/null +++ b/feature_hts/src/androidTest/java/no/nordicsemi/android/hts/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package no.nordicsemi.android.hts + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("no.nordicsemi.android.hts.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/feature_hts/src/main/AndroidManifest.xml b/feature_hts/src/main/AndroidManifest.xml new file mode 100644 index 00000000..f102f082 --- /dev/null +++ b/feature_hts/src/main/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/feature_hts/src/main/java/no/nordicsemi/android/hts/data/HTSData.kt b/feature_hts/src/main/java/no/nordicsemi/android/hts/data/HTSData.kt new file mode 100644 index 00000000..bd7a5c19 --- /dev/null +++ b/feature_hts/src/main/java/no/nordicsemi/android/hts/data/HTSData.kt @@ -0,0 +1,22 @@ +package no.nordicsemi.android.hts.data + +internal data class HTSData( + val heartRates: List = 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 +} diff --git a/feature_hts/src/main/java/no/nordicsemi/android/hts/service/DateTimeParser.kt b/feature_hts/src/main/java/no/nordicsemi/android/hts/service/DateTimeParser.kt new file mode 100644 index 00000000..9b1d266f --- /dev/null +++ b/feature_hts/src/main/java/no/nordicsemi/android/hts/service/DateTimeParser.kt @@ -0,0 +1,53 @@ +/* + * 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.callback.DateTimeDataCallback +import no.nordicsemi.android.ble.data.Data +import java.util.* + +object DateTimeParser { + /** + * Parses the date and time info. + * + * @param data + * @return time in human readable format + */ + fun parse(data: Data): String { + return parse(data, 0) + } + + /** + * Parses the date and time info. This data has 7 bytes + * + * @param data + * @param offset + * offset to start reading the time + * @return time in human readable format + */ + /* package */ + @JvmStatic + fun parse(data: Data, offset: Int): String { + val calendar = DateTimeDataCallback.readDateTime(data, offset) + return String.format(Locale.US, "%1\$te %1\$tb %1\$tY, %1\$tH:%1\$tM:%1\$tS", calendar) + } +} \ No newline at end of file diff --git a/feature_hts/src/main/java/no/nordicsemi/android/hts/service/HTSDataBroadcast.kt b/feature_hts/src/main/java/no/nordicsemi/android/hts/service/HTSDataBroadcast.kt new file mode 100644 index 00000000..feaafcbd --- /dev/null +++ b/feature_hts/src/main/java/no/nordicsemi/android/hts/service/HTSDataBroadcast.kt @@ -0,0 +1,9 @@ +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() diff --git a/feature_hts/src/main/java/no/nordicsemi/android/hts/service/HTSManager.kt b/feature_hts/src/main/java/no/nordicsemi/android/hts/service/HTSManager.kt new file mode 100644 index 00000000..ba78d138 --- /dev/null +++ b/feature_hts/src/main/java/no/nordicsemi/android/hts/service/HTSManager.kt @@ -0,0 +1,103 @@ +/* + * 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 android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCharacteristic +import android.content.Context +import no.nordicsemi.android.ble.common.callback.ht.TemperatureMeasurementDataCallback +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.log.LogContract +import no.nordicsemi.android.service.BatteryManager +import java.util.* + +private val HT_SERVICE_UUID = UUID.fromString("00001809-0000-1000-8000-00805f9b34fb") +private val HT_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb") + +/** + * [HTSManager] class performs [BluetoothGatt] operations for connection, service discovery, + * 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(context) { + + private var htCharacteristic: BluetoothGattCharacteristic? = null + + override fun getGattCallback(): BatteryManagerGattCallback { + return HTManagerGattCallback() + } + + /** + * BluetoothGatt callbacks for connection/disconnection, service discovery, + * receiving indication, etc.. + */ + private inner class HTManagerGattCallback : BatteryManagerGattCallback() { + override fun initialize() { + super.initialize() + setIndicationCallback(htCharacteristic) + .with(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 + ) + } + }) + enableIndications(htCharacteristic).enqueue() + } + + override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean { + val service = gatt.getService(HT_SERVICE_UUID) + if (service != null) { + htCharacteristic = service.getCharacteristic(HT_MEASUREMENT_CHARACTERISTIC_UUID) + } + return htCharacteristic != null + } + + override fun onDeviceDisconnected() { + super.onDeviceDisconnected() + htCharacteristic = null + } + + override fun onServicesInvalidated() {} + } +} diff --git a/feature_hts/src/main/java/no/nordicsemi/android/hts/service/HTSManagerCallbacks.kt b/feature_hts/src/main/java/no/nordicsemi/android/hts/service/HTSManagerCallbacks.kt new file mode 100644 index 00000000..821cf28c --- /dev/null +++ b/feature_hts/src/main/java/no/nordicsemi/android/hts/service/HTSManagerCallbacks.kt @@ -0,0 +1,31 @@ +/* + * 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 diff --git a/feature_hts/src/main/java/no/nordicsemi/android/hts/service/HTSService.kt b/feature_hts/src/main/java/no/nordicsemi/android/hts/service/HTSService.kt new file mode 100644 index 00000000..8fd638a4 --- /dev/null +++ b/feature_hts/src/main/java/no/nordicsemi/android/hts/service/HTSService.kt @@ -0,0 +1,49 @@ +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(), HTSManagerCallbacks { + + private var data = HTSData() + private val points = mutableListOf() + + @Inject + lateinit var localBroadcast: HTSDataBroadcast + + override val manager: HTSManager by lazy { + HTSManager(this).apply { + setGattCallbacks(this@HTSService) + } + } + + override fun initializeManager(): LoggableBleManager { + 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) + } +} diff --git a/feature_hts/src/main/java/no/nordicsemi/android/hts/service/TemperatureMeasurementParser.java b/feature_hts/src/main/java/no/nordicsemi/android/hts/service/TemperatureMeasurementParser.java new file mode 100644 index 00000000..2d0b5b66 --- /dev/null +++ b/feature_hts/src/main/java/no/nordicsemi/android/hts/service/TemperatureMeasurementParser.java @@ -0,0 +1,85 @@ +/* + * 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 java.util.Locale; + +import no.nordicsemi.android.ble.data.Data; + +@SuppressWarnings("ConstantConditions") +public class TemperatureMeasurementParser { + private static final byte TEMPERATURE_UNIT_FLAG = 0x01; // 1 bit + private static final byte TIMESTAMP_FLAG = 0x02; // 1 bits + private static final byte TEMPERATURE_TYPE_FLAG = 0x04; // 1 bit + + public static String parse(final Data data) { + int offset = 0; + final int flags = data.getIntValue(Data.FORMAT_UINT8, offset++); + + /* + * false Temperature is in Celsius degrees + * true Temperature is in Fahrenheit degrees + */ + final boolean fahrenheit = (flags & TEMPERATURE_UNIT_FLAG) > 0; + + /* + * false No Timestamp in the packet + * true There is a timestamp information + */ + final boolean timestampIncluded = (flags & TIMESTAMP_FLAG) > 0; + + /* + * false Temperature type is not included + * true Temperature type included in the packet + */ + final boolean temperatureTypeIncluded = (flags & TEMPERATURE_TYPE_FLAG) > 0; + + final float tempValue = data.getFloatValue(Data.FORMAT_FLOAT, offset); + offset += 4; + + String dateTime = null; + if (timestampIncluded) { + dateTime = DateTimeParser.parse(data, offset); + offset += 7; + } + + String type = null; + if (temperatureTypeIncluded) { + type = TemperatureTypeParser.parse(data, offset); + // offset++; + } + + final StringBuilder builder = new StringBuilder(); + builder.append(String.format(Locale.US, "%.02f", tempValue)); + + if (fahrenheit) + builder.append("°F"); + else + builder.append("°C"); + + if (timestampIncluded) + builder.append("\nTime: ").append(dateTime); + if (temperatureTypeIncluded) + builder.append("\nType: ").append(type); + return builder.toString(); + } +} diff --git a/feature_hts/src/main/java/no/nordicsemi/android/hts/service/TemperatureTypeParser.kt b/feature_hts/src/main/java/no/nordicsemi/android/hts/service/TemperatureTypeParser.kt new file mode 100644 index 00000000..be841f02 --- /dev/null +++ b/feature_hts/src/main/java/no/nordicsemi/android/hts/service/TemperatureTypeParser.kt @@ -0,0 +1,48 @@ +/* + * 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.data.Data + +object TemperatureTypeParser { + fun parse(data: Data): String { + return parse(data, 0) + } + + /* package */ + @JvmStatic + fun parse(data: Data, offset: Int): String { + val type = data.value!![offset].toInt() + return when (type) { + 1 -> "Armpit" + 2 -> "Body (general)" + 3 -> "Ear (usually ear lobe)" + 4 -> "Finger" + 5 -> "Gastro-intestinal Tract" + 6 -> "Mouth" + 7 -> "Rectum" + 8 -> "Toe" + 9 -> "Tympanum (ear drum)" + else -> "Unknown" + } + } +} diff --git a/feature_hts/src/main/java/no/nordicsemi/android/hts/view/HTSContentView.kt b/feature_hts/src/main/java/no/nordicsemi/android/hts/view/HTSContentView.kt new file mode 100644 index 00000000..74e9befc --- /dev/null +++ b/feature_hts/src/main/java/no/nordicsemi/android/hts/view/HTSContentView.kt @@ -0,0 +1,57 @@ +package no.nordicsemi.android.hts.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 +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.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.theme.view.BatteryLevelView +import no.nordicsemi.android.theme.view.SensorRecordCard + +@Composable +internal fun HTSContentView(state: HTSData, onEvent: (HTSScreenViewEvent) -> Unit) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + SensorRecordCard { + Box(modifier = Modifier.padding(16.dp)) { + + } + } + + 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)) + } + } +} + +@Preview +@Composable +private fun Preview() { + HTSContentView(state = HTSData()) { } +} diff --git a/feature_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreen.kt b/feature_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreen.kt new file mode 100644 index 00000000..8f0074cc --- /dev/null +++ b/feature_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreen.kt @@ -0,0 +1,52 @@ +package no.nordicsemi.android.hts.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 +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import no.nordicsemi.android.hts.R +import no.nordicsemi.android.hts.data.HTSData +import no.nordicsemi.android.hts.service.HTSService +import no.nordicsemi.android.hts.viewmodel.HTSViewModel +import no.nordicsemi.android.utils.isServiceRunning + +@Composable +fun HTSScreen(finishAction: () -> Unit) { + val viewModel: HTSViewModel = hiltViewModel() + val state = viewModel.state.collectAsState().value + + val context = LocalContext.current + LaunchedEffect(state.isScreenActive) { + if (!state.isScreenActive) { + finishAction() + } + if (context.isServiceRunning(HTSService::class.java.name)) { + val intent = Intent(context, HTSService::class.java) + context.stopService(intent) + } + } + + LaunchedEffect("start-service") { + if (!context.isServiceRunning(HTSService::class.java.name)) { + val intent = Intent(context, HTSService::class.java) + context.startService(intent) + } + } + + HRSView(state) { viewModel.onEvent(it) } +} + +@Composable +private fun HRSView(state: HTSData, onEvent: (HTSScreenViewEvent) -> Unit) { + Column { + TopAppBar(title = { Text(text = stringResource(id = R.string.hts_title)) }) + + HTSContentView(state) { onEvent(it) } + } +} diff --git a/feature_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreenViewEvent.kt b/feature_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreenViewEvent.kt new file mode 100644 index 00000000..8c826645 --- /dev/null +++ b/feature_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreenViewEvent.kt @@ -0,0 +1,5 @@ +package no.nordicsemi.android.hts.view + +sealed class HTSScreenViewEvent + +object DisconnectEvent : HTSScreenViewEvent() diff --git a/feature_hts/src/main/java/no/nordicsemi/android/hts/viewmodel/HRSViewModel.kt b/feature_hts/src/main/java/no/nordicsemi/android/hts/viewmodel/HRSViewModel.kt new file mode 100644 index 00000000..918e938d --- /dev/null +++ b/feature_hts/src/main/java/no/nordicsemi/android/hts/viewmodel/HRSViewModel.kt @@ -0,0 +1,47 @@ +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)) + } +} diff --git a/feature_hts/src/main/res/values/strings.xml b/feature_hts/src/main/res/values/strings.xml new file mode 100644 index 00000000..cf6cda54 --- /dev/null +++ b/feature_hts/src/main/res/values/strings.xml @@ -0,0 +1,8 @@ + + + HTS + + %.1f °C + %.1f °F + %.1f °K + diff --git a/feature_hts/src/test/java/no/nordicsemi/android/hts/ExampleUnitTest.kt b/feature_hts/src/test/java/no/nordicsemi/android/hts/ExampleUnitTest.kt new file mode 100644 index 00000000..67ea2c81 --- /dev/null +++ b/feature_hts/src/test/java/no/nordicsemi/android/hts/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package no.nordicsemi.android.hts + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/SelectedBluetoothDeviceHolder.kt b/lib_service/src/main/java/no/nordicsemi/android/service/SelectedBluetoothDeviceHolder.kt index c1cc9747..645ae68e 100644 --- a/lib_service/src/main/java/no/nordicsemi/android/service/SelectedBluetoothDeviceHolder.kt +++ b/lib_service/src/main/java/no/nordicsemi/android/service/SelectedBluetoothDeviceHolder.kt @@ -16,13 +16,11 @@ class SelectedBluetoothDeviceHolder constructor( return deviceManager.associations.firstOrNull()?.let { bluetoothAdapter?.getRemoteDevice(it) } } - //TODO: Check if starts automatically + fun isDeviceBonded(): Boolean { + return device?.bondState == BluetoothDevice.BOND_NONE + } fun bondDevice() { - device?.let { - if (it.bondState == BluetoothDevice.BOND_NONE) { - it.createBond() - } - } + device?.createBond() } fun forgetDevice() { diff --git a/lib_theme/src/main/java/no/nordicsemi/android/theme/view/SensorRecordCard.kt b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/SensorRecordCard.kt new file mode 100644 index 00000000..c5c02880 --- /dev/null +++ b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/SensorRecordCard.kt @@ -0,0 +1,18 @@ +package no.nordicsemi.android.theme.view + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.theme.NordicColors + +@Composable +fun SensorRecordCard(content: @Composable () -> Unit) { + Card( + backgroundColor = NordicColors.NordicGray4.value(), + shape = RoundedCornerShape(10.dp), + elevation = 0.dp + ) { + content() + } +} diff --git a/settings.gradle b/settings.gradle index ec304a49..b4b29012 100644 --- a/settings.gradle +++ b/settings.gradle @@ -64,6 +64,7 @@ include ':app' include ':feature_csc' include ':feature_gls' include ':feature_hrs' +include ':feature_hts' include ':feature_scanner' include ':lib_service'