mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2026-01-19 14:44:33 +01:00
Add GLS feature
This commit is contained in:
28
feature_gls/build.gradle
Normal file
28
feature_gls/build.gradle
Normal file
@@ -0,0 +1,28 @@
|
||||
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.chart
|
||||
|
||||
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.gls
|
||||
|
||||
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.gls.test", appContext.packageName)
|
||||
}
|
||||
}
|
||||
5
feature_gls/src/main/AndroidManifest.xml
Normal file
5
feature_gls/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.gls">
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,11 @@
|
||||
package no.nordicsemi.android.gls.data
|
||||
|
||||
internal data class GLSData(
|
||||
val record: List<GLSRecord> = emptyList(),
|
||||
val batteryLevel: Int = 0,
|
||||
val requestStatus: RequestStatus = RequestStatus.IDLE
|
||||
)
|
||||
|
||||
internal enum class RequestStatus {
|
||||
IDLE, PENDING, SUCCESS, ABORTED, FAILED, NOT_SUPPORTED
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* 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.gls.data
|
||||
|
||||
import java.util.*
|
||||
|
||||
internal data class GLSRecord(
|
||||
/** Record sequence number */
|
||||
val sequenceNumber: Int = 0,
|
||||
|
||||
/** The base time of the measurement */
|
||||
val time: Calendar? = null,
|
||||
|
||||
/** The glucose concentration. 0 if not present */
|
||||
val glucoseConcentration: Float = 0f,
|
||||
|
||||
/** Concentration unit. One of the following: [GLSRecord.UNIT_kgpl], [GLSRecord.UNIT_molpl] */
|
||||
val unit: ConcentrationUnit = ConcentrationUnit.UNIT_KGPL,
|
||||
|
||||
/** The type of the record. 0 if not present */
|
||||
val type: Int = 0,
|
||||
|
||||
/** The sample location. 0 if unknown */
|
||||
val sampleLocation: Int = 0,
|
||||
|
||||
/** Sensor status annunciation flags. 0 if not present */
|
||||
val status: Int = 0,
|
||||
|
||||
var context: MeasurementContext? = null
|
||||
)
|
||||
|
||||
internal data class MeasurementContext(
|
||||
|
||||
val carbohydrateId: CarbohydrateId = CarbohydrateId.NOT_PRESENT,
|
||||
|
||||
/** Number of kilograms of carbohydrate */
|
||||
val carbohydrateUnits: Float = 0f,
|
||||
|
||||
val meal: TypeOfMeal = TypeOfMeal.NOT_PRESENT,
|
||||
|
||||
val tester: TestType = TestType.NOT_PRESENT,
|
||||
|
||||
val health: HealthStatus = HealthStatus.NOT_PRESENT,
|
||||
|
||||
/** Exercise duration in seconds. 0 if not present */
|
||||
val exerciseDuration: Int = 0,
|
||||
|
||||
/** Exercise intensity in percent. 0 if not present */
|
||||
val exerciseIntensity: Int = 0,
|
||||
|
||||
val medicationId: MedicationId = MedicationId.NOT_PRESENT,
|
||||
|
||||
/** Quantity of medication. See [.medicationUnit] for the unit. */
|
||||
val medicationQuantity: Float = 0f,
|
||||
|
||||
/** One of the following: [MeasurementContext.UNIT_kg], [MeasurementContext.UNIT_l]. */
|
||||
val medicationUnit: MedicationUnit = MedicationUnit.UNIT_KG,
|
||||
|
||||
/** HbA1c value. 0 if not present */
|
||||
val HbA1c: Float = 0f
|
||||
)
|
||||
|
||||
internal enum class ConcentrationUnit(val id: Int) {
|
||||
UNIT_KGPL(0),
|
||||
UNIT_MOLPL(1);
|
||||
|
||||
companion object {
|
||||
fun create(value: Int): ConcentrationUnit {
|
||||
return values().firstOrNull { it.id == value }
|
||||
?: throw IllegalArgumentException("Cannot find element for provided value.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal enum class CarbohydrateId(val id: Int) {
|
||||
NOT_PRESENT(0),
|
||||
BREAKFAST(1),
|
||||
LUNCH(2),
|
||||
DINNER(3),
|
||||
SNACK(4),
|
||||
DRINK(5),
|
||||
SUPPER(6),
|
||||
BRUNCH(7);
|
||||
|
||||
companion object {
|
||||
fun create(value: Byte): CarbohydrateId {
|
||||
return values().firstOrNull { it.id == value.toInt() }
|
||||
?: throw IllegalArgumentException("Cannot find element for provided value.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal enum class TypeOfMeal(val id: Int) {
|
||||
NOT_PRESENT(0),
|
||||
PREPRANDIAL(1),
|
||||
POSTPRANDIAL(2),
|
||||
FASTING(3),
|
||||
CASUAL(4),
|
||||
BEDTIME(5);
|
||||
|
||||
companion object {
|
||||
fun create(value: Byte): TypeOfMeal {
|
||||
return values().firstOrNull { it.id == value.toInt() }
|
||||
?: throw IllegalArgumentException("Cannot find element for provided value.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal enum class TestType(val id: Int) {
|
||||
NOT_PRESENT(0),
|
||||
SELF(1),
|
||||
HEALTH_CARE_PROFESSIONAL(2),
|
||||
LAB_TEST(3),
|
||||
VALUE_NOT_AVAILABLE(15);
|
||||
|
||||
companion object {
|
||||
fun create(value: Byte): TestType {
|
||||
return values().firstOrNull { it.id == value.toInt() }
|
||||
?: throw IllegalArgumentException("Cannot find element for provided value.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal enum class HealthStatus(val id: Int) {
|
||||
NOT_PRESENT(0),
|
||||
MINOR_HEALTH_ISSUES(1),
|
||||
MAJOR_HEALTH_ISSUES(2),
|
||||
DURING_MENSES(3),
|
||||
UNDER_STRESS(4),
|
||||
NO_HEALTH_ISSUES(5),
|
||||
VALUE_NOT_AVAILABLE(15);
|
||||
|
||||
companion object {
|
||||
fun create(value: Byte): HealthStatus {
|
||||
return values().firstOrNull { it.id == value.toInt() }
|
||||
?: throw IllegalArgumentException("Cannot find element for provided value.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal enum class MedicationId(val id: Int) {
|
||||
NOT_PRESENT(0),
|
||||
RAPID_ACTING_INSULIN(1),
|
||||
SHORT_ACTING_INSULIN(2),
|
||||
INTERMEDIATE_ACTING_INSULIN(3),
|
||||
LONG_ACTING_INSULIN(4),
|
||||
PRE_MIXED_INSULIN(5);
|
||||
|
||||
companion object {
|
||||
fun create(value: Byte): MedicationId {
|
||||
return values().firstOrNull { it.id == value.toInt() }
|
||||
?: throw IllegalArgumentException("Cannot find element for provided value.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal enum class MedicationUnit(val id: Int) {
|
||||
UNIT_KG(0),
|
||||
UNIT_L(1);
|
||||
|
||||
companion object {
|
||||
fun create(value: Int): MedicationUnit {
|
||||
return values().firstOrNull { it.id == value }
|
||||
?: throw IllegalArgumentException("Cannot find element for provided value.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
/*
|
||||
* 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.gls.repository
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothGatt
|
||||
import android.bluetooth.BluetoothGattCharacteristic
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import no.nordicsemi.android.ble.common.callback.RecordAccessControlPointDataCallback
|
||||
import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementContextDataCallback
|
||||
import no.nordicsemi.android.ble.common.callback.glucose.GlucoseMeasurementDataCallback
|
||||
import no.nordicsemi.android.ble.common.data.RecordAccessControlPointData
|
||||
import no.nordicsemi.android.ble.common.profile.RecordAccessControlPointCallback.RACPErrorCode
|
||||
import no.nordicsemi.android.ble.common.profile.RecordAccessControlPointCallback.RACPOpCode
|
||||
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementCallback.GlucoseStatus
|
||||
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Carbohydrate
|
||||
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Health
|
||||
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Meal
|
||||
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Medication
|
||||
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Tester
|
||||
import no.nordicsemi.android.ble.data.Data
|
||||
import no.nordicsemi.android.gls.data.CarbohydrateId
|
||||
import no.nordicsemi.android.gls.data.ConcentrationUnit
|
||||
import no.nordicsemi.android.gls.data.GLSData
|
||||
import no.nordicsemi.android.gls.data.GLSRecord
|
||||
import no.nordicsemi.android.gls.data.HealthStatus
|
||||
import no.nordicsemi.android.gls.data.MeasurementContext
|
||||
import no.nordicsemi.android.gls.data.MedicationId
|
||||
import no.nordicsemi.android.gls.data.MedicationUnit
|
||||
import no.nordicsemi.android.gls.data.RequestStatus
|
||||
import no.nordicsemi.android.gls.data.TestType
|
||||
import no.nordicsemi.android.gls.data.TypeOfMeal
|
||||
import no.nordicsemi.android.log.LogContract
|
||||
import no.nordicsemi.android.service.BatteryManager
|
||||
import no.nordicsemi.android.service.BatteryManagerCallbacks
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/** Glucose service UUID */
|
||||
private val GLS_SERVICE_UUID = UUID.fromString("00001808-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
/** Glucose Measurement characteristic UUID */
|
||||
private val GM_CHARACTERISTIC = UUID.fromString("00002A18-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
/** Glucose Measurement Context characteristic UUID */
|
||||
private val GM_CONTEXT_CHARACTERISTIC =
|
||||
UUID.fromString("00002A34-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
/** Glucose Feature characteristic UUID */
|
||||
private val GF_CHARACTERISTIC = UUID.fromString("00002A51-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
/** Record Access Control Point characteristic UUID */
|
||||
private val RACP_CHARACTERISTIC = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
@Singleton
|
||||
internal class GLSManager @Inject constructor(
|
||||
@ApplicationContext context: Context
|
||||
) : BatteryManager<BatteryManagerCallbacks?>(context) {
|
||||
|
||||
val data = MutableStateFlow(GLSData())
|
||||
private val records = hashMapOf<Int, GLSRecord>()
|
||||
|
||||
private var glucoseMeasurementCharacteristic: BluetoothGattCharacteristic? = null
|
||||
private var glucoseMeasurementContextCharacteristic: BluetoothGattCharacteristic? = null
|
||||
private var recordAccessControlPointCharacteristic: BluetoothGattCharacteristic? = null
|
||||
|
||||
override fun getGattCallback(): BatteryManagerGattCallback {
|
||||
return GlucoseManagerGattCallback()
|
||||
}
|
||||
|
||||
/**
|
||||
* BluetoothGatt callbacks for connection/disconnection, service discovery,
|
||||
* receiving notification, etc.
|
||||
*/
|
||||
private inner class GlucoseManagerGattCallback : BatteryManagerGattCallback() {
|
||||
override fun initialize() {
|
||||
super.initialize()
|
||||
|
||||
// The gatt.setCharacteristicNotification(...) method is called in BleManager during
|
||||
// enabling notifications or indications
|
||||
// (see BleManager#internalEnableNotifications/Indications).
|
||||
// However, on Samsung S3 with Android 4.3 it looks like the 2 gatt calls
|
||||
// (gatt.setCharacteristicNotification(...) and gatt.writeDescriptor(...)) are called
|
||||
// too quickly, or from a wrong thread, and in result the notification listener is not
|
||||
// set, causing onCharacteristicChanged(...) callback never being called when a
|
||||
// notification comes. Enabling them here, like below, solves the problem.
|
||||
// However... the original approach works for the Battery Level CCCD, which makes it
|
||||
// even weirder.
|
||||
/*
|
||||
gatt.setCharacteristicNotification(glucoseMeasurementCharacteristic, true);
|
||||
if (glucoseMeasurementContextCharacteristic != null) {
|
||||
device.setCharacteristicNotification(glucoseMeasurementContextCharacteristic, true);
|
||||
}
|
||||
device.setCharacteristicNotification(recordAccessControlPointCharacteristic, true);
|
||||
*/
|
||||
setNotificationCallback(glucoseMeasurementCharacteristic)
|
||||
.with(object : GlucoseMeasurementDataCallback() {
|
||||
|
||||
override fun onGlucoseMeasurementReceived(
|
||||
device: BluetoothDevice, sequenceNumber: Int,
|
||||
time: Calendar, glucoseConcentration: Float?,
|
||||
unit: Int?, type: Int?,
|
||||
sampleLocation: Int?, status: GlucoseStatus?,
|
||||
contextInformationFollows: Boolean
|
||||
) {
|
||||
val record = GLSRecord(
|
||||
sequenceNumber = sequenceNumber,
|
||||
time = time,
|
||||
glucoseConcentration = glucoseConcentration ?: 0f,
|
||||
unit = unit?.let { ConcentrationUnit.create(it) }
|
||||
?: ConcentrationUnit.UNIT_KGPL,
|
||||
type = type ?: 0,
|
||||
sampleLocation = sampleLocation ?: 0,
|
||||
status = status?.value ?: 0
|
||||
)
|
||||
|
||||
records[record.sequenceNumber] = record
|
||||
if (!contextInformationFollows) {
|
||||
data.tryEmit(data.value.copy(record = records.values.toList()))
|
||||
}
|
||||
}
|
||||
})
|
||||
setNotificationCallback(glucoseMeasurementContextCharacteristic)
|
||||
.with(object : GlucoseMeasurementContextDataCallback() {
|
||||
|
||||
override fun onGlucoseMeasurementContextReceived(
|
||||
device: BluetoothDevice, sequenceNumber: Int,
|
||||
carbohydrate: Carbohydrate?, carbohydrateAmount: Float?,
|
||||
meal: Meal?, tester: Tester?,
|
||||
health: Health?, exerciseDuration: Int?,
|
||||
exerciseIntensity: Int?, medication: Medication?,
|
||||
medicationAmount: Float?, medicationUnit: Int?,
|
||||
HbA1c: Float?
|
||||
) {
|
||||
val record = records[sequenceNumber] ?: return
|
||||
|
||||
val context = MeasurementContext(
|
||||
carbohydrateId = carbohydrate?.value?.let { CarbohydrateId.create(it) }
|
||||
?: CarbohydrateId.NOT_PRESENT,
|
||||
carbohydrateUnits = carbohydrateAmount ?: 0f,
|
||||
meal = meal?.value?.let { TypeOfMeal.create(it) }
|
||||
?: TypeOfMeal.NOT_PRESENT,
|
||||
tester = tester?.value?.let { TestType.create(it) }
|
||||
?: TestType.NOT_PRESENT,
|
||||
health = health?.value?.let { HealthStatus.create(it) }
|
||||
?: HealthStatus.NOT_PRESENT,
|
||||
exerciseDuration = exerciseDuration ?: 0,
|
||||
exerciseIntensity = exerciseIntensity ?: 0,
|
||||
medicationId = medication?.value?.let { MedicationId.create(it) }
|
||||
?: MedicationId.NOT_PRESENT,
|
||||
medicationQuantity = medicationAmount ?: 0f,
|
||||
medicationUnit = medicationUnit?.let { MedicationUnit.create(it) }
|
||||
?: MedicationUnit.UNIT_KG,
|
||||
HbA1c = HbA1c ?: 0f
|
||||
)
|
||||
record.context = context
|
||||
|
||||
data.tryEmit(data.value)
|
||||
}
|
||||
})
|
||||
setIndicationCallback(recordAccessControlPointCharacteristic)
|
||||
.with(object : RecordAccessControlPointDataCallback() {
|
||||
|
||||
@SuppressLint("SwitchIntDef")
|
||||
override fun onRecordAccessOperationCompleted(
|
||||
device: BluetoothDevice,
|
||||
@RACPOpCode requestCode: Int
|
||||
) {
|
||||
val status = when (requestCode) {
|
||||
RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED
|
||||
else -> RequestStatus.SUCCESS
|
||||
}
|
||||
data.tryEmit(data.value.copy(requestStatus = status))
|
||||
}
|
||||
|
||||
override fun onRecordAccessOperationCompletedWithNoRecordsFound(
|
||||
device: BluetoothDevice,
|
||||
@RACPOpCode requestCode: Int
|
||||
) {
|
||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.SUCCESS))
|
||||
}
|
||||
|
||||
override fun onNumberOfRecordsReceived(
|
||||
device: BluetoothDevice,
|
||||
numberOfRecords: Int
|
||||
) {
|
||||
//TODO("Probably not needed")
|
||||
// mCallbacks!!.onNumberOfRecordsRequested(device, numberOfRecords)
|
||||
if (numberOfRecords > 0) {
|
||||
if (records.size > 0) {
|
||||
val sequenceNumber = records.keys.last() + 1
|
||||
writeCharacteristic(
|
||||
recordAccessControlPointCharacteristic,
|
||||
RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(
|
||||
sequenceNumber
|
||||
)
|
||||
)
|
||||
.enqueue()
|
||||
} else {
|
||||
writeCharacteristic(
|
||||
recordAccessControlPointCharacteristic,
|
||||
RecordAccessControlPointData.reportAllStoredRecords()
|
||||
)
|
||||
.enqueue()
|
||||
}
|
||||
} else {
|
||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.SUCCESS))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRecordAccessOperationError(
|
||||
device: BluetoothDevice,
|
||||
@RACPOpCode requestCode: Int,
|
||||
@RACPErrorCode errorCode: Int
|
||||
) {
|
||||
log(Log.WARN, "Record Access operation failed (error $errorCode)")
|
||||
if (errorCode == RACP_ERROR_OP_CODE_NOT_SUPPORTED) {
|
||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.NOT_SUPPORTED))
|
||||
} else {
|
||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.FAILED))
|
||||
}
|
||||
}
|
||||
})
|
||||
enableNotifications(glucoseMeasurementCharacteristic).enqueue()
|
||||
enableNotifications(glucoseMeasurementContextCharacteristic).enqueue()
|
||||
enableIndications(recordAccessControlPointCharacteristic)
|
||||
.fail { device: BluetoothDevice?, status: Int ->
|
||||
log(
|
||||
Log.WARN,
|
||||
"Failed to enabled Record Access Control Point indications (error $status)"
|
||||
)
|
||||
}
|
||||
.enqueue()
|
||||
}
|
||||
|
||||
public override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean {
|
||||
val service = gatt.getService(GLS_SERVICE_UUID)
|
||||
if (service != null) {
|
||||
glucoseMeasurementCharacteristic = service.getCharacteristic(GM_CHARACTERISTIC)
|
||||
glucoseMeasurementContextCharacteristic = service.getCharacteristic(
|
||||
GM_CONTEXT_CHARACTERISTIC
|
||||
)
|
||||
recordAccessControlPointCharacteristic = service.getCharacteristic(
|
||||
RACP_CHARACTERISTIC
|
||||
)
|
||||
}
|
||||
return glucoseMeasurementCharacteristic != null && recordAccessControlPointCharacteristic != null
|
||||
}
|
||||
|
||||
override fun onServicesInvalidated() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun isOptionalServiceSupported(gatt: BluetoothGatt): Boolean {
|
||||
super.isOptionalServiceSupported(gatt)
|
||||
return glucoseMeasurementContextCharacteristic != null
|
||||
}
|
||||
|
||||
override fun onDeviceDisconnected() {
|
||||
glucoseMeasurementCharacteristic = null
|
||||
glucoseMeasurementContextCharacteristic = null
|
||||
recordAccessControlPointCharacteristic = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the records list locally.
|
||||
*/
|
||||
fun clear() {
|
||||
records.clear()
|
||||
val target = bluetoothDevice
|
||||
if (target != null) {
|
||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.SUCCESS))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the request to obtain the last (most recent) record from glucose device. The data will
|
||||
* be returned to Glucose Measurement characteristic as a notification followed by Record Access
|
||||
* Control Point indication with status code Success or other in case of error.
|
||||
*/
|
||||
fun lastRecord(): Unit {
|
||||
if (recordAccessControlPointCharacteristic == null) return
|
||||
val target = bluetoothDevice ?: return
|
||||
clear()
|
||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
|
||||
writeCharacteristic(
|
||||
recordAccessControlPointCharacteristic,
|
||||
RecordAccessControlPointData.reportLastStoredRecord()
|
||||
)
|
||||
.with { device: BluetoothDevice, data: Data ->
|
||||
log(
|
||||
LogContract.Log.Level.APPLICATION,
|
||||
"\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent"
|
||||
)
|
||||
}
|
||||
.enqueue()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the request to obtain the first (oldest) record from glucose device. The data will be
|
||||
* returned to Glucose Measurement characteristic as a notification followed by Record Access
|
||||
* Control Point indication with status code Success or other in case of error.
|
||||
*/
|
||||
fun requestFirstRecord(): Unit {
|
||||
if (recordAccessControlPointCharacteristic == null) return
|
||||
val target = bluetoothDevice ?: return
|
||||
clear()
|
||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
|
||||
writeCharacteristic(
|
||||
recordAccessControlPointCharacteristic,
|
||||
RecordAccessControlPointData.reportFirstStoredRecord()
|
||||
)
|
||||
.with { device: BluetoothDevice, data: Data ->
|
||||
log(
|
||||
LogContract.Log.Level.APPLICATION,
|
||||
"\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent"
|
||||
)
|
||||
}
|
||||
.enqueue()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the request to obtain all records from glucose device. Initially we want to notify user
|
||||
* about the number of the records so the 'Report Number of Stored Records' is send. The data
|
||||
* will be returned to Glucose Measurement characteristic as a notification followed by
|
||||
* Record Access Control Point indication with status code Success or other in case of error.
|
||||
*/
|
||||
fun requestAllRecords(): Unit {
|
||||
if (recordAccessControlPointCharacteristic == null) return
|
||||
val target = bluetoothDevice ?: return
|
||||
clear()
|
||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
|
||||
writeCharacteristic(
|
||||
recordAccessControlPointCharacteristic,
|
||||
RecordAccessControlPointData.reportNumberOfAllStoredRecords()
|
||||
)
|
||||
.with { device: BluetoothDevice, data: Data ->
|
||||
log(
|
||||
LogContract.Log.Level.APPLICATION,
|
||||
"\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent"
|
||||
)
|
||||
}
|
||||
.enqueue()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the request to obtain from the glucose device all records newer than the newest one
|
||||
* from local storage. The data will be returned to Glucose Measurement characteristic as
|
||||
* a notification followed by Record Access Control Point indication with status code Success
|
||||
* or other in case of error.
|
||||
*
|
||||
*
|
||||
* Refresh button will not download records older than the oldest in the local memory.
|
||||
* E.g. if you have pressed Last and then Refresh, than it will try to get only newer records.
|
||||
* However if there are no records, it will download all existing (using [.getAllRecords]).
|
||||
*/
|
||||
fun refreshRecords() {
|
||||
if (recordAccessControlPointCharacteristic == null) return
|
||||
val target = bluetoothDevice ?: return
|
||||
if (records.size == 0) {
|
||||
requestAllRecords()
|
||||
} else {
|
||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
|
||||
|
||||
// obtain the last sequence number
|
||||
val sequenceNumber = records.keys.last() + 1
|
||||
writeCharacteristic(
|
||||
recordAccessControlPointCharacteristic,
|
||||
RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber)
|
||||
)
|
||||
.with { device: BluetoothDevice, data: Data ->
|
||||
log(
|
||||
LogContract.Log.Level.APPLICATION,
|
||||
"\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent"
|
||||
)
|
||||
}
|
||||
.enqueue()
|
||||
// Info:
|
||||
// Operators OPERATOR_LESS_THEN_OR_EQUAL and OPERATOR_RANGE are not supported by Nordic Semiconductor Glucose Service in SDK 4.4.2.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends abort operation signal to the device.
|
||||
*/
|
||||
fun abort() {
|
||||
if (recordAccessControlPointCharacteristic == null) return
|
||||
val target = bluetoothDevice ?: return
|
||||
writeCharacteristic(
|
||||
recordAccessControlPointCharacteristic,
|
||||
RecordAccessControlPointData.abortOperation()
|
||||
)
|
||||
.with { device: BluetoothDevice, data: Data ->
|
||||
log(
|
||||
LogContract.Log.Level.APPLICATION,
|
||||
"\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent"
|
||||
)
|
||||
}
|
||||
.enqueue()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the request to delete all data from the device. A Record Access Control Point
|
||||
* indication with status code Success (or other in case of error) will be send.
|
||||
*/
|
||||
fun deleteAllRecords() {
|
||||
if (recordAccessControlPointCharacteristic == null) return
|
||||
val target = bluetoothDevice ?: return
|
||||
clear()
|
||||
data.tryEmit(data.value.copy(requestStatus = RequestStatus.PENDING))
|
||||
writeCharacteristic(
|
||||
recordAccessControlPointCharacteristic,
|
||||
RecordAccessControlPointData.deleteAllStoredRecords()
|
||||
)
|
||||
.with { device: BluetoothDevice, data: Data ->
|
||||
log(
|
||||
LogContract.Log.Level.APPLICATION,
|
||||
"\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent"
|
||||
)
|
||||
}
|
||||
.enqueue()
|
||||
|
||||
val elements = listOf<Int>(1, 2, 3)
|
||||
val result = elements.all { it > 3 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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.gls.repository
|
||||
|
||||
import no.nordicsemi.android.ble.data.Data
|
||||
|
||||
object GLSRecordAccessControlPointParser {
|
||||
|
||||
private const val OP_CODE_REPORT_STORED_RECORDS = 1
|
||||
private const val OP_CODE_DELETE_STORED_RECORDS = 2
|
||||
private const val OP_CODE_ABORT_OPERATION = 3
|
||||
private const val OP_CODE_REPORT_NUMBER_OF_RECORDS = 4
|
||||
private const val OP_CODE_NUMBER_OF_STORED_RECORDS_RESPONSE = 5
|
||||
private const val OP_CODE_RESPONSE_CODE = 6
|
||||
private const val OPERATOR_NULL = 0
|
||||
private const val OPERATOR_ALL_RECORDS = 1
|
||||
private const val OPERATOR_LESS_THEN_OR_EQUAL = 2
|
||||
private const val OPERATOR_GREATER_THEN_OR_EQUAL = 3
|
||||
private const val OPERATOR_WITHING_RANGE = 4
|
||||
private const val OPERATOR_FIRST_RECORD = 5
|
||||
private const val OPERATOR_LAST_RECORD = 6
|
||||
private const val RESPONSE_SUCCESS = 1
|
||||
private const val RESPONSE_OP_CODE_NOT_SUPPORTED = 2
|
||||
private const val RESPONSE_INVALID_OPERATOR = 3
|
||||
private const val RESPONSE_OPERATOR_NOT_SUPPORTED = 4
|
||||
private const val RESPONSE_INVALID_OPERAND = 5
|
||||
private const val RESPONSE_NO_RECORDS_FOUND = 6
|
||||
private const val RESPONSE_ABORT_UNSUCCESSFUL = 7
|
||||
private const val RESPONSE_PROCEDURE_NOT_COMPLETED = 8
|
||||
private const val RESPONSE_OPERAND_NOT_SUPPORTED = 9
|
||||
|
||||
fun parse(data: Data): String {
|
||||
val builder = StringBuilder()
|
||||
val opCode = data.getIntValue(Data.FORMAT_UINT8, 0)!!
|
||||
val operator = data.getIntValue(Data.FORMAT_UINT8, 1)!!
|
||||
when (opCode) {
|
||||
OP_CODE_REPORT_STORED_RECORDS, OP_CODE_DELETE_STORED_RECORDS, OP_CODE_ABORT_OPERATION, OP_CODE_REPORT_NUMBER_OF_RECORDS -> builder.append(
|
||||
getOpCode(opCode)
|
||||
).append("\n")
|
||||
OP_CODE_NUMBER_OF_STORED_RECORDS_RESPONSE -> {
|
||||
builder.append(getOpCode(opCode)).append(": ")
|
||||
val value = data.getIntValue(Data.FORMAT_UINT16, 2)!!
|
||||
builder.append(value).append("\n")
|
||||
}
|
||||
OP_CODE_RESPONSE_CODE -> {
|
||||
builder.append(getOpCode(opCode)).append(" for ")
|
||||
val targetOpCode = data.getIntValue(Data.FORMAT_UINT8, 2)!!
|
||||
builder.append(getOpCode(targetOpCode)).append(": ")
|
||||
val status = data.getIntValue(Data.FORMAT_UINT8, 3)!!
|
||||
builder.append(getStatus(status)).append("\n")
|
||||
}
|
||||
}
|
||||
when (operator) {
|
||||
OPERATOR_ALL_RECORDS, OPERATOR_FIRST_RECORD, OPERATOR_LAST_RECORD -> builder.append("Operator: ")
|
||||
.append(
|
||||
getOperator(operator)
|
||||
).append("\n")
|
||||
OPERATOR_GREATER_THEN_OR_EQUAL, OPERATOR_LESS_THEN_OR_EQUAL -> {
|
||||
val filter = data.getIntValue(Data.FORMAT_UINT8, 2)!!
|
||||
val value = data.getIntValue(Data.FORMAT_UINT16, 3)!!
|
||||
builder.append("Operator: ").append(getOperator(operator)).append(" ").append(value)
|
||||
.append(" (filter: ").append(filter).append(")\n")
|
||||
}
|
||||
OPERATOR_WITHING_RANGE -> {
|
||||
val filter = data.getIntValue(Data.FORMAT_UINT8, 2)!!
|
||||
val value1 = data.getIntValue(Data.FORMAT_UINT16, 3)!!
|
||||
val value2 = data.getIntValue(Data.FORMAT_UINT16, 5)!!
|
||||
builder.append("Operator: ").append(getOperator(operator)).append(" ")
|
||||
.append(value1).append("-").append(value2).append(" (filter: ").append(filter)
|
||||
.append(")\n")
|
||||
}
|
||||
}
|
||||
if (builder.isNotEmpty()) {
|
||||
builder.setLength(builder.length - 1)
|
||||
}
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
private fun getOpCode(opCode: Int): String {
|
||||
return when (opCode) {
|
||||
OP_CODE_REPORT_STORED_RECORDS -> "Report stored records"
|
||||
OP_CODE_DELETE_STORED_RECORDS -> "Delete stored records"
|
||||
OP_CODE_ABORT_OPERATION -> "Abort operation"
|
||||
OP_CODE_REPORT_NUMBER_OF_RECORDS -> "Report number of stored records"
|
||||
OP_CODE_NUMBER_OF_STORED_RECORDS_RESPONSE -> "Number of stored records response"
|
||||
OP_CODE_RESPONSE_CODE -> "Response Code"
|
||||
else -> "Reserved for future use"
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOperator(operator: Int): String {
|
||||
return when (operator) {
|
||||
OPERATOR_NULL -> "Null"
|
||||
OPERATOR_ALL_RECORDS -> "All records"
|
||||
OPERATOR_LESS_THEN_OR_EQUAL -> "Less than or equal to"
|
||||
OPERATOR_GREATER_THEN_OR_EQUAL -> "Greater than or equal to"
|
||||
OPERATOR_WITHING_RANGE -> "Within range of"
|
||||
OPERATOR_FIRST_RECORD -> "First record(i.e. oldest record)"
|
||||
OPERATOR_LAST_RECORD -> "Last record (i.e. most recent record)"
|
||||
else -> "Reserved for future use"
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStatus(status: Int): String {
|
||||
return when (status) {
|
||||
RESPONSE_SUCCESS -> "Success"
|
||||
RESPONSE_OP_CODE_NOT_SUPPORTED -> "Operation not supported"
|
||||
RESPONSE_INVALID_OPERATOR -> "Invalid operator"
|
||||
RESPONSE_OPERATOR_NOT_SUPPORTED -> "Operator not supported"
|
||||
RESPONSE_INVALID_OPERAND -> "Invalid operand"
|
||||
RESPONSE_NO_RECORDS_FOUND -> "No records found"
|
||||
RESPONSE_ABORT_UNSUCCESSFUL -> "Abort unsuccessful"
|
||||
RESPONSE_PROCEDURE_NOT_COMPLETED -> "Procedure not completed"
|
||||
RESPONSE_OPERAND_NOT_SUPPORTED -> "Operand not supported"
|
||||
else -> "Reserved for future use"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package no.nordicsemi.android.gls.view
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import no.nordicsemi.android.gls.R
|
||||
import no.nordicsemi.android.gls.data.GLSData
|
||||
import no.nordicsemi.android.gls.viewmodel.DisconnectEvent
|
||||
import no.nordicsemi.android.gls.viewmodel.GLSScreenViewEvent
|
||||
import no.nordicsemi.android.theme.view.BatteryLevelView
|
||||
|
||||
@Composable
|
||||
internal fun GLSContentView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
BatteryLevelView(state.batteryLevel)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Button(
|
||||
colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.secondary),
|
||||
onClick = { onEvent(DisconnectEvent) }
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.disconnect))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package no.nordicsemi.android.gls.view
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import no.nordicsemi.android.gls.R
|
||||
import no.nordicsemi.android.gls.data.GLSData
|
||||
import no.nordicsemi.android.gls.viewmodel.GLSScreenViewEvent
|
||||
import no.nordicsemi.android.gls.viewmodel.GLSViewModel
|
||||
|
||||
@Composable
|
||||
fun GLSScreen(finishAction: () -> Unit) {
|
||||
val viewModel: GLSViewModel = hiltViewModel()
|
||||
val state = viewModel.state.collectAsState().value
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GLSView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) {
|
||||
Column {
|
||||
TopAppBar(title = { Text(text = stringResource(id = R.string.gls_title)) })
|
||||
|
||||
GLSContentView(state, onEvent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package no.nordicsemi.android.gls.viewmodel
|
||||
|
||||
sealed class GLSScreenViewEvent
|
||||
|
||||
object DisconnectEvent : GLSScreenViewEvent()
|
||||
@@ -0,0 +1,20 @@
|
||||
package no.nordicsemi.android.gls.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import no.nordicsemi.android.gls.repository.GLSManager
|
||||
import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class GLSViewModel @Inject constructor(
|
||||
private val glsManager: GLSManager,
|
||||
private val deviceHolder: SelectedBluetoothDeviceHolder
|
||||
) : ViewModel() {
|
||||
|
||||
val state = glsManager.data
|
||||
|
||||
fun bondDevice() {
|
||||
|
||||
}
|
||||
}
|
||||
4
feature_gls/src/main/res/values/strings.xml
Normal file
4
feature_gls/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="gls_title">GLS</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,17 @@
|
||||
package no.nordicsemi.android.gls
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user