diff --git a/app/build.gradle b/app/build.gradle index bda0ea86..1bfdfe72 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -52,12 +52,14 @@ dependencies { //https://github.com/google/dagger/issues/2123 implementation project(':profile_bps') implementation project(':profile_csc') + implementation project(':profile_gls') implementation project(':profile_hrs') implementation project(':profile_hts') - implementation project(':profile_gls') + implementation project(':profile_prx') implementation project(':profile_rscs') - implementation project(':profile_permission') implementation project(':profile_scanner') + + implementation project(':lib_permission') implementation project(":lib_theme") implementation project(":lib_utils") 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 e8e10137..a67c99ca 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/HomeScreen.kt @@ -33,6 +33,7 @@ import no.nordicsemi.android.hts.view.HTSScreen import no.nordicsemi.android.permission.view.BluetoothNotAvailableScreen import no.nordicsemi.android.permission.view.BluetoothNotEnabledScreen import no.nordicsemi.android.permission.view.RequestPermissionScreen +import no.nordicsemi.android.prx.view.PRXScreen import no.nordicsemi.android.rscs.view.RSCSScreen import no.nordicsemi.android.scanner.view.ScanDeviceScreen import no.nordicsemi.android.scanner.view.ScanDeviceScreenResult @@ -56,6 +57,7 @@ internal fun HomeScreen() { composable(NavDestination.HTS.id) { HTSScreen { viewModel.navigateUp() } } composable(NavDestination.GLS.id) { GLSScreen { viewModel.navigateUp() } } composable(NavDestination.BPS.id) { BPSScreen { viewModel.navigateUp() } } + composable(NavDestination.PRX.id) { PRXScreen { viewModel.navigateUp() } } composable(NavDestination.RSCS.id) { RSCSScreen { viewModel.navigateUp() } } composable(NavDestination.REQUEST_PERMISSION.id) { RequestPermissionScreen(continueAction) } composable(NavDestination.BLUETOOTH_NOT_AVAILABLE.id) { BluetoothNotAvailableScreen{ viewModel.finish() } } @@ -99,6 +101,8 @@ fun HomeView(callback: (NavDestination) -> Unit) { FeatureButton(R.drawable.ic_bps, R.string.bps_module) { callback(NavDestination.BPS) } Spacer(modifier = Modifier.height(1.dp)) FeatureButton(R.drawable.ic_rscs, R.string.rscs_module) { callback(NavDestination.RSCS) } + Spacer(modifier = Modifier.height(1.dp)) + FeatureButton(R.drawable.ic_proximity, R.string.prx_module) { callback(NavDestination.PRX) } } } 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 9b28ebcf..fdcfecd1 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavDestination.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavDestination.kt @@ -9,6 +9,7 @@ enum class NavDestination(val id: String) { HTS("hts-screen"), GLS("gls-screen"), BPS("bps-screen"), + PRX("prx-screen"), RSCS("rscs-screen"), REQUEST_PERMISSION("request-permission"), BLUETOOTH_NOT_AVAILABLE("bluetooth-not-available"), diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavigationViewModel.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavigationViewModel.kt index 1cbac2ca..a929352d 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavigationViewModel.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/NavigationViewModel.kt @@ -12,6 +12,7 @@ import no.nordicsemi.android.permission.tools.NordicBleScanner import no.nordicsemi.android.permission.tools.PermissionHelper import no.nordicsemi.android.permission.tools.ScannerStatus import no.nordicsemi.android.permission.viewmodel.BluetoothPermissionState +import no.nordicsemi.android.prx.service.IMMEDIATE_ALERT_SERVICE_UUID import no.nordicsemi.android.rscs.service.RSCS_SERVICE_UUID import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder import javax.inject.Inject @@ -77,6 +78,7 @@ class NavigationViewModel @Inject constructor( NavDestination.GLS -> GLS_SERVICE_UUID.toString() NavDestination.BPS -> BPS_SERVICE_UUID.toString() NavDestination.RSCS -> RSCS_SERVICE_UUID.toString() + NavDestination.PRX -> IMMEDIATE_ALERT_SERVICE_UUID.toString() NavDestination.HOME, NavDestination.REQUEST_PERMISSION, NavDestination.BLUETOOTH_NOT_AVAILABLE, diff --git a/app/src/main/res/drawable/ic_proximity.xml b/app/src/main/res/drawable/ic_proximity.xml new file mode 100644 index 00000000..085a6050 --- /dev/null +++ b/app/src/main/res/drawable/ic_proximity.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4ed85901..fa104f0b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,4 +5,5 @@ HTS BPS RSCS + PRX \ No newline at end of file diff --git a/profile_permission/build.gradle b/lib_permission/build.gradle similarity index 100% rename from profile_permission/build.gradle rename to lib_permission/build.gradle diff --git a/profile_permission/src/androidTest/java/no/nordicsemi/android/permission/ExampleInstrumentedTest.kt b/lib_permission/src/androidTest/java/no/nordicsemi/android/permission/ExampleInstrumentedTest.kt similarity index 100% rename from profile_permission/src/androidTest/java/no/nordicsemi/android/permission/ExampleInstrumentedTest.kt rename to lib_permission/src/androidTest/java/no/nordicsemi/android/permission/ExampleInstrumentedTest.kt diff --git a/profile_permission/src/main/AndroidManifest.xml b/lib_permission/src/main/AndroidManifest.xml similarity index 100% rename from profile_permission/src/main/AndroidManifest.xml rename to lib_permission/src/main/AndroidManifest.xml diff --git a/profile_permission/src/main/java/no/nordicsemi/android/permission/HiltModule.kt b/lib_permission/src/main/java/no/nordicsemi/android/permission/HiltModule.kt similarity index 100% rename from profile_permission/src/main/java/no/nordicsemi/android/permission/HiltModule.kt rename to lib_permission/src/main/java/no/nordicsemi/android/permission/HiltModule.kt diff --git a/profile_permission/src/main/java/no/nordicsemi/android/permission/tools/NordicBleScanner.kt b/lib_permission/src/main/java/no/nordicsemi/android/permission/tools/NordicBleScanner.kt similarity index 100% rename from profile_permission/src/main/java/no/nordicsemi/android/permission/tools/NordicBleScanner.kt rename to lib_permission/src/main/java/no/nordicsemi/android/permission/tools/NordicBleScanner.kt diff --git a/profile_permission/src/main/java/no/nordicsemi/android/permission/tools/PermissionHelper.kt b/lib_permission/src/main/java/no/nordicsemi/android/permission/tools/PermissionHelper.kt similarity index 100% rename from profile_permission/src/main/java/no/nordicsemi/android/permission/tools/PermissionHelper.kt rename to lib_permission/src/main/java/no/nordicsemi/android/permission/tools/PermissionHelper.kt diff --git a/profile_permission/src/main/java/no/nordicsemi/android/permission/tools/ScannerStatus.kt b/lib_permission/src/main/java/no/nordicsemi/android/permission/tools/ScannerStatus.kt similarity index 100% rename from profile_permission/src/main/java/no/nordicsemi/android/permission/tools/ScannerStatus.kt rename to lib_permission/src/main/java/no/nordicsemi/android/permission/tools/ScannerStatus.kt diff --git a/profile_permission/src/main/java/no/nordicsemi/android/permission/view/BluetoothNotAvailableScreen.kt b/lib_permission/src/main/java/no/nordicsemi/android/permission/view/BluetoothNotAvailableScreen.kt similarity index 100% rename from profile_permission/src/main/java/no/nordicsemi/android/permission/view/BluetoothNotAvailableScreen.kt rename to lib_permission/src/main/java/no/nordicsemi/android/permission/view/BluetoothNotAvailableScreen.kt diff --git a/profile_permission/src/main/java/no/nordicsemi/android/permission/view/NotConnectedView.kt b/lib_permission/src/main/java/no/nordicsemi/android/permission/view/NotConnectedView.kt similarity index 100% rename from profile_permission/src/main/java/no/nordicsemi/android/permission/view/NotConnectedView.kt rename to lib_permission/src/main/java/no/nordicsemi/android/permission/view/NotConnectedView.kt diff --git a/profile_permission/src/main/java/no/nordicsemi/android/permission/view/RequestPermissionScreen.kt b/lib_permission/src/main/java/no/nordicsemi/android/permission/view/RequestPermissionScreen.kt similarity index 93% rename from profile_permission/src/main/java/no/nordicsemi/android/permission/view/RequestPermissionScreen.kt rename to lib_permission/src/main/java/no/nordicsemi/android/permission/view/RequestPermissionScreen.kt index df68ea9c..6659d36c 100644 --- a/profile_permission/src/main/java/no/nordicsemi/android/permission/view/RequestPermissionScreen.kt +++ b/lib_permission/src/main/java/no/nordicsemi/android/permission/view/RequestPermissionScreen.kt @@ -77,7 +77,10 @@ private fun PermissionNotGranted(onClick: () -> Unit) { verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { - Text(textAlign = TextAlign.Center, text = stringResource(id = R.string.scanner__permission_rationale)) + Text( + textAlign = TextAlign.Center, + text = stringResource(id = R.string.scanner__permission_rationale) + ) Spacer(modifier = Modifier.height(16.dp)) Row { Button(modifier = Modifier.width(100.dp), onClick = { onClick() }) { @@ -102,7 +105,10 @@ private fun PermissionNotAvailable() { verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { - Text(stringResource(id = R.string.scanner__permission_denied)) + Text( + textAlign = TextAlign.Center, + text = stringResource(id = R.string.scanner__permission_denied) + ) Spacer(modifier = Modifier.height(8.dp)) Button(onClick = { openPermissionSettings(context) }) { Text(stringResource(id = R.string.scanner__open_settings)) diff --git a/profile_permission/src/main/java/no/nordicsemi/android/permission/viewmodel/BluetoothPermissionState.kt b/lib_permission/src/main/java/no/nordicsemi/android/permission/viewmodel/BluetoothPermissionState.kt similarity index 100% rename from profile_permission/src/main/java/no/nordicsemi/android/permission/viewmodel/BluetoothPermissionState.kt rename to lib_permission/src/main/java/no/nordicsemi/android/permission/viewmodel/BluetoothPermissionState.kt diff --git a/profile_permission/src/main/res/values/strings.xml b/lib_permission/src/main/res/values/strings.xml similarity index 100% rename from profile_permission/src/main/res/values/strings.xml rename to lib_permission/src/main/res/values/strings.xml diff --git a/profile_permission/src/test/java/no/nordicsemi/android/permission/ExampleUnitTest.kt b/lib_permission/src/test/java/no/nordicsemi/android/permission/ExampleUnitTest.kt similarity index 100% rename from profile_permission/src/test/java/no/nordicsemi/android/permission/ExampleUnitTest.kt rename to lib_permission/src/test/java/no/nordicsemi/android/permission/ExampleUnitTest.kt diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/service/DateTimeParser.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/service/HTSDateTimeParser.kt similarity index 98% rename from profile_hts/src/main/java/no/nordicsemi/android/hts/service/DateTimeParser.kt rename to profile_hts/src/main/java/no/nordicsemi/android/hts/service/HTSDateTimeParser.kt index 9b1d266f..f6d95040 100644 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/service/DateTimeParser.kt +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/service/HTSDateTimeParser.kt @@ -25,7 +25,7 @@ import no.nordicsemi.android.ble.common.callback.DateTimeDataCallback import no.nordicsemi.android.ble.data.Data import java.util.* -object DateTimeParser { +internal object HTSDateTimeParser { /** * Parses the date and time info. * @@ -50,4 +50,4 @@ object DateTimeParser { 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/profile_hts/src/main/java/no/nordicsemi/android/hts/service/HTSManager.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/service/HTSManager.kt index 14b7c7f5..4d8198c3 100644 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/service/HTSManager.kt +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/service/HTSManager.kt @@ -42,7 +42,10 @@ 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, private val dataHolder: HTSDataHolder) : BatteryManager(context) { +internal class HTSManager internal constructor( + context: Context, + private val dataHolder: HTSDataHolder +) : BatteryManager(context) { private var htCharacteristic: BluetoothGattCharacteristic? = null @@ -50,7 +53,7 @@ class HTSManager internal constructor(context: Context, private val dataHolder: override fun onDataReceived(device: BluetoothDevice, data: Data) { log( LogContract.Log.Level.APPLICATION, - "\"" + TemperatureMeasurementParser.parse(data) + "\" received" + "\"" + HTSTemperatureMeasurementParser.parse(data) + "\" received" ) super.onDataReceived(device, data) } diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/service/TemperatureMeasurementParser.java b/profile_hts/src/main/java/no/nordicsemi/android/hts/service/HTSTemperatureMeasurementParser.kt similarity index 55% rename from profile_hts/src/main/java/no/nordicsemi/android/hts/service/TemperatureMeasurementParser.java rename to profile_hts/src/main/java/no/nordicsemi/android/hts/service/HTSTemperatureMeasurementParser.kt index 2d0b5b66..cae099da 100644 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/service/TemperatureMeasurementParser.java +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/service/HTSTemperatureMeasurementParser.kt @@ -19,67 +19,55 @@ * 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; +package no.nordicsemi.android.hts.service -import java.util.Locale; +import no.nordicsemi.android.ble.data.Data +import java.util.* -import no.nordicsemi.android.ble.data.Data; +private const val TEMPERATURE_UNIT_FLAG: Byte = 0x01 // 1 bit +private const val TIMESTAMP_FLAG: Byte = 0x02 // 1 bits +private const val TEMPERATURE_TYPE_FLAG: Byte = 0x04 // 1 bit -@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 +internal object HTSTemperatureMeasurementParser { - public static String parse(final Data data) { - int offset = 0; - final int flags = data.getIntValue(Data.FORMAT_UINT8, offset++); + fun parse(data: Data): String { + var offset = 0 + val 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; + val fahrenheit = flags and TEMPERATURE_UNIT_FLAG.toInt() > 0 - /* + /* * false No Timestamp in the packet * true There is a timestamp information */ - final boolean timestampIncluded = (flags & TIMESTAMP_FLAG) > 0; + val timestampIncluded = flags and TIMESTAMP_FLAG.toInt() > 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(); - } + val temperatureTypeIncluded = flags and TEMPERATURE_TYPE_FLAG.toInt() > 0 + val tempValue = data.getFloatValue(Data.FORMAT_FLOAT, offset)!! + offset += 4 + var dateTime: String? = null + if (timestampIncluded) { + dateTime = HTSDateTimeParser.parse(data, offset) + offset += 7 + } + var type: String? = null + if (temperatureTypeIncluded) { + type = HTSTemperatureTypeParser.parse(data, offset) + // offset++; + } + val builder = 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/profile_hts/src/main/java/no/nordicsemi/android/hts/service/TemperatureTypeParser.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/service/HTSTemperatureTypeParser.kt similarity index 98% rename from profile_hts/src/main/java/no/nordicsemi/android/hts/service/TemperatureTypeParser.kt rename to profile_hts/src/main/java/no/nordicsemi/android/hts/service/HTSTemperatureTypeParser.kt index dc789973..339bfd52 100644 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/service/TemperatureTypeParser.kt +++ b/profile_hts/src/main/java/no/nordicsemi/android/hts/service/HTSTemperatureTypeParser.kt @@ -23,7 +23,7 @@ package no.nordicsemi.android.hts.service import no.nordicsemi.android.ble.data.Data -object TemperatureTypeParser { +internal object HTSTemperatureTypeParser { fun parse(data: Data): String { return parse(data, 0) } diff --git a/profile_prx/build.gradle b/profile_prx/build.gradle new file mode 100644 index 00000000..d397c91b --- /dev/null +++ b/profile_prx/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/profile_prx/src/androidTest/java/no/nordicsemi/android/prx/ExampleInstrumentedTest.kt b/profile_prx/src/androidTest/java/no/nordicsemi/android/prx/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..5e8752f6 --- /dev/null +++ b/profile_prx/src/androidTest/java/no/nordicsemi/android/prx/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package no.nordicsemi.android.prx + +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.prx.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/profile_prx/src/main/AndroidManifest.xml b/profile_prx/src/main/AndroidManifest.xml new file mode 100644 index 00000000..f322bb43 --- /dev/null +++ b/profile_prx/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXData.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXData.kt new file mode 100644 index 00000000..926e6608 --- /dev/null +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXData.kt @@ -0,0 +1,20 @@ +package no.nordicsemi.android.prx.data + +internal data class PRXData( + private val batteryLevel: Int = 0, + private val localAlarmLevel: AlarmLevel = AlarmLevel.NONE, + private val remoteAlarmLevel: Boolean = false +) + +internal enum class AlarmLevel(val value: Int) { + NONE(0x00), + MEDIUM(0x01), + HIGH(0x02); + + companion object { + fun create(value: Int): AlarmLevel { + return AlarmLevel.values().firstOrNull { it.value == value } + ?: throw IllegalArgumentException("Cannot find AlarmLevel for provided value: $value") + } + } +} diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXDataHolder.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXDataHolder.kt new file mode 100644 index 00000000..4f2fab54 --- /dev/null +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXDataHolder.kt @@ -0,0 +1,30 @@ +package no.nordicsemi.android.prx.data + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class PRXDataHolder @Inject constructor() { + + private val _data = MutableStateFlow(PRXData()) + val data: StateFlow = _data + + fun setBatteryLevel(batteryLevel: Int) { + _data.tryEmit(_data.value.copy(batteryLevel = batteryLevel)) + } + + fun setLocalAlarmLevel(value: Int) { + val alarmLevel = AlarmLevel.create(value) + _data.tryEmit(_data.value.copy(localAlarmLevel = alarmLevel)) + } + + fun setRemoteAlarmLevel(isOn: Boolean) { + _data.tryEmit(_data.value.copy(remoteAlarmLevel = isOn)) + } + + fun clear(){ + _data.tryEmit(PRXData()) + } +} diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/service/PRXAlertLevelParser.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/service/PRXAlertLevelParser.kt new file mode 100644 index 00000000..43084db8 --- /dev/null +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/service/PRXAlertLevelParser.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.prx.service + +import android.bluetooth.BluetoothGattCharacteristic +import no.nordicsemi.android.ble.data.Data + +internal object PRXAlertLevelParser { + + fun parse(characteristic: BluetoothGattCharacteristic?): String { + return parse(Data.from(characteristic!!)) + } + + /** + * Parses the alert level. + * + * @param data + * @return alert level in human readable format + */ + fun parse(data: Data): String { + val value = data.getIntValue(Data.FORMAT_UINT8, 0)!! + return when (value) { + 0 -> "No Alert" + 1 -> "Mild Alert" + 2 -> "High Alert" + else -> "Reserved value ($value)" + } + } +} diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/service/PRXManager.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/service/PRXManager.kt new file mode 100644 index 00000000..7b39a5f7 --- /dev/null +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/service/PRXManager.kt @@ -0,0 +1,192 @@ +/* + * 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.prx.service + +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothGatt +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattServer +import android.content.Context +import android.util.Log +import no.nordicsemi.android.ble.callback.FailCallback +import no.nordicsemi.android.ble.common.callback.alert.AlertLevelDataCallback +import no.nordicsemi.android.ble.common.data.alert.AlertLevelData +import no.nordicsemi.android.ble.data.Data +import no.nordicsemi.android.ble.error.GattError +import no.nordicsemi.android.log.LogContract +import no.nordicsemi.android.prx.data.PRXDataHolder +import no.nordicsemi.android.service.BatteryManager +import java.util.* + +/** Link Loss service UUID. */ +val LINK_LOSS_SERVICE_UUID = UUID.fromString("00001803-0000-1000-8000-00805f9b34fb") + +/** Immediate Alert service UUID. */ +val IMMEDIATE_ALERT_SERVICE_UUID = UUID.fromString("00001802-0000-1000-8000-00805f9b34fb") + +/** Alert Level characteristic UUID. */ +val ALERT_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A06-0000-1000-8000-00805f9b34fb") + +internal class PRXManager( + context: Context, + private val dataHolder: PRXDataHolder +) : BatteryManager(context) { + + // Client characteristics. + private var alertLevelCharacteristic: BluetoothGattCharacteristic? = null + private var linkLossCharacteristic: BluetoothGattCharacteristic? = null + + // Server characteristics. + private var localAlertLevelCharacteristic: BluetoothGattCharacteristic? = null + /** + * Returns true if the alert has been enabled on the proximity tag, false otherwise. + */ + /** A flag indicating whether the alarm on the connected proximity tag has been activated. */ + var isAlertEnabled = false + private set + + /** + * BluetoothGatt callbacks for connection/disconnection, service discovery, + * receiving indication, etc. + */ + private inner class ProximityManagerGattCallback : BatteryManagerGattCallback() { + override fun initialize() { + super.initialize() + // This callback will be called whenever local Alert Level char is written + // by a connected proximity tag. + setWriteCallback(localAlertLevelCharacteristic) + .with(object : AlertLevelDataCallback() { + override fun onAlertLevelChanged(device: BluetoothDevice, level: Int) { + dataHolder.setLocalAlarmLevel(level) + } + }) + // After connection, set the Link Loss behaviour on the tag. + writeCharacteristic(linkLossCharacteristic, AlertLevelData.highAlert()) + .done { device: BluetoothDevice? -> + log( + Log.INFO, + "Link loss alert level set" + ) + } + .fail { device: BluetoothDevice?, status: Int -> + log( + Log.WARN, + "Failed to set link loss level: $status" + ) + } + .enqueue() + } + + override fun onServerReady(server: BluetoothGattServer) { + val immediateAlertService = server.getService(IMMEDIATE_ALERT_SERVICE_UUID) + if (immediateAlertService != null) { + localAlertLevelCharacteristic = immediateAlertService.getCharacteristic( + ALERT_LEVEL_CHARACTERISTIC_UUID + ) + } + } + + override fun onServicesInvalidated() { } + + override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean { + val llService = gatt.getService(LINK_LOSS_SERVICE_UUID) + if (llService != null) { + linkLossCharacteristic = + llService.getCharacteristic(ALERT_LEVEL_CHARACTERISTIC_UUID) + } + return linkLossCharacteristic != null + } + + override fun isOptionalServiceSupported(gatt: BluetoothGatt): Boolean { + super.isOptionalServiceSupported(gatt) + val iaService = gatt.getService(IMMEDIATE_ALERT_SERVICE_UUID) + if (iaService != null) { + alertLevelCharacteristic = iaService.getCharacteristic( + ALERT_LEVEL_CHARACTERISTIC_UUID + ) + } + return alertLevelCharacteristic != null + } + + override fun onDeviceDisconnected() { + super.onDeviceDisconnected() + alertLevelCharacteristic = null + linkLossCharacteristic = null + localAlertLevelCharacteristic = null + // Reset the alert flag + isAlertEnabled = false + } + } + + /** + * Toggles the immediate alert on the target device. + */ + fun toggleImmediateAlert() { + writeImmediateAlert(!isAlertEnabled) + } + + /** + * Writes the HIGH ALERT or NO ALERT command to the target device. + * + * @param on true to enable the alarm on proximity tag, false to disable it. + */ + fun writeImmediateAlert(on: Boolean) { + if (!isConnected()) return + writeCharacteristic( + alertLevelCharacteristic, + if (on) AlertLevelData.highAlert() else AlertLevelData.noAlert() + ) + .before { device: BluetoothDevice? -> + log( + Log.VERBOSE, + if (on) "Setting alarm to HIGH..." else "Disabling alarm..." + ) + } + .with { _: BluetoothDevice, data: Data -> + log( + LogContract.Log.Level.APPLICATION, + "\"" + PRXAlertLevelParser.parse(data) + "\" sent" + ) + } + .done { device: BluetoothDevice? -> + isAlertEnabled = on + dataHolder.setRemoteAlarmLevel(on) + } + .fail { device: BluetoothDevice?, status: Int -> + log( + Log.WARN, + if (status == FailCallback.REASON_NULL_ATTRIBUTE) "Alert Level characteristic not found" else GattError.parse( + status + ) + ) + } + .enqueue() + } + + override fun onBatteryLevelChanged(batteryLevel: Int) { + dataHolder.setBatteryLevel(batteryLevel) + } + + override fun getGattCallback(): BleManagerGattCallback { + return ProximityManagerGattCallback() + } +} diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/service/PRXService.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/service/PRXService.kt new file mode 100644 index 00000000..071c542d --- /dev/null +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/service/PRXService.kt @@ -0,0 +1,15 @@ +package no.nordicsemi.android.prx.service + +import dagger.hilt.android.AndroidEntryPoint +import no.nordicsemi.android.prx.data.PRXDataHolder +import no.nordicsemi.android.service.ForegroundBleService +import javax.inject.Inject + +@AndroidEntryPoint +internal class PRXService : ForegroundBleService() { + + @Inject + lateinit var dataHolder: PRXDataHolder + + override val manager: PRXManager by lazy { PRXManager(this, dataHolder) } +} diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXContentView.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXContentView.kt new file mode 100644 index 00000000..0cc33481 --- /dev/null +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXContentView.kt @@ -0,0 +1,11 @@ +package no.nordicsemi.android.prx.view + +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import no.nordicsemi.android.prx.data.PRXData + +@Composable +internal fun ContentView(state: PRXData, onEvent: (PRXScreenViewEvent) -> Unit) { + + Text(text = "aa") +} diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXScreen.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXScreen.kt new file mode 100644 index 00000000..dbf2981f --- /dev/null +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXScreen.kt @@ -0,0 +1,61 @@ +package no.nordicsemi.android.prx.view + +import android.content.Intent +import androidx.compose.foundation.layout.Column +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.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import no.nordicsemi.android.prx.R +import no.nordicsemi.android.prx.data.PRXData +import no.nordicsemi.android.prx.service.PRXService +import no.nordicsemi.android.prx.viewmodel.PRXViewModel +import no.nordicsemi.android.theme.view.BackIconAppBar +import no.nordicsemi.android.utils.isServiceRunning + +@Composable +fun PRXScreen(finishAction: () -> Unit) { + val viewModel: PRXViewModel = hiltViewModel() + val state = viewModel.state.collectAsState().value + val isActive = viewModel.isActive.collectAsState().value + + val context = LocalContext.current + LaunchedEffect(isActive) { + if (!isActive) { + finishAction() + } + if (context.isServiceRunning(PRXService::class.java.name)) { + val intent = Intent(context, PRXService::class.java) + context.stopService(intent) + } + } + + LaunchedEffect("start-service") { + if (!context.isServiceRunning(PRXService::class.java.name)) { + val intent = Intent(context, PRXService::class.java) + context.startService(intent) + } + } + + PRXView(state) { viewModel.onEvent(it) } +} + +@Composable +private fun PRXView(state: PRXData, onEvent: (PRXScreenViewEvent) -> Unit) { + Column { + BackIconAppBar(stringResource(id = R.string.prx_title)) { + onEvent(DisconnectEvent) + } + + ContentView(state) { onEvent(it) } + } +} + +@Preview +@Composable +private fun PRXViewPreview(state: PRXData, onEvent: (PRXScreenViewEvent) -> Unit) { + PRXView(state) { } +} diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXScreenViewEvent.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXScreenViewEvent.kt new file mode 100644 index 00000000..e88ed02e --- /dev/null +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXScreenViewEvent.kt @@ -0,0 +1,5 @@ +package no.nordicsemi.android.prx.view + +internal sealed class PRXScreenViewEvent + +internal object DisconnectEvent : PRXScreenViewEvent() diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/viewmodel/PRXViewModel.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/viewmodel/PRXViewModel.kt new file mode 100644 index 00000000..167a521a --- /dev/null +++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/viewmodel/PRXViewModel.kt @@ -0,0 +1,28 @@ +package no.nordicsemi.android.prx.viewmodel + +import dagger.hilt.android.lifecycle.HiltViewModel +import no.nordicsemi.android.prx.data.PRXDataHolder +import no.nordicsemi.android.prx.view.DisconnectEvent +import no.nordicsemi.android.prx.view.PRXScreenViewEvent +import no.nordicsemi.android.theme.viewmodel.CloseableViewModel +import no.nordicsemi.android.utils.exhaustive +import javax.inject.Inject + +@HiltViewModel +internal class PRXViewModel @Inject constructor( + private val dataHolder: PRXDataHolder +) : CloseableViewModel() { + + val state = dataHolder.data + + fun onEvent(event: PRXScreenViewEvent) { + when (event) { + DisconnectEvent -> onDisconnectButtonClick() + }.exhaustive + } + + private fun onDisconnectButtonClick() { + finish() + dataHolder.clear() + } +} diff --git a/profile_prx/src/main/res/values/strings.xml b/profile_prx/src/main/res/values/strings.xml new file mode 100644 index 00000000..6d3258a2 --- /dev/null +++ b/profile_prx/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Proximity + diff --git a/profile_prx/src/test/java/no/nordicsemi/android/prx/ExampleUnitTest.kt b/profile_prx/src/test/java/no/nordicsemi/android/prx/ExampleUnitTest.kt new file mode 100644 index 00000000..774842bf --- /dev/null +++ b/profile_prx/src/test/java/no/nordicsemi/android/prx/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package no.nordicsemi.android.prx + +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/settings.gradle b/settings.gradle index b361e1f2..a75b3116 100644 --- a/settings.gradle +++ b/settings.gradle @@ -67,9 +67,11 @@ include ':profile_csc' include ':profile_gls' include ':profile_hrs' include ':profile_hts' +include ':profile_prx' include ':profile_rscs' -include ':profile_permission' +include ':profile_scanner' +include ':lib_permission' include ':lib_service' include ':lib_theme' include ':lib_utils' @@ -81,4 +83,3 @@ if (file('../Android-BLE-Library').exists()) { if (file('../Android-Scanner-Compat-Library').exists()) { includeBuild('../Android-Scanner-Compat-Library') } -include ':profile_scanner'