Add PRX profile

This commit is contained in:
Sylwester Zieliński
2021-10-15 11:07:28 +02:00
parent 2a28d7b255
commit d6ccce9187
38 changed files with 576 additions and 58 deletions

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="prx_title">Proximity</string>
</resources>

View File

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

View File

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