mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2026-01-03 23:04:20 +01:00
Add PRX profile
This commit is contained in:
@@ -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")
|
||||
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
24
app/src/main/res/drawable/ic_proximity.xml
Normal file
24
app/src/main/res/drawable/ic_proximity.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="80dp"
|
||||
android:height="80dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:fillColor="#00B3DC"
|
||||
android:pathData="M176,180.1m-70.2,0a70.2,70.2 0,1 1,140.4 0a70.2,70.2 0,1 1,-140.4 0" />
|
||||
<path
|
||||
android:fillColor="#00B3DC"
|
||||
android:pathData="M152.7,433.7c-13.6,0 -25.7,-9.8 -28.1,-23.7c-2.7,-15.5 7.7,-30.3 23.3,-32.9c73.8,-12.7 123.5,-35.2 161.3,-72.9c44.4,-44.4 60.8,-101.4 71.1,-151.3c3.2,-15.4 18.2,-25.4 33.6,-22.2c15.4,3.2 25.4,18.2 22.2,33.6c-7.7,37.7 -16.8,67.8 -28.6,94.5c-14.8,33.4 -33.7,61.4 -58,85.7c-59.7,59.7 -133.9,78.8 -191.9,88.8C155.9,433.6 154.3,433.7 152.7,433.7z" />
|
||||
<path
|
||||
android:fillColor="#00B3DC"
|
||||
android:pathData="M154.8,584.1c-0.8,0 -1.7,0 -2.5,0c-15.7,-0.1 -28.4,-12.9 -28.4,-28.7c0.1,-15.7 12.8,-28.4 28.5,-28.4c0.1,0 0.1,0 0.2,0c0.7,0 1.5,0 2.2,0c104,0 199.4,-38.2 268.9,-107.6c67.6,-67.6 105.8,-160.4 107.5,-261.4c0.3,-15.6 13,-28 28.5,-28c0.2,0 0.3,0 0.5,0c15.7,0.3 28.3,13.3 28,29c-1,56.7 -11.9,111 -32.3,161.6c-21.2,52.5 -52.2,99.3 -92,139.1C383.8,540 274.1,584.1 154.8,584.1z" />
|
||||
<path
|
||||
android:fillColor="#00B3DC"
|
||||
android:pathData="M848,843.9m-70.2,0a70.2,70.2 0,1 1,140.4 0a70.2,70.2 0,1 1,-140.4 0" />
|
||||
<path
|
||||
android:fillColor="#00B3DC"
|
||||
android:pathData="M615.9,893.9c-1.9,0 -3.8,-0.2 -5.8,-0.6c-15.4,-3.2 -25.4,-18.2 -22.2,-33.6c7.7,-37.7 16.8,-67.8 28.6,-94.5c14.8,-33.4 33.7,-61.5 58,-85.7c59.7,-59.7 133.9,-78.8 191.9,-88.8c15.5,-2.7 30.3,7.7 32.9,23.3c2.7,15.5 -7.7,30.3 -23.3,32.9c-73.8,12.7 -123.5,35.2 -161.3,72.9c-44.4,44.4 -60.8,101.4 -71.1,151.3C641,884.6 629.2,893.9 615.9,893.9z" />
|
||||
<path
|
||||
android:fillColor="#00B3DC"
|
||||
android:pathData="M464.3,893.9c-0.2,0 -0.3,0 -0.5,0c-15.7,-0.3 -28.3,-13.3 -28,-29c1,-56.7 11.9,-111 32.3,-161.6c21.2,-52.5 52.2,-99.3 92,-139.1c80.8,-80.8 191.5,-124.9 311.7,-124.3c15.7,0.1 28.4,12.9 28.4,28.7c-0.1,15.7 -12.8,28.4 -28.5,28.4c-0.1,0 -0.1,0 -0.2,0c-104.9,-0.5 -201.2,37.7 -271.1,107.6c-67.6,67.6 -105.8,160.4 -107.5,261.4C492.5,881.5 479.8,893.9 464.3,893.9z" />
|
||||
</vector>
|
||||
@@ -5,4 +5,5 @@
|
||||
<string name="hts_module">HTS</string>
|
||||
<string name="bps_module">BPS</string>
|
||||
<string name="rscs_module">RSCS</string>
|
||||
<string name="prx_module">PRX</string>
|
||||
</resources>
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
26
profile_prx/build.gradle
Normal file
26
profile_prx/build.gradle
Normal file
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
5
profile_prx/src/main/AndroidManifest.xml
Normal file
5
profile_prx/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="no.nordicsemi.android.prx">
|
||||
|
||||
</manifest>
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<PRXData> = _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())
|
||||
}
|
||||
}
|
||||
@@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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) { }
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package no.nordicsemi.android.prx.view
|
||||
|
||||
internal sealed class PRXScreenViewEvent
|
||||
|
||||
internal object DisconnectEvent : PRXScreenViewEvent()
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
4
profile_prx/src/main/res/values/strings.xml
Normal file
4
profile_prx/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="prx_title">Proximity</string>
|
||||
</resources>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user