mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2025-12-19 07:24:22 +01:00
Add connecting view to profiles
This commit is contained in:
@@ -1,13 +1,11 @@
|
||||
package no.nordicsemi.android.nrftoolbox
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
|
||||
@@ -2,7 +2,6 @@ package no.nordicsemi.android.nrftoolbox
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.ParcelUuid
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -46,15 +45,15 @@ internal fun HomeScreen() {
|
||||
navController = navController,
|
||||
startDestination = NavigationId.HOME.id
|
||||
) {
|
||||
composable(NavigationId.HOME.id) {
|
||||
HomeView(viewModel)
|
||||
}
|
||||
composable(NavigationId.SCANNER.id) {
|
||||
val profile = viewModel.profile!!
|
||||
FindDeviceScreen(ParcelUuid(profile.uuid)) {
|
||||
viewModel.onScannerFlowResult(it)
|
||||
}
|
||||
}
|
||||
composable(NavigationId.HOME.id) {
|
||||
HomeView(viewModel)
|
||||
}
|
||||
composable(NavigationId.CSC.id) {
|
||||
CSCScreen(navigateUp)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package no.nordicsemi.android.nrftoolbox
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package no.nordicsemi.android.service
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
|
||||
@@ -21,6 +21,8 @@ import java.util.*
|
||||
</T> */
|
||||
abstract class BatteryManager(context: Context) : BleManager(context) {
|
||||
|
||||
private val TAG = "BLE-MANAGER"
|
||||
|
||||
private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null
|
||||
|
||||
private val batteryLevelDataCallback: DataReceivedCallback =
|
||||
@@ -67,6 +69,11 @@ abstract class BatteryManager(context: Context) : BleManager(context) {
|
||||
}
|
||||
}
|
||||
|
||||
override fun log(priority: Int, message: String) {
|
||||
super.log(priority, message)
|
||||
Log.println(priority, TAG, message)
|
||||
}
|
||||
|
||||
protected abstract inner class BatteryManagerGattCallback : BleManagerGattCallback() {
|
||||
override fun initialize() {
|
||||
readBatteryLevelCharacteristic()
|
||||
|
||||
@@ -26,9 +26,7 @@ import android.bluetooth.BluetoothDevice
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -90,6 +88,13 @@ abstract class BleProfileService : Service() {
|
||||
_status.value = BleManagerStatus.OK
|
||||
}
|
||||
|
||||
override fun onDeviceFailedToConnect(device: BluetoothDevice, reason: Int) {
|
||||
super.onDeviceFailedToConnect(device, reason)
|
||||
_status.value = BleManagerStatus.DISCONNECTED
|
||||
stopSelf()
|
||||
scope.close()
|
||||
}
|
||||
|
||||
override fun onDeviceDisconnected(device: BluetoothDevice, reason: Int) {
|
||||
super.onDeviceDisconnected(device, reason)
|
||||
_status.value = BleManagerStatus.DISCONNECTED
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package no.nordicsemi.android.service
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import java.io.Closeable
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
@@ -9,6 +9,6 @@ class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineS
|
||||
override val coroutineContext: CoroutineContext = context
|
||||
|
||||
override fun close() {
|
||||
coroutineContext.cancel()
|
||||
coroutineContext.cancelChildren()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,34 @@
|
||||
package no.nordicsemi.android.service
|
||||
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.util.Log
|
||||
import no.nordicsemi.android.ble.observer.ConnectionObserver
|
||||
|
||||
abstract class ConnectionObserverAdapter : ConnectionObserver {
|
||||
|
||||
override fun onDeviceConnecting(device: BluetoothDevice) { }
|
||||
private val TAG = "BLE-CONNECTION"
|
||||
|
||||
override fun onDeviceConnected(device: BluetoothDevice) { }
|
||||
override fun onDeviceConnecting(device: BluetoothDevice) {
|
||||
Log.d(TAG, "onDeviceConnecting()")
|
||||
}
|
||||
|
||||
override fun onDeviceFailedToConnect(device: BluetoothDevice, reason: Int) { }
|
||||
override fun onDeviceConnected(device: BluetoothDevice) {
|
||||
Log.d(TAG, "onDeviceConnected()")
|
||||
}
|
||||
|
||||
override fun onDeviceReady(device: BluetoothDevice) { }
|
||||
override fun onDeviceFailedToConnect(device: BluetoothDevice, reason: Int) {
|
||||
Log.d(TAG, "onDeviceFailedToConnect()")
|
||||
}
|
||||
|
||||
override fun onDeviceDisconnecting(device: BluetoothDevice) { }
|
||||
override fun onDeviceReady(device: BluetoothDevice) {
|
||||
Log.d(TAG, "onDeviceReady()")
|
||||
}
|
||||
|
||||
override fun onDeviceDisconnected(device: BluetoothDevice, reason: Int) { }
|
||||
override fun onDeviceDisconnecting(device: BluetoothDevice) {
|
||||
Log.d(TAG, "onDeviceDisconnecting()")
|
||||
}
|
||||
|
||||
override fun onDeviceDisconnected(device: BluetoothDevice, reason: Int) {
|
||||
Log.d(TAG, "onDeviceDisconnected()")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package no.nordicsemi.android.service
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
|
||||
@@ -3,7 +3,12 @@ package no.nordicsemi.android.theme.view
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
||||
@@ -5,7 +5,6 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -19,6 +18,7 @@ import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -62,18 +62,18 @@ fun StringListView(config: StringListDialogConfig) {
|
||||
) {
|
||||
|
||||
config.items.forEachIndexed { i, entry ->
|
||||
Column(modifier = Modifier.clickable { config.onResult(ItemSelectedResult(i)) }) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.clickable { config.onResult(ItemSelectedResult(i)) }
|
||||
.padding(8.dp),
|
||||
) {
|
||||
Row {
|
||||
config.leftIcon?.let {
|
||||
Image(
|
||||
modifier = Modifier.padding(horizontal = 4.dp),
|
||||
painter = painterResource(it),
|
||||
contentDescription = "Content image",
|
||||
// colorFilter = ColorFilter.tint(
|
||||
// NordicColors.NordicDarkGray.value()
|
||||
// )
|
||||
)
|
||||
}
|
||||
Text(
|
||||
@@ -83,10 +83,6 @@ fun StringListView(config: StringListDialogConfig) {
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
if (i != config.items.size - 1) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package no.nordicsemi.android.utils
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package no.nordicsemi.android.utils
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package no.nordicsemi.android.bps
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="no.nordicsemi.android.bps">
|
||||
<manifest package="no.nordicsemi.android.bps">
|
||||
|
||||
</manifest>
|
||||
@@ -63,6 +63,7 @@ internal class BPSRepository @Inject constructor() {
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
_status.value = BleManagerStatus.CONNECTING
|
||||
_data.tryEmit(BPSData())
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ import no.nordicsemi.android.ble.common.callback.bps.IntermediateCuffPressureDat
|
||||
import no.nordicsemi.android.ble.common.profile.bp.BloodPressureTypes
|
||||
import no.nordicsemi.android.ble.data.Data
|
||||
import no.nordicsemi.android.bps.data.BPSRepository
|
||||
import no.nordicsemi.android.log.LogContract
|
||||
import no.nordicsemi.android.service.BatteryManager
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -42,6 +42,11 @@ internal class BPSViewModel @Inject constructor(
|
||||
repository.setNewStatus(BleManagerStatus.OK)
|
||||
}
|
||||
|
||||
override fun onDeviceFailedToConnect(device: BluetoothDevice, reason: Int) {
|
||||
super.onDeviceFailedToConnect(device, reason)
|
||||
repository.setNewStatus(BleManagerStatus.DISCONNECTED)
|
||||
}
|
||||
|
||||
override fun onDeviceDisconnected(device: BluetoothDevice, reason: Int) {
|
||||
super.onDeviceDisconnected(device, reason)
|
||||
repository.setNewStatus(BleManagerStatus.DISCONNECTED)
|
||||
@@ -63,7 +68,13 @@ internal class BPSViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun onDisconnectButtonClick() {
|
||||
deviceHolder.forgetDevice()
|
||||
bpsManager.disconnect().enqueue()
|
||||
deviceHolder.forgetDevice()
|
||||
repository.clear()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
repository.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package no.nordicsemi.android.bps
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package no.nordicsemi.android.cgms
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package no.nordicsemi.android.cgms.data
|
||||
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import no.nordicsemi.android.service.BleManagerStatus
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@@ -39,6 +43,7 @@ internal class CGMRepository @Inject constructor() {
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
_status.value = BleManagerStatus.CONNECTING
|
||||
_data.tryEmit(CGMData())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
package no.nordicsemi.android.cgms.view
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -17,8 +25,8 @@ import androidx.compose.ui.unit.dp
|
||||
import no.nordicsemi.android.cgms.R
|
||||
import no.nordicsemi.android.cgms.data.CGMData
|
||||
import no.nordicsemi.android.cgms.data.CGMRecord
|
||||
import no.nordicsemi.android.cgms.data.RequestStatus
|
||||
import no.nordicsemi.android.cgms.data.CGMServiceCommand
|
||||
import no.nordicsemi.android.cgms.data.RequestStatus
|
||||
import no.nordicsemi.android.material.you.CircularProgressIndicator
|
||||
import no.nordicsemi.android.theme.view.BatteryLevelView
|
||||
import no.nordicsemi.android.theme.view.ScreenSection
|
||||
|
||||
@@ -4,7 +4,6 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import no.nordicsemi.android.cgms.R
|
||||
import no.nordicsemi.android.cgms.data.CGMRecord
|
||||
import no.nordicsemi.android.cgms.data.CGMServiceCommand
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
|
||||
@@ -39,7 +39,12 @@ internal class CGMScreenViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun disconnect() {
|
||||
repository.clear()
|
||||
repository.sendNewServiceCommand(CGMServiceCommand.DISCONNECT)
|
||||
repository.clear()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
repository.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package no.nordicsemi.android.cgms
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package no.nordicsemi.android.csc
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package no.nordicsemi.android.csc.data
|
||||
|
||||
import no.nordicsemi.android.csc.view.CSCSettings
|
||||
import no.nordicsemi.android.csc.view.SpeedUnit
|
||||
import no.nordicsemi.android.material.you.RadioButtonItem
|
||||
import no.nordicsemi.android.material.you.RadioGroupViewEntity
|
||||
@@ -11,7 +10,6 @@ private const val DISPLAY_KM_H = "km/h"
|
||||
private const val DISPLAY_MPH = "mph"
|
||||
|
||||
internal data class CSCData(
|
||||
val showDialog: Boolean = false,
|
||||
val scanDevices: Boolean = false,
|
||||
val selectedSpeedUnit: SpeedUnit = SpeedUnit.M_S,
|
||||
val speed: Float = 0f,
|
||||
@@ -20,8 +18,7 @@ internal data class CSCData(
|
||||
val totalDistance: Float = 0f,
|
||||
val gearRatio: Float = 0f,
|
||||
val batteryLevel: Int = 0,
|
||||
val wheelSize: Int = CSCSettings.DefaultWheelSize.VALUE,
|
||||
val wheelSizeDisplay: String = CSCSettings.DefaultWheelSize.NAME
|
||||
val wheelSize: WheelSize = WheelSize()
|
||||
) {
|
||||
|
||||
private val speedWithUnit = when (selectedSpeedUnit) {
|
||||
|
||||
@@ -5,7 +5,9 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import no.nordicsemi.android.csc.view.SpeedUnit
|
||||
import no.nordicsemi.android.service.BleManagerStatus
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -13,37 +15,38 @@ import javax.inject.Singleton
|
||||
internal class CSCRepository @Inject constructor() {
|
||||
|
||||
private val _data = MutableStateFlow(CSCData())
|
||||
val data: StateFlow<CSCData> = _data
|
||||
val data: StateFlow<CSCData> = _data.asStateFlow()
|
||||
|
||||
private val _command = MutableSharedFlow<CSCServiceCommand>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST)
|
||||
val command = _command.asSharedFlow()
|
||||
|
||||
fun setWheelSize(wheelSize: Int, wheelSizeDisplay: String) {
|
||||
_data.tryEmit(_data.value.copy(
|
||||
wheelSize = wheelSize,
|
||||
wheelSizeDisplay = wheelSizeDisplay,
|
||||
showDialog = false
|
||||
))
|
||||
}
|
||||
private val _status = MutableStateFlow(BleManagerStatus.CONNECTING)
|
||||
val status = _status.asStateFlow()
|
||||
|
||||
fun setSpeedUnit(selectedSpeedUnit: SpeedUnit) {
|
||||
_data.tryEmit(_data.value.copy(selectedSpeedUnit = selectedSpeedUnit))
|
||||
}
|
||||
|
||||
fun setHideWheelSizeDialog() {
|
||||
_data.tryEmit(_data.value.copy(showDialog = false))
|
||||
fun setNewDistance(
|
||||
totalDistance: Float,
|
||||
distance: Float,
|
||||
speed: Float,
|
||||
wheelSize: WheelSize
|
||||
) {
|
||||
_data.tryEmit(_data.value.copy(
|
||||
totalDistance = totalDistance,
|
||||
distance = distance,
|
||||
speed = speed,
|
||||
wheelSize = wheelSize
|
||||
))
|
||||
}
|
||||
|
||||
fun setDisplayWheelSizeDialog() {
|
||||
_data.tryEmit(_data.value.copy(showDialog = true))
|
||||
}
|
||||
|
||||
fun setNewDistance(totalDistance: Float, distance: Float, speed: Float) {
|
||||
_data.tryEmit(_data.value.copy(totalDistance = totalDistance, distance = distance, speed = speed))
|
||||
}
|
||||
|
||||
fun setNewCrankCadence(crankCadence: Float, gearRatio: Float) {
|
||||
_data.tryEmit(_data.value.copy(cadence = crankCadence, gearRatio = gearRatio))
|
||||
fun setNewCrankCadence(
|
||||
crankCadence: Float,
|
||||
gearRatio: Float,
|
||||
wheelSize: WheelSize
|
||||
) {
|
||||
_data.tryEmit(_data.value.copy(cadence = crankCadence, gearRatio = gearRatio, wheelSize = wheelSize))
|
||||
}
|
||||
|
||||
fun setBatteryLevel(batteryLevel: Int) {
|
||||
@@ -54,7 +57,12 @@ internal class CSCRepository @Inject constructor() {
|
||||
_command.tryEmit(workingMode)
|
||||
}
|
||||
|
||||
fun setNewStatus(status: BleManagerStatus) {
|
||||
_status.value = status
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
_status.value = BleManagerStatus.CONNECTING
|
||||
_data.tryEmit(CSCData())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,6 @@ package no.nordicsemi.android.csc.data
|
||||
|
||||
internal sealed class CSCServiceCommand
|
||||
|
||||
internal data class SetWheelSizeCommand(val size: Int) : CSCServiceCommand()
|
||||
internal data class SetWheelSizeCommand(val wheelSize: WheelSize) : CSCServiceCommand()
|
||||
|
||||
internal object DisconnectCommand : CSCServiceCommand()
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package no.nordicsemi.android.csc.data
|
||||
|
||||
import no.nordicsemi.android.csc.view.CSCSettings
|
||||
|
||||
data class WheelSize(
|
||||
val value: Int = CSCSettings.DefaultWheelSize.VALUE,
|
||||
val name: String = CSCSettings.DefaultWheelSize.NAME
|
||||
)
|
||||
@@ -30,9 +30,7 @@ import androidx.annotation.FloatRange
|
||||
import no.nordicsemi.android.ble.common.callback.csc.CyclingSpeedAndCadenceMeasurementDataCallback
|
||||
import no.nordicsemi.android.ble.data.Data
|
||||
import no.nordicsemi.android.csc.data.CSCRepository
|
||||
import no.nordicsemi.android.csc.repository.CSCMeasurementParser.parse
|
||||
import no.nordicsemi.android.csc.view.CSCSettings
|
||||
import no.nordicsemi.android.log.LogContract
|
||||
import no.nordicsemi.android.csc.data.WheelSize
|
||||
import no.nordicsemi.android.service.BatteryManager
|
||||
import java.util.*
|
||||
|
||||
@@ -42,20 +40,20 @@ val CYCLING_SPEED_AND_CADENCE_SERVICE_UUID: UUID = UUID.fromString("00001816-000
|
||||
/** Cycling Speed and Cadence Measurement characteristic UUID. */
|
||||
private val CSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A5B-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
internal class CSCManager(context: Context, private val dataHolder: CSCRepository) : BatteryManager(context) {
|
||||
internal class CSCManager(context: Context, private val repository: CSCRepository) : BatteryManager(context) {
|
||||
|
||||
private var cscMeasurementCharacteristic: BluetoothGattCharacteristic? = null
|
||||
private var wheelSize = CSCSettings.DefaultWheelSize.VALUE
|
||||
private var wheelSize: WheelSize = WheelSize()
|
||||
|
||||
override fun onBatteryLevelChanged(batteryLevel: Int) {
|
||||
dataHolder.setBatteryLevel(batteryLevel)
|
||||
repository.setBatteryLevel(batteryLevel)
|
||||
}
|
||||
|
||||
override fun getGattCallback(): BatteryManagerGattCallback {
|
||||
return CSCManagerGattCallback()
|
||||
}
|
||||
|
||||
fun setWheelSize(value: Int) {
|
||||
fun setWheelSize(value: WheelSize) {
|
||||
wheelSize = value
|
||||
}
|
||||
|
||||
@@ -72,7 +70,7 @@ internal class CSCManager(context: Context, private val dataHolder: CSCRepositor
|
||||
.with(object : CyclingSpeedAndCadenceMeasurementDataCallback() {
|
||||
|
||||
override fun getWheelCircumference(): Float {
|
||||
return wheelSize.toFloat()
|
||||
return wheelSize.value.toFloat()
|
||||
}
|
||||
|
||||
override fun onDistanceChanged(
|
||||
@@ -81,7 +79,7 @@ internal class CSCManager(context: Context, private val dataHolder: CSCRepositor
|
||||
@FloatRange(from = 0.0) distance: Float,
|
||||
@FloatRange(from = 0.0) speed: Float
|
||||
) {
|
||||
dataHolder.setNewDistance(totalDistance, distance, speed)
|
||||
repository.setNewDistance(totalDistance, distance, speed, wheelSize)
|
||||
}
|
||||
|
||||
override fun onCrankDataChanged(
|
||||
@@ -89,7 +87,7 @@ internal class CSCManager(context: Context, private val dataHolder: CSCRepositor
|
||||
@FloatRange(from = 0.0) crankCadence: Float,
|
||||
gearRatio: Float
|
||||
) {
|
||||
dataHolder.setNewCrankCadence(crankCadence, gearRatio)
|
||||
repository.setNewCrankCadence(crankCadence, gearRatio, wheelSize)
|
||||
}
|
||||
|
||||
override fun onInvalidDataReceived(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package no.nordicsemi.android.csc.repository
|
||||
|
||||
import android.util.Log
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@@ -21,10 +22,14 @@ internal class CSCService : ForegroundBleService() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
status.onEach {
|
||||
repository.setNewStatus(it)
|
||||
}.launchIn(scope)
|
||||
|
||||
repository.command.onEach {
|
||||
when (it) {
|
||||
DisconnectCommand -> stopSelf()
|
||||
is SetWheelSizeCommand -> manager.setWheelSize(it.size)
|
||||
is SetWheelSizeCommand -> manager.setWheelSize(it.wheelSize)
|
||||
}.exhaustive
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
@@ -11,21 +11,41 @@ import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringArrayResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import no.nordicsemi.android.csc.R
|
||||
import no.nordicsemi.android.csc.data.CSCData
|
||||
import no.nordicsemi.android.csc.data.WheelSize
|
||||
import no.nordicsemi.android.material.you.RadioButtonGroup
|
||||
import no.nordicsemi.android.theme.view.ScreenSection
|
||||
import no.nordicsemi.android.theme.view.SectionTitle
|
||||
import no.nordicsemi.android.theme.view.dialog.FlowCanceled
|
||||
import no.nordicsemi.android.theme.view.dialog.ItemSelectedResult
|
||||
import no.nordicsemi.android.utils.exhaustive
|
||||
|
||||
@Composable
|
||||
internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
||||
if (state.showDialog) {
|
||||
SelectWheelSizeDialog { onEvent(it) }
|
||||
val showDialog = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
if (showDialog.value) {
|
||||
val wheelEntries = stringArrayResource(R.array.wheel_entries)
|
||||
val wheelValues = stringArrayResource(R.array.wheel_values)
|
||||
|
||||
SelectWheelSizeDialog {
|
||||
when (it) {
|
||||
FlowCanceled -> showDialog.value = false
|
||||
is ItemSelectedResult -> {
|
||||
onEvent(OnWheelSizeSelected(WheelSize(wheelValues[it.index].toInt(), wheelEntries[it.index])))
|
||||
showDialog.value = false
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
@@ -33,7 +53,7 @@ internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
SettingsSection(state, onEvent)
|
||||
SettingsSection(state, onEvent) { showDialog.value = true }
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
@@ -51,7 +71,7 @@ internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsSection(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
||||
private fun SettingsSection(state: CSCData, onEvent: (CSCViewEvent) -> Unit, onWheelButtonClick: () -> Unit) {
|
||||
ScreenSection {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
@@ -60,7 +80,7 @@ private fun SettingsSection(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
WheelSizeView(state, onEvent)
|
||||
WheelSizeView(state, onWheelButtonClick)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
|
||||
@@ -9,46 +9,40 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import no.nordicsemi.android.csc.R
|
||||
import no.nordicsemi.android.csc.data.CSCData
|
||||
import no.nordicsemi.android.csc.repository.CSCService
|
||||
import no.nordicsemi.android.csc.viewmodel.CSCViewModel
|
||||
import no.nordicsemi.android.theme.view.BackIconAppBar
|
||||
import no.nordicsemi.android.utils.isServiceRunning
|
||||
import no.nordicsemi.android.theme.view.DeviceConnectingView
|
||||
import no.nordicsemi.android.utils.exhaustive
|
||||
|
||||
@Composable
|
||||
fun CSCScreen(finishAction: () -> Unit) {
|
||||
val viewModel: CSCViewModel = hiltViewModel()
|
||||
val state = viewModel.state.collectAsState().value
|
||||
val isScreenActive = viewModel.isActive.collectAsState().value
|
||||
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(isScreenActive) {
|
||||
if (!isScreenActive) {
|
||||
finishAction()
|
||||
}
|
||||
if (context.isServiceRunning(CSCService::class.java.name)) {
|
||||
val intent = Intent(context, CSCService::class.java)
|
||||
context.stopService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect("start-service") {
|
||||
if (!context.isServiceRunning(CSCService::class.java.name)) {
|
||||
LaunchedEffect(state.isActive) {
|
||||
if (state.isActive) {
|
||||
val intent = Intent(context, CSCService::class.java)
|
||||
context.startService(intent)
|
||||
} else {
|
||||
finishAction()
|
||||
}
|
||||
}
|
||||
|
||||
CSCView(state) { viewModel.onEvent(it) }
|
||||
CSCView(state.viewState) { viewModel.onEvent(it) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CSCView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
||||
private fun CSCView(state: CSCViewState, onEvent: (CSCViewEvent) -> Unit) {
|
||||
Column {
|
||||
BackIconAppBar(stringResource(id = R.string.csc_title)) {
|
||||
onEvent(OnDisconnectButtonClick)
|
||||
}
|
||||
|
||||
CSCContentView(state) { onEvent(it) }
|
||||
when (state) {
|
||||
is DisplayDataState -> CSCContentView(state.data, onEvent)
|
||||
LoadingState -> DeviceConnectingView()
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package no.nordicsemi.android.csc.view
|
||||
|
||||
import no.nordicsemi.android.csc.data.CSCData
|
||||
|
||||
internal data class CSCState(
|
||||
val viewState: CSCViewState,
|
||||
val isActive: Boolean = true
|
||||
)
|
||||
|
||||
internal sealed class CSCViewState
|
||||
|
||||
internal object LoadingState : CSCViewState()
|
||||
|
||||
internal data class DisplayDataState(val data: CSCData) : CSCViewState()
|
||||
@@ -1,12 +1,10 @@
|
||||
package no.nordicsemi.android.csc.view
|
||||
|
||||
import no.nordicsemi.android.csc.data.WheelSize
|
||||
|
||||
internal sealed class CSCViewEvent
|
||||
|
||||
internal object OnShowEditWheelSizeDialogButtonClick : CSCViewEvent()
|
||||
|
||||
internal data class OnWheelSizeSelected(val wheelSize: Int, val wheelSizeDisplayInfo: String) : CSCViewEvent()
|
||||
|
||||
internal object OnCloseSelectWheelSizeDialog : CSCViewEvent()
|
||||
internal data class OnWheelSizeSelected(val wheelSize: WheelSize) : CSCViewEvent()
|
||||
|
||||
internal data class OnSelectedSpeedUnitSelected(val selectedSpeedUnit: SpeedUnit) : CSCViewEvent()
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.compose.ui.res.stringArrayResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import no.nordicsemi.android.csc.R
|
||||
import no.nordicsemi.android.csc.data.WheelSize
|
||||
import no.nordicsemi.android.material.you.NordicTheme
|
||||
import no.nordicsemi.android.theme.view.dialog.FlowCanceled
|
||||
import no.nordicsemi.android.theme.view.dialog.ItemSelectedResult
|
||||
@@ -15,16 +16,12 @@ import no.nordicsemi.android.theme.view.dialog.toAnnotatedString
|
||||
import no.nordicsemi.android.utils.exhaustive
|
||||
|
||||
@Composable
|
||||
internal fun SelectWheelSizeDialog(onEvent: (CSCViewEvent) -> Unit) {
|
||||
internal fun SelectWheelSizeDialog(onEvent: (StringListDialogResult) -> Unit) {
|
||||
val wheelEntries = stringArrayResource(R.array.wheel_entries)
|
||||
val wheelValues = stringArrayResource(R.array.wheel_values)
|
||||
|
||||
StringListDialog(createConfig(wheelEntries) {
|
||||
when (it) {
|
||||
FlowCanceled -> onEvent(OnCloseSelectWheelSizeDialog)
|
||||
is ItemSelectedResult ->
|
||||
onEvent(OnWheelSizeSelected(wheelValues[it.index].toInt(), wheelEntries[it.index]))
|
||||
}.exhaustive
|
||||
onEvent(it)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ import no.nordicsemi.android.csc.R
|
||||
import no.nordicsemi.android.csc.data.CSCData
|
||||
|
||||
@Composable
|
||||
internal fun WheelSizeView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
||||
OutlinedButton(onClick = { onEvent(OnShowEditWheelSizeDialogButtonClick) }) {
|
||||
internal fun WheelSizeView(state: CSCData, onClick: () -> Unit) {
|
||||
OutlinedButton(onClick = { onClick() }) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(0.5f),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -31,7 +31,7 @@ internal fun WheelSizeView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
||||
text = stringResource(id = R.string.csc_field_wheel_size),
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
Text(text = state.wheelSizeDisplay, style = MaterialTheme.typography.bodyMedium)
|
||||
Text(text = state.wheelSize.name, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
|
||||
Icon(Icons.Default.ArrowDropDown, contentDescription = "")
|
||||
|
||||
@@ -1,52 +1,61 @@
|
||||
package no.nordicsemi.android.csc.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import no.nordicsemi.android.csc.data.CSCRepository
|
||||
import no.nordicsemi.android.csc.data.DisconnectCommand
|
||||
import no.nordicsemi.android.csc.data.SetWheelSizeCommand
|
||||
import no.nordicsemi.android.csc.view.CSCState
|
||||
import no.nordicsemi.android.csc.view.CSCViewEvent
|
||||
import no.nordicsemi.android.csc.view.OnCloseSelectWheelSizeDialog
|
||||
import no.nordicsemi.android.csc.view.DisplayDataState
|
||||
import no.nordicsemi.android.csc.view.LoadingState
|
||||
import no.nordicsemi.android.csc.view.OnDisconnectButtonClick
|
||||
import no.nordicsemi.android.csc.view.OnSelectedSpeedUnitSelected
|
||||
import no.nordicsemi.android.csc.view.OnShowEditWheelSizeDialogButtonClick
|
||||
import no.nordicsemi.android.csc.view.OnWheelSizeSelected
|
||||
import no.nordicsemi.android.service.BleManagerStatus
|
||||
import no.nordicsemi.android.utils.exhaustive
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class CSCViewModel @Inject constructor(
|
||||
private val dataHolder: CSCRepository
|
||||
private val repository: CSCRepository
|
||||
) : ViewModel() {
|
||||
|
||||
val state = dataHolder.data
|
||||
val state = repository.data.combine(repository.status) { data, status ->
|
||||
when (status) {
|
||||
BleManagerStatus.CONNECTING -> CSCState(LoadingState)
|
||||
BleManagerStatus.OK -> CSCState(DisplayDataState(data))
|
||||
BleManagerStatus.DISCONNECTED -> CSCState(DisplayDataState(data), false)
|
||||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.Lazily, CSCState(LoadingState))
|
||||
|
||||
fun onEvent(event: CSCViewEvent) {
|
||||
when (event) {
|
||||
is OnSelectedSpeedUnitSelected -> onSelectedSpeedUnit(event)
|
||||
OnShowEditWheelSizeDialogButtonClick -> onShowDialogEvent()
|
||||
is OnWheelSizeSelected -> onWheelSizeChanged(event)
|
||||
OnDisconnectButtonClick -> onDisconnectButtonClick()
|
||||
OnCloseSelectWheelSizeDialog -> onHideDialogEvent()
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun onSelectedSpeedUnit(event: OnSelectedSpeedUnitSelected) {
|
||||
dataHolder.setSpeedUnit(event.selectedSpeedUnit)
|
||||
}
|
||||
|
||||
private fun onShowDialogEvent() {
|
||||
dataHolder.setDisplayWheelSizeDialog()
|
||||
repository.setSpeedUnit(event.selectedSpeedUnit)
|
||||
}
|
||||
|
||||
private fun onWheelSizeChanged(event: OnWheelSizeSelected) {
|
||||
dataHolder.setWheelSize(event.wheelSize, event.wheelSizeDisplayInfo)
|
||||
repository.sendNewServiceCommand(SetWheelSizeCommand(event.wheelSize))
|
||||
}
|
||||
|
||||
private fun onDisconnectButtonClick() {
|
||||
finish()
|
||||
dataHolder.clear()
|
||||
repository.sendNewServiceCommand(DisconnectCommand)
|
||||
repository.clear()
|
||||
}
|
||||
|
||||
private fun onHideDialogEvent() {
|
||||
dataHolder.setHideWheelSizeDialog()
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
repository.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package no.nordicsemi.dfu
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package no.nordicsemi.dfu.data
|
||||
|
||||
import android.net.Uri
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import no.nordicsemi.android.service.BleManagerStatus
|
||||
import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@@ -17,6 +21,12 @@ internal class DFURepository @Inject constructor(
|
||||
private val _data = MutableStateFlow<DFUData>(NoFileSelectedState())
|
||||
val data: StateFlow<DFUData> = _data.asStateFlow()
|
||||
|
||||
private val _command = MutableSharedFlow<DisconnectCommand>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST)
|
||||
val command = _command.asSharedFlow()
|
||||
|
||||
private val _status = MutableStateFlow(BleManagerStatus.CONNECTING)
|
||||
val status = _status.asStateFlow()
|
||||
|
||||
fun setZipFile(file: Uri) {
|
||||
val currentState = _data.value as NoFileSelectedState
|
||||
_data.value = fileManger.createFile(file)?.let {
|
||||
@@ -36,7 +46,16 @@ internal class DFURepository @Inject constructor(
|
||||
_data.value = FileInstallingState()
|
||||
}
|
||||
|
||||
fun sendNewCommand(command: DisconnectCommand) {
|
||||
_command.tryEmit(command)
|
||||
}
|
||||
|
||||
fun setNewStatus(status: BleManagerStatus) {
|
||||
_status.value = status
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
_status.value = BleManagerStatus.CONNECTING
|
||||
_data.value = NoFileSelectedState()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package no.nordicsemi.dfu.data
|
||||
|
||||
internal object DisconnectCommand
|
||||
@@ -28,10 +28,25 @@ import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import no.nordicsemi.android.dfu.DfuBaseService
|
||||
import no.nordicsemi.android.service.BleManagerStatus
|
||||
import no.nordicsemi.android.service.CloseableCoroutineScope
|
||||
import no.nordicsemi.dfu.R
|
||||
import no.nordicsemi.dfu.data.DFURepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class DFUService : DfuBaseService() {
|
||||
@AndroidEntryPoint
|
||||
internal class DFUService : DfuBaseService() {
|
||||
|
||||
private val scope = CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
|
||||
@Inject
|
||||
lateinit var repository: DFURepository
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
@@ -39,6 +54,12 @@ class DFUService : DfuBaseService() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
createDfuNotificationChannel(this)
|
||||
}
|
||||
|
||||
repository.command.onEach {
|
||||
stopSelf()
|
||||
}.launchIn(scope)
|
||||
|
||||
repository.setNewStatus(BleManagerStatus.OK)
|
||||
}
|
||||
|
||||
override fun getNotificationTarget(): Class<out Activity?>? {
|
||||
@@ -77,4 +98,10 @@ class DFUService : DfuBaseService() {
|
||||
context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager?.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
repository.setNewStatus(BleManagerStatus.DISCONNECTED)
|
||||
super.onDestroy()
|
||||
scope.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,12 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import no.nordicsemi.android.utils.exhaustive
|
||||
import no.nordicsemi.dfu.data.*
|
||||
import no.nordicsemi.dfu.data.DFUData
|
||||
import no.nordicsemi.dfu.data.FileInstallingState
|
||||
import no.nordicsemi.dfu.data.FileReadyState
|
||||
import no.nordicsemi.dfu.data.NoFileSelectedState
|
||||
import no.nordicsemi.dfu.data.UploadFailureState
|
||||
import no.nordicsemi.dfu.data.UploadSuccessState
|
||||
|
||||
@Composable
|
||||
internal fun DFUContentView(state: DFUData, onEvent: (DFUViewEvent) -> Unit) {
|
||||
|
||||
@@ -9,9 +9,9 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import no.nordicsemi.android.theme.view.BackIconAppBar
|
||||
import no.nordicsemi.android.utils.isServiceRunning
|
||||
import no.nordicsemi.android.theme.view.DeviceConnectingView
|
||||
import no.nordicsemi.android.utils.exhaustive
|
||||
import no.nordicsemi.dfu.R
|
||||
import no.nordicsemi.dfu.data.DFUData
|
||||
import no.nordicsemi.dfu.repository.DFUService
|
||||
import no.nordicsemi.dfu.viewmodel.DFUViewModel
|
||||
|
||||
@@ -19,36 +19,30 @@ import no.nordicsemi.dfu.viewmodel.DFUViewModel
|
||||
fun DFUScreen(finishAction: () -> Unit) {
|
||||
val viewModel: DFUViewModel = hiltViewModel()
|
||||
val state = viewModel.state.collectAsState().value
|
||||
val isScreenActive = viewModel.isActive.collectAsState().value
|
||||
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(isScreenActive) {
|
||||
if (!isScreenActive) {
|
||||
finishAction()
|
||||
}
|
||||
if (context.isServiceRunning(DFUService::class.java.name)) {
|
||||
val intent = Intent(context, DFUService::class.java)
|
||||
context.stopService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect("start-service") {
|
||||
if (!context.isServiceRunning(DFUService::class.java.name)) {
|
||||
LaunchedEffect(state.isActive) {
|
||||
if (state.isActive) {
|
||||
val intent = Intent(context, DFUService::class.java)
|
||||
context.startService(intent)
|
||||
} else {
|
||||
finishAction()
|
||||
}
|
||||
}
|
||||
|
||||
DFUView(state) { viewModel.onEvent(it) }
|
||||
DFUView(state.viewState) { viewModel.onEvent(it) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DFUView(state: DFUData, onEvent: (DFUViewEvent) -> Unit) {
|
||||
private fun DFUView(state: DFUViewState, onEvent: (DFUViewEvent) -> Unit) {
|
||||
Column {
|
||||
BackIconAppBar(stringResource(id = R.string.dfu_title)) {
|
||||
onEvent(OnDisconnectButtonClick)
|
||||
}
|
||||
|
||||
DFUContentView(state) { onEvent(it) }
|
||||
when (state) {
|
||||
is DisplayDataState -> DFUContentView(state.data) { onEvent(it) }
|
||||
LoadingState -> DeviceConnectingView()
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
|
||||
14
profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUState.kt
Normal file
14
profile_dfu/src/main/java/no/nordicsemi/dfu/view/DFUState.kt
Normal file
@@ -0,0 +1,14 @@
|
||||
package no.nordicsemi.dfu.view
|
||||
|
||||
import no.nordicsemi.dfu.data.DFUData
|
||||
|
||||
internal data class DFUState(
|
||||
val viewState: DFUViewState,
|
||||
val isActive: Boolean = true
|
||||
)
|
||||
|
||||
internal sealed class DFUViewState
|
||||
|
||||
internal object LoadingState : DFUViewState()
|
||||
|
||||
internal data class DisplayDataState(val data: DFUData) : DFUViewState()
|
||||
@@ -1,24 +1,28 @@
|
||||
package no.nordicsemi.dfu.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import no.nordicsemi.android.service.BleManagerStatus
|
||||
import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
|
||||
import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
|
||||
import no.nordicsemi.android.utils.exhaustive
|
||||
import no.nordicsemi.dfu.data.Completed
|
||||
import no.nordicsemi.dfu.data.DFUManager
|
||||
import no.nordicsemi.dfu.data.DFUProgressManager
|
||||
import no.nordicsemi.dfu.data.DFURepository
|
||||
import no.nordicsemi.dfu.data.DFUServiceStatus
|
||||
import no.nordicsemi.dfu.data.DisconnectCommand
|
||||
import no.nordicsemi.dfu.data.Error
|
||||
import no.nordicsemi.dfu.data.FileInstallingState
|
||||
import no.nordicsemi.dfu.data.FileReadyState
|
||||
import no.nordicsemi.dfu.data.NoFileSelectedState
|
||||
import no.nordicsemi.dfu.data.ZipFile
|
||||
import no.nordicsemi.dfu.view.DFUState
|
||||
import no.nordicsemi.dfu.view.DFUViewEvent
|
||||
import no.nordicsemi.dfu.view.DisplayDataState
|
||||
import no.nordicsemi.dfu.view.LoadingState
|
||||
import no.nordicsemi.dfu.view.OnDisconnectButtonClick
|
||||
import no.nordicsemi.dfu.view.OnInstallButtonClick
|
||||
import no.nordicsemi.dfu.view.OnPauseButtonClick
|
||||
@@ -32,13 +36,19 @@ internal class DFUViewModel @Inject constructor(
|
||||
private val progressManager: DFUProgressManager,
|
||||
private val deviceHolder: SelectedBluetoothDeviceHolder,
|
||||
private val dfuManager: DFUManager
|
||||
) : CloseableViewModel() {
|
||||
) : ViewModel() {
|
||||
|
||||
val state = repository.data.combine(progressManager.status) { state, status ->
|
||||
(state as? FileInstallingState)
|
||||
?.run { createInstallingStateWithNewStatus(state, status) }
|
||||
?: state
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, NoFileSelectedState())
|
||||
}.combine(repository.status) { data, status ->
|
||||
when (status) {
|
||||
BleManagerStatus.CONNECTING -> DFUState(LoadingState)
|
||||
BleManagerStatus.OK -> DFUState(DisplayDataState(data))
|
||||
BleManagerStatus.DISCONNECTED -> DFUState(DisplayDataState(data), false)
|
||||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.Lazily, DFUState(LoadingState))
|
||||
|
||||
init {
|
||||
progressManager.registerListener()
|
||||
@@ -58,9 +68,9 @@ internal class DFUViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun closeScreen() {
|
||||
repository.sendNewCommand(DisconnectCommand)
|
||||
repository.clear()
|
||||
deviceHolder.forgetDevice()
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun requireFile(): ZipFile {
|
||||
@@ -82,6 +92,7 @@ internal class DFUViewModel @Inject constructor(
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
repository.clear()
|
||||
progressManager.unregisterListener()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package no.nordicsemi.dfu
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package no.nordicsemi.android.gls
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
|
||||
@@ -3,6 +3,7 @@ package no.nordicsemi.android.gls.data
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import no.nordicsemi.android.service.BleManagerStatus
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -12,6 +13,9 @@ internal class GLSRepository @Inject constructor() {
|
||||
private val _data = MutableStateFlow(GLSData())
|
||||
val data: StateFlow<GLSData> = _data.asStateFlow()
|
||||
|
||||
private val _status = MutableStateFlow(BleManagerStatus.CONNECTING)
|
||||
val status = _status.asStateFlow()
|
||||
|
||||
fun addNewRecord(record: GLSRecord) {
|
||||
val newRecords = _data.value.records.toMutableList().apply {
|
||||
add(record)
|
||||
@@ -40,7 +44,12 @@ internal class GLSRepository @Inject constructor() {
|
||||
_data.tryEmit(_data.value.copy(batteryLevel = batteryLevel))
|
||||
}
|
||||
|
||||
fun setNewStatus(status: BleManagerStatus) {
|
||||
_status.value = status
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
_status.value = BleManagerStatus.CONNECTING
|
||||
_data.tryEmit(GLSData())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,17 +35,23 @@ 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.*
|
||||
import no.nordicsemi.android.ble.data.Data
|
||||
import no.nordicsemi.android.gls.data.*
|
||||
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.gls.data.CarbohydrateId
|
||||
import no.nordicsemi.android.gls.data.ConcentrationUnit
|
||||
import no.nordicsemi.android.gls.data.GLSRecord
|
||||
import no.nordicsemi.android.gls.data.GLSRepository
|
||||
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.RecordType
|
||||
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 java.util.*
|
||||
import javax.inject.Inject
|
||||
@@ -304,14 +310,7 @@ internal class GLSManager @Inject constructor(
|
||||
writeCharacteristic(
|
||||
recordAccessControlPointCharacteristic,
|
||||
RecordAccessControlPointData.reportLastStoredRecord()
|
||||
)
|
||||
.with { device: BluetoothDevice, data: Data ->
|
||||
log(
|
||||
LogContract.Log.Level.APPLICATION,
|
||||
"\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent"
|
||||
)
|
||||
}
|
||||
.enqueue()
|
||||
).enqueue()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -327,14 +326,7 @@ internal class GLSManager @Inject constructor(
|
||||
writeCharacteristic(
|
||||
recordAccessControlPointCharacteristic,
|
||||
RecordAccessControlPointData.reportFirstStoredRecord()
|
||||
)
|
||||
.with { device: BluetoothDevice, data: Data ->
|
||||
log(
|
||||
LogContract.Log.Level.APPLICATION,
|
||||
"\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent"
|
||||
)
|
||||
}
|
||||
.enqueue()
|
||||
).enqueue()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -351,14 +343,7 @@ internal class GLSManager @Inject constructor(
|
||||
writeCharacteristic(
|
||||
recordAccessControlPointCharacteristic,
|
||||
RecordAccessControlPointData.reportNumberOfAllStoredRecords()
|
||||
)
|
||||
.with { device: BluetoothDevice, data: Data ->
|
||||
log(
|
||||
LogContract.Log.Level.APPLICATION,
|
||||
"\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent"
|
||||
)
|
||||
}
|
||||
.enqueue()
|
||||
).enqueue()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -385,14 +370,7 @@ internal class GLSManager @Inject constructor(
|
||||
writeCharacteristic(
|
||||
recordAccessControlPointCharacteristic,
|
||||
RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber)
|
||||
)
|
||||
.with { device: BluetoothDevice, data: Data ->
|
||||
log(
|
||||
LogContract.Log.Level.APPLICATION,
|
||||
"\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent"
|
||||
)
|
||||
}
|
||||
.enqueue()
|
||||
).enqueue()
|
||||
// Info:
|
||||
// Operators OPERATOR_LESS_THEN_OR_EQUAL and OPERATOR_RANGE are not supported by Nordic Semiconductor Glucose Service in SDK 4.4.2.
|
||||
}
|
||||
@@ -407,14 +385,7 @@ internal class GLSManager @Inject constructor(
|
||||
writeCharacteristic(
|
||||
recordAccessControlPointCharacteristic,
|
||||
RecordAccessControlPointData.abortOperation()
|
||||
)
|
||||
.with { device: BluetoothDevice, data: Data ->
|
||||
log(
|
||||
LogContract.Log.Level.APPLICATION,
|
||||
"\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent"
|
||||
)
|
||||
}
|
||||
.enqueue()
|
||||
).enqueue()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -429,14 +400,7 @@ internal class GLSManager @Inject constructor(
|
||||
writeCharacteristic(
|
||||
recordAccessControlPointCharacteristic,
|
||||
RecordAccessControlPointData.deleteAllStoredRecords()
|
||||
)
|
||||
.with { device: BluetoothDevice, data: Data ->
|
||||
log(
|
||||
LogContract.Log.Level.APPLICATION,
|
||||
"\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent"
|
||||
)
|
||||
}
|
||||
.enqueue()
|
||||
).enqueue()
|
||||
|
||||
val elements = listOf(1, 2, 3)
|
||||
val result = elements.all { it > 3 }
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
/*
|
||||
* 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,14 @@
|
||||
package no.nordicsemi.android.gls.view
|
||||
|
||||
import no.nordicsemi.android.gls.data.GLSData
|
||||
|
||||
internal data class GLSState(
|
||||
val viewState: GLSViewState,
|
||||
val isActive: Boolean = true
|
||||
)
|
||||
|
||||
internal sealed class GLSViewState
|
||||
|
||||
internal object LoadingState : GLSViewState()
|
||||
|
||||
internal data class DisplayDataState(val data: GLSData) : GLSViewState()
|
||||
@@ -1,6 +1,14 @@
|
||||
package no.nordicsemi.android.gls.view
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package no.nordicsemi.android.gls.view
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -7,40 +8,43 @@ 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.DisconnectEvent
|
||||
import no.nordicsemi.android.gls.viewmodel.GLSScreenViewEvent
|
||||
import no.nordicsemi.android.gls.viewmodel.GLSViewModel
|
||||
import no.nordicsemi.android.theme.view.BackIconAppBar
|
||||
import no.nordicsemi.android.theme.view.DeviceConnectingView
|
||||
import no.nordicsemi.android.utils.exhaustive
|
||||
|
||||
@Composable
|
||||
fun GLSScreen(finishAction: () -> Unit) {
|
||||
val viewModel: GLSViewModel = hiltViewModel()
|
||||
val state = viewModel.state.collectAsState().value
|
||||
val isScreenActive = viewModel.isActive.collectAsState().value
|
||||
|
||||
LaunchedEffect("connect") {
|
||||
viewModel.connectDevice()
|
||||
}
|
||||
Log.d("AAATESTAAA", "$viewModel") //TODO fix screen rotation
|
||||
|
||||
LaunchedEffect(isScreenActive) {
|
||||
if (!isScreenActive) {
|
||||
LaunchedEffect(state.isActive) {
|
||||
if (state.isActive) {
|
||||
viewModel.connectDevice()
|
||||
} else {
|
||||
finishAction()
|
||||
}
|
||||
}
|
||||
|
||||
GLSView(state) {
|
||||
GLSView(state.viewState) {
|
||||
viewModel.onEvent(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GLSView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) {
|
||||
private fun GLSView(state: GLSViewState, onEvent: (GLSScreenViewEvent) -> Unit) {
|
||||
Column {
|
||||
BackIconAppBar(stringResource(id = R.string.gls_title)) {
|
||||
onEvent(DisconnectEvent)
|
||||
}
|
||||
|
||||
GLSContentView(state, onEvent)
|
||||
when (state) {
|
||||
is DisplayDataState -> GLSContentView(state.data, onEvent)
|
||||
LoadingState -> DeviceConnectingView()
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
package no.nordicsemi.android.gls.viewmodel
|
||||
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import no.nordicsemi.android.gls.data.GLSRepository
|
||||
import no.nordicsemi.android.gls.data.WorkingMode
|
||||
import no.nordicsemi.android.gls.repository.GLSManager
|
||||
import no.nordicsemi.android.gls.view.DisplayDataState
|
||||
import no.nordicsemi.android.gls.view.GLSState
|
||||
import no.nordicsemi.android.gls.view.LoadingState
|
||||
import no.nordicsemi.android.service.BleManagerStatus
|
||||
import no.nordicsemi.android.service.ConnectionObserverAdapter
|
||||
import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
|
||||
import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
|
||||
import no.nordicsemi.android.utils.exhaustive
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -13,10 +23,35 @@ import javax.inject.Inject
|
||||
internal class GLSViewModel @Inject constructor(
|
||||
private val glsManager: GLSManager,
|
||||
private val deviceHolder: SelectedBluetoothDeviceHolder,
|
||||
private val dataHolder: GLSRepository
|
||||
) : CloseableViewModel() {
|
||||
private val repository: GLSRepository
|
||||
) : ViewModel() {
|
||||
|
||||
val state = dataHolder.data
|
||||
val state = repository.data.combine(repository.status) { data, status ->
|
||||
when (status) {
|
||||
BleManagerStatus.CONNECTING -> GLSState(LoadingState)
|
||||
BleManagerStatus.OK -> GLSState(DisplayDataState(data))
|
||||
BleManagerStatus.DISCONNECTED -> GLSState(DisplayDataState(data), false)
|
||||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.Lazily, GLSState(LoadingState))
|
||||
|
||||
init {
|
||||
glsManager.setConnectionObserver(object : ConnectionObserverAdapter() {
|
||||
override fun onDeviceConnected(device: BluetoothDevice) {
|
||||
super.onDeviceConnected(device)
|
||||
repository.setNewStatus(BleManagerStatus.OK)
|
||||
}
|
||||
|
||||
override fun onDeviceFailedToConnect(device: BluetoothDevice, reason: Int) {
|
||||
super.onDeviceFailedToConnect(device, reason)
|
||||
repository.setNewStatus(BleManagerStatus.DISCONNECTED)
|
||||
}
|
||||
|
||||
override fun onDeviceDisconnected(device: BluetoothDevice, reason: Int) {
|
||||
super.onDeviceDisconnected(device, reason)
|
||||
repository.setNewStatus(BleManagerStatus.DISCONNECTED)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun onEvent(event: GLSScreenViewEvent) {
|
||||
when (event) {
|
||||
@@ -43,8 +78,12 @@ internal class GLSViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun disconnect() {
|
||||
finish()
|
||||
deviceHolder.forgetDevice()
|
||||
dataHolder.clear()
|
||||
glsManager.disconnect().enqueue()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
repository.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package no.nordicsemi.android.gls
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package no.nordicsemi.android.hrs
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
package no.nordicsemi.android.hrs.data
|
||||
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import no.nordicsemi.android.service.BleManagerStatus
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -11,6 +16,12 @@ internal class HRSRepository @Inject constructor() {
|
||||
private val _data = MutableStateFlow(HRSData())
|
||||
val data: StateFlow<HRSData> = _data
|
||||
|
||||
private val _command = MutableSharedFlow<DisconnectCommand>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST)
|
||||
val command = _command.asSharedFlow()
|
||||
|
||||
private val _status = MutableStateFlow(BleManagerStatus.CONNECTING)
|
||||
val status = _status.asStateFlow()
|
||||
|
||||
fun addNewHeartRate(heartRate: Int) {
|
||||
val result = _data.value.heartRates.toMutableList().apply {
|
||||
add(heartRate)
|
||||
@@ -26,7 +37,16 @@ internal class HRSRepository @Inject constructor() {
|
||||
_data.tryEmit(_data.value.copy(batteryLevel = batteryLevel))
|
||||
}
|
||||
|
||||
fun sendDisconnectCommand() {
|
||||
_command.tryEmit(DisconnectCommand)
|
||||
}
|
||||
|
||||
fun setNewStatus(status: BleManagerStatus) {
|
||||
_status.value = status
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
_status.value = BleManagerStatus.CONNECTING
|
||||
_data.tryEmit(HRSData())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package no.nordicsemi.android.hrs.data
|
||||
|
||||
internal object DisconnectCommand
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* 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.hrs.service
|
||||
|
||||
import no.nordicsemi.android.ble.data.Data
|
||||
|
||||
internal object BodySensorLocationParser {
|
||||
fun parse(data: Data): String {
|
||||
return when (data.getIntValue(Data.FORMAT_UINT8, 0)!!) {
|
||||
6 -> "Foot"
|
||||
5 -> "Ear Lobe"
|
||||
4 -> "Hand"
|
||||
3 -> "Finger"
|
||||
2 -> "Wrist"
|
||||
1 -> "Chest"
|
||||
0 -> "Other"
|
||||
else -> "Other"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,9 +30,7 @@ import androidx.annotation.IntRange
|
||||
import no.nordicsemi.android.ble.common.callback.hr.BodySensorLocationDataCallback
|
||||
import no.nordicsemi.android.ble.common.callback.hr.HeartRateMeasurementDataCallback
|
||||
import no.nordicsemi.android.ble.common.profile.hr.BodySensorLocation
|
||||
import no.nordicsemi.android.ble.data.Data
|
||||
import no.nordicsemi.android.hrs.data.HRSRepository
|
||||
import no.nordicsemi.android.log.LogContract
|
||||
import no.nordicsemi.android.service.BatteryManager
|
||||
import java.util.*
|
||||
|
||||
@@ -53,11 +51,6 @@ internal class HRSManager(context: Context, private val dataHolder: HRSRepositor
|
||||
|
||||
private val bodySensorLocationDataCallback = object : BodySensorLocationDataCallback() {
|
||||
|
||||
override fun onDataReceived(device: BluetoothDevice, data: Data) {
|
||||
log(LogContract.Log.Level.APPLICATION, "\"" + BodySensorLocationParser.parse(data) + "\" received")
|
||||
super.onDataReceived(device, data)
|
||||
}
|
||||
|
||||
override fun onBodySensorLocationReceived(
|
||||
device: BluetoothDevice,
|
||||
@BodySensorLocation sensorLocation: Int
|
||||
@@ -68,11 +61,6 @@ internal class HRSManager(context: Context, private val dataHolder: HRSRepositor
|
||||
|
||||
private val heartRateMeasurementDataCallback = object : HeartRateMeasurementDataCallback() {
|
||||
|
||||
override fun onDataReceived(device: BluetoothDevice, data: Data) {
|
||||
log(LogContract.Log.Level.APPLICATION, "\"" + HeartRateMeasurementParser.parse(data) + "\" received")
|
||||
super.onDataReceived(device, data)
|
||||
}
|
||||
|
||||
override fun onHeartRateMeasurementReceived(
|
||||
device: BluetoothDevice,
|
||||
@IntRange(from = 0) heartRate: Int,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package no.nordicsemi.android.hrs.service
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import no.nordicsemi.android.hrs.data.HRSRepository
|
||||
import no.nordicsemi.android.service.ForegroundBleService
|
||||
import javax.inject.Inject
|
||||
@@ -9,7 +11,19 @@ import javax.inject.Inject
|
||||
internal class HRSService : ForegroundBleService() {
|
||||
|
||||
@Inject
|
||||
lateinit var dataHolder: HRSRepository
|
||||
lateinit var repository: HRSRepository
|
||||
|
||||
override val manager: HRSManager by lazy { HRSManager(this, dataHolder) }
|
||||
override val manager: HRSManager by lazy { HRSManager(this, repository) }
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
status.onEach {
|
||||
repository.setNewStatus(it)
|
||||
}.launchIn(scope)
|
||||
|
||||
repository.command.onEach {
|
||||
stopSelf()
|
||||
}.launchIn(scope)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
/*
|
||||
* 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.hrs.service
|
||||
|
||||
import no.nordicsemi.android.ble.data.Data
|
||||
import java.util.*
|
||||
|
||||
internal object HeartRateMeasurementParser {
|
||||
|
||||
private const val HEART_RATE_VALUE_FORMAT: Byte = 0x01 // 1 bit
|
||||
private const val SENSOR_CONTACT_STATUS: Byte = 0x06 // 2 bits
|
||||
private const val ENERGY_EXPANDED_STATUS: Byte = 0x08 // 1 bit
|
||||
private const val RR_INTERVAL: Byte = 0x10 // 1 bit
|
||||
|
||||
fun parse(data: Data): String {
|
||||
var offset = 0
|
||||
val flags = data.getIntValue(Data.FORMAT_UINT8, offset++)!!
|
||||
|
||||
/*
|
||||
* false Heart Rate Value Format is set to UINT8. Units: beats per minute (bpm)
|
||||
* true Heart Rate Value Format is set to UINT16. Units: beats per minute (bpm)
|
||||
*/
|
||||
val value16bit = flags and HEART_RATE_VALUE_FORMAT.toInt() > 0
|
||||
|
||||
/*
|
||||
* 0 Sensor Contact feature is not supported in the current connection
|
||||
* 1 Sensor Contact feature is not supported in the current connection
|
||||
* 2 Sensor Contact feature is supported, but contact is not detected
|
||||
* 3 Sensor Contact feature is supported and contact is detected
|
||||
*/
|
||||
val sensorContactStatus = flags and SENSOR_CONTACT_STATUS.toInt() shr 1
|
||||
|
||||
/*
|
||||
* false Energy Expended field is not present
|
||||
* true Energy Expended field is present. Units: kilo Joules
|
||||
*/
|
||||
val energyExpandedStatus = flags and ENERGY_EXPANDED_STATUS.toInt() > 0
|
||||
|
||||
/*
|
||||
* false RR-Interval values are not present.
|
||||
* true One or more RR-Interval values are present. Units: 1/1024 seconds
|
||||
*/
|
||||
val rrIntervalStatus = flags and RR_INTERVAL.toInt() > 0
|
||||
|
||||
// heart rate value is 8 or 16 bit long
|
||||
val heartRateValue = data.getIntValue(
|
||||
if (value16bit) {
|
||||
Data.FORMAT_UINT16
|
||||
} else {
|
||||
Data.FORMAT_UINT8
|
||||
},
|
||||
offset++
|
||||
) // bits per minute
|
||||
if (value16bit) offset++
|
||||
|
||||
// energy expanded value is present if a flag was set
|
||||
var energyExpanded = -1
|
||||
if (energyExpandedStatus) energyExpanded = data.getIntValue(Data.FORMAT_UINT16, offset)!!
|
||||
offset += 2
|
||||
|
||||
// RR-interval is set when a flag is set
|
||||
val rrIntervals: MutableList<Float> = ArrayList()
|
||||
if (rrIntervalStatus) {
|
||||
var o = offset
|
||||
while (o < data.value!!.size) {
|
||||
val units = data.getIntValue(Data.FORMAT_UINT16, o)!!
|
||||
rrIntervals.add(units * 1000.0f / 1024.0f) // RR interval is in [1/1024s]
|
||||
o += 2
|
||||
}
|
||||
}
|
||||
val builder = StringBuilder()
|
||||
builder.append("Heart Rate Measurement: ").append(heartRateValue).append(" bpm")
|
||||
when (sensorContactStatus) {
|
||||
0, 1 -> builder.append(",\nSensor Contact Not Supported")
|
||||
2 -> builder.append(",\nContact is NOT Detected")
|
||||
3 -> builder.append(",\nContact is Detected")
|
||||
}
|
||||
if (energyExpandedStatus) {
|
||||
builder.append(",\nEnergy Expanded: ")
|
||||
.append(energyExpanded)
|
||||
.append(" kJ")
|
||||
}
|
||||
if (rrIntervalStatus) {
|
||||
builder.append(",\nRR Interval: ")
|
||||
for (interval in rrIntervals) builder.append(
|
||||
String.format(
|
||||
Locale.US,
|
||||
"%.02f ms, ",
|
||||
interval
|
||||
)
|
||||
)
|
||||
builder.setLength(builder.length - 2) // remove the ", " at the end
|
||||
}
|
||||
return builder.toString()
|
||||
}
|
||||
}
|
||||
@@ -9,46 +9,40 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import no.nordicsemi.android.hrs.R
|
||||
import no.nordicsemi.android.hrs.data.HRSData
|
||||
import no.nordicsemi.android.hrs.service.HRSService
|
||||
import no.nordicsemi.android.hrs.viewmodel.HRSViewModel
|
||||
import no.nordicsemi.android.theme.view.BackIconAppBar
|
||||
import no.nordicsemi.android.utils.isServiceRunning
|
||||
import no.nordicsemi.android.theme.view.DeviceConnectingView
|
||||
import no.nordicsemi.android.utils.exhaustive
|
||||
|
||||
@Composable
|
||||
fun HRSScreen(finishAction: () -> Unit) {
|
||||
val viewModel: HRSViewModel = 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(HRSService::class.java.name)) {
|
||||
val intent = Intent(context, HRSService::class.java)
|
||||
context.stopService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect("start-service") {
|
||||
if (!context.isServiceRunning(HRSService::class.java.name)) {
|
||||
LaunchedEffect(state.isActive) {
|
||||
if (state.isActive) {
|
||||
val intent = Intent(context, HRSService::class.java)
|
||||
context.startService(intent)
|
||||
} else {
|
||||
finishAction()
|
||||
}
|
||||
}
|
||||
|
||||
HRSView(state) { viewModel.onEvent(it) }
|
||||
HRSView(state.viewState) { viewModel.onEvent(it) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HRSView(state: HRSData, onEvent: (HRSScreenViewEvent) -> Unit) {
|
||||
private fun HRSView(state: HRSViewState, onEvent: (HRSScreenViewEvent) -> Unit) {
|
||||
Column {
|
||||
BackIconAppBar(stringResource(id = R.string.hrs_title)) {
|
||||
onEvent(DisconnectEvent)
|
||||
}
|
||||
|
||||
HRSContentView(state) { onEvent(it) }
|
||||
when (state) {
|
||||
is DisplayDataState -> HRSContentView(state.data) { onEvent(it) }
|
||||
LoadingState -> DeviceConnectingView()
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package no.nordicsemi.android.hrs.view
|
||||
|
||||
import no.nordicsemi.android.hrs.data.HRSData
|
||||
|
||||
internal data class HRSState(
|
||||
val viewState: HRSViewState,
|
||||
val isActive: Boolean = true
|
||||
)
|
||||
|
||||
internal sealed class HRSViewState
|
||||
|
||||
internal object LoadingState : HRSViewState()
|
||||
|
||||
internal data class DisplayDataState(val data: HRSData) : HRSViewState()
|
||||
@@ -1,18 +1,32 @@
|
||||
package no.nordicsemi.android.hrs.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import no.nordicsemi.android.hrs.data.HRSRepository
|
||||
import no.nordicsemi.android.hrs.view.DisconnectEvent
|
||||
import no.nordicsemi.android.hrs.view.DisplayDataState
|
||||
import no.nordicsemi.android.hrs.view.HRSScreenViewEvent
|
||||
import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
|
||||
import no.nordicsemi.android.hrs.view.HRSState
|
||||
import no.nordicsemi.android.hrs.view.LoadingState
|
||||
import no.nordicsemi.android.service.BleManagerStatus
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class HRSViewModel @Inject constructor(
|
||||
private val dataHolder: HRSRepository
|
||||
) : CloseableViewModel() {
|
||||
private val repository: HRSRepository
|
||||
) : ViewModel() {
|
||||
|
||||
val state = dataHolder.data
|
||||
val state = repository.data.combine(repository.status) { data, status ->
|
||||
when (status) {
|
||||
BleManagerStatus.CONNECTING -> HRSState(LoadingState)
|
||||
BleManagerStatus.OK -> HRSState(DisplayDataState(data))
|
||||
BleManagerStatus.DISCONNECTED -> HRSState(DisplayDataState(data), false)
|
||||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.Lazily, HRSState(LoadingState))
|
||||
|
||||
fun onEvent(event: HRSScreenViewEvent) {
|
||||
(event as? DisconnectEvent)?.let {
|
||||
@@ -21,7 +35,12 @@ internal class HRSViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun onDisconnectButtonClick() {
|
||||
finish()
|
||||
dataHolder.clear()
|
||||
repository.sendDisconnectCommand()
|
||||
repository.clear()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
repository.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package no.nordicsemi.android.hrs
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package no.nordicsemi.android.hts
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
package no.nordicsemi.android.hts.data
|
||||
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import no.nordicsemi.android.service.BleManagerStatus
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -11,6 +16,12 @@ internal class HTSRepository @Inject constructor() {
|
||||
private val _data = MutableStateFlow(HTSData())
|
||||
val data: StateFlow<HTSData> = _data
|
||||
|
||||
private val _command = MutableSharedFlow<DisconnectCommand>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST)
|
||||
val command = _command.asSharedFlow()
|
||||
|
||||
private val _status = MutableStateFlow(BleManagerStatus.CONNECTING)
|
||||
val status = _status.asStateFlow()
|
||||
|
||||
fun setNewTemperature(temperature: Float) {
|
||||
_data.tryEmit(_data.value.copy(temperatureValue = temperature))
|
||||
}
|
||||
@@ -23,7 +34,16 @@ internal class HTSRepository @Inject constructor() {
|
||||
_data.tryEmit(_data.value.copy(temperatureUnit = unit))
|
||||
}
|
||||
|
||||
fun sendDisconnectCommand() {
|
||||
_command.tryEmit(DisconnectCommand)
|
||||
}
|
||||
|
||||
fun setNewStatus(status: BleManagerStatus) {
|
||||
_status.value = status
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
_status.value = BleManagerStatus.CONNECTING
|
||||
_data.tryEmit(HTSData())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package no.nordicsemi.android.hts.data
|
||||
|
||||
internal object DisconnectCommand
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2015, Nordic Semiconductor
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package no.nordicsemi.android.hts.repository
|
||||
|
||||
import no.nordicsemi.android.ble.common.callback.DateTimeDataCallback
|
||||
import no.nordicsemi.android.ble.data.Data
|
||||
import java.util.*
|
||||
|
||||
internal object HTSDateTimeParser {
|
||||
/**
|
||||
* Parses the date and time info.
|
||||
*
|
||||
* @param data
|
||||
* @return time in human readable format
|
||||
*/
|
||||
fun parse(data: Data): String {
|
||||
return parse(data, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the date and time info. This data has 7 bytes
|
||||
*
|
||||
* @param data
|
||||
* @param offset
|
||||
* offset to start reading the time
|
||||
* @return time in human readable format
|
||||
*/
|
||||
/* package */
|
||||
@JvmStatic
|
||||
fun parse(data: Data, offset: Int): String {
|
||||
val calendar = DateTimeDataCallback.readDateTime(data, offset)
|
||||
return String.format(Locale.US, "%1\$te %1\$tb %1\$tY, %1\$tH:%1\$tM:%1\$tS", calendar)
|
||||
}
|
||||
}
|
||||
@@ -28,9 +28,7 @@ import android.content.Context
|
||||
import no.nordicsemi.android.ble.common.callback.ht.TemperatureMeasurementDataCallback
|
||||
import no.nordicsemi.android.ble.common.profile.ht.TemperatureType
|
||||
import no.nordicsemi.android.ble.common.profile.ht.TemperatureUnit
|
||||
import no.nordicsemi.android.ble.data.Data
|
||||
import no.nordicsemi.android.hts.data.HTSRepository
|
||||
import no.nordicsemi.android.log.LogContract
|
||||
import no.nordicsemi.android.service.BatteryManager
|
||||
import java.util.*
|
||||
|
||||
@@ -50,13 +48,6 @@ internal class HTSManager internal constructor(
|
||||
private var htCharacteristic: BluetoothGattCharacteristic? = null
|
||||
|
||||
private val temperatureMeasurementDataCallback = object : TemperatureMeasurementDataCallback() {
|
||||
override fun onDataReceived(device: BluetoothDevice, data: Data) {
|
||||
log(
|
||||
LogContract.Log.Level.APPLICATION,
|
||||
"\"" + HTSTemperatureMeasurementParser.parse(data) + "\" received"
|
||||
)
|
||||
super.onDataReceived(device, data)
|
||||
}
|
||||
|
||||
override fun onTemperatureMeasurementReceived(
|
||||
device: BluetoothDevice,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package no.nordicsemi.android.hts.repository
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import no.nordicsemi.android.hts.data.HTSRepository
|
||||
import no.nordicsemi.android.service.ForegroundBleService
|
||||
import javax.inject.Inject
|
||||
@@ -9,7 +11,19 @@ import javax.inject.Inject
|
||||
internal class HTSService : ForegroundBleService() {
|
||||
|
||||
@Inject
|
||||
lateinit var dataHolder: HTSRepository
|
||||
lateinit var repository: HTSRepository
|
||||
|
||||
override val manager: HTSManager by lazy { HTSManager(this, dataHolder) }
|
||||
override val manager: HTSManager by lazy { HTSManager(this, repository) }
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
status.onEach {
|
||||
repository.setNewStatus(it)
|
||||
}.launchIn(scope)
|
||||
|
||||
repository.command.onEach {
|
||||
stopSelf()
|
||||
}.launchIn(scope)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2015, Nordic Semiconductor
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package no.nordicsemi.android.hts.repository
|
||||
|
||||
import no.nordicsemi.android.ble.data.Data
|
||||
import java.util.*
|
||||
|
||||
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
|
||||
|
||||
internal object HTSTemperatureMeasurementParser {
|
||||
|
||||
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
|
||||
*/
|
||||
val fahrenheit = flags and TEMPERATURE_UNIT_FLAG.toInt() > 0
|
||||
|
||||
/*
|
||||
* false No Timestamp in the packet
|
||||
* true There is a timestamp information
|
||||
*/
|
||||
val timestampIncluded = flags and TIMESTAMP_FLAG.toInt() > 0
|
||||
|
||||
/*
|
||||
* false Temperature type is not included
|
||||
* true Temperature type included in the packet
|
||||
*/
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2015, Nordic Semiconductor
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
|
||||
* software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package no.nordicsemi.android.hts.repository
|
||||
|
||||
import no.nordicsemi.android.ble.data.Data
|
||||
|
||||
internal object HTSTemperatureTypeParser {
|
||||
fun parse(data: Data): String {
|
||||
return parse(data, 0)
|
||||
}
|
||||
|
||||
/* package */
|
||||
@JvmStatic
|
||||
fun parse(data: Data, offset: Int): String {
|
||||
return when (data.value!![offset].toInt()) {
|
||||
1 -> "Armpit"
|
||||
2 -> "Body (general)"
|
||||
3 -> "Ear (usually ear lobe)"
|
||||
4 -> "Finger"
|
||||
5 -> "Gastro-intestinal Tract"
|
||||
6 -> "Mouth"
|
||||
7 -> "Rectum"
|
||||
8 -> "Toe"
|
||||
9 -> "Tympanum (ear drum)"
|
||||
else -> "Unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
package no.nordicsemi.android.hts.view
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
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.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@@ -9,46 +9,40 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import no.nordicsemi.android.hts.R
|
||||
import no.nordicsemi.android.hts.data.HTSData
|
||||
import no.nordicsemi.android.hts.repository.HTSService
|
||||
import no.nordicsemi.android.hts.viewmodel.HTSViewModel
|
||||
import no.nordicsemi.android.theme.view.BackIconAppBar
|
||||
import no.nordicsemi.android.utils.isServiceRunning
|
||||
import no.nordicsemi.android.theme.view.DeviceConnectingView
|
||||
import no.nordicsemi.android.utils.exhaustive
|
||||
|
||||
@Composable
|
||||
fun HTSScreen(finishAction: () -> Unit) {
|
||||
val viewModel: HTSViewModel = 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(HTSService::class.java.name)) {
|
||||
val intent = Intent(context, HTSService::class.java)
|
||||
context.stopService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect("start-service") {
|
||||
if (!context.isServiceRunning(HTSService::class.java.name)) {
|
||||
LaunchedEffect(state.isActive) {
|
||||
if (state.isActive) {
|
||||
val intent = Intent(context, HTSService::class.java)
|
||||
context.startService(intent)
|
||||
} else {
|
||||
finishAction()
|
||||
}
|
||||
}
|
||||
|
||||
HTSView(state) { viewModel.onEvent(it) }
|
||||
HTSView(state.viewState) { viewModel.onEvent(it) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HTSView(state: HTSData, onEvent: (HTSScreenViewEvent) -> Unit) {
|
||||
private fun HTSView(state: HTSViewState, onEvent: (HTSScreenViewEvent) -> Unit) {
|
||||
Column {
|
||||
BackIconAppBar(stringResource(id = R.string.hts_title)) {
|
||||
onEvent(DisconnectEvent)
|
||||
}
|
||||
|
||||
HTSContentView(state) { onEvent(it) }
|
||||
when (state) {
|
||||
is DisplayDataState -> HTSContentView(state.data) { onEvent(it) }
|
||||
LoadingState -> DeviceConnectingView()
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package no.nordicsemi.android.hts.view
|
||||
|
||||
import no.nordicsemi.android.hts.data.HTSData
|
||||
|
||||
internal data class HTSState(
|
||||
val viewState: HTSViewState,
|
||||
val isActive: Boolean = true
|
||||
)
|
||||
|
||||
internal sealed class HTSViewState
|
||||
|
||||
internal object LoadingState : HTSViewState()
|
||||
|
||||
internal data class DisplayDataState(val data: HTSData) : HTSViewState()
|
||||
@@ -1,20 +1,34 @@
|
||||
package no.nordicsemi.android.hts.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import no.nordicsemi.android.hts.data.HTSRepository
|
||||
import no.nordicsemi.android.hts.view.DisconnectEvent
|
||||
import no.nordicsemi.android.hts.view.DisplayDataState
|
||||
import no.nordicsemi.android.hts.view.HTSScreenViewEvent
|
||||
import no.nordicsemi.android.hts.view.HTSState
|
||||
import no.nordicsemi.android.hts.view.LoadingState
|
||||
import no.nordicsemi.android.hts.view.OnTemperatureUnitSelected
|
||||
import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
|
||||
import no.nordicsemi.android.service.BleManagerStatus
|
||||
import no.nordicsemi.android.utils.exhaustive
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class HTSViewModel @Inject constructor(
|
||||
private val dataHolder: HTSRepository
|
||||
) : CloseableViewModel() {
|
||||
private val repository: HTSRepository
|
||||
) : ViewModel() {
|
||||
|
||||
val state = dataHolder.data
|
||||
val state = repository.data.combine(repository.status) { data, status ->
|
||||
when (status) {
|
||||
BleManagerStatus.CONNECTING -> HTSState(LoadingState)
|
||||
BleManagerStatus.OK -> HTSState(DisplayDataState(data))
|
||||
BleManagerStatus.DISCONNECTED -> HTSState(DisplayDataState(data), false)
|
||||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.Lazily, HTSState(LoadingState))
|
||||
|
||||
fun onEvent(event: HTSScreenViewEvent) {
|
||||
when (event) {
|
||||
@@ -24,11 +38,16 @@ internal class HTSViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun onDisconnectButtonClick() {
|
||||
finish()
|
||||
dataHolder.clear()
|
||||
repository.sendDisconnectCommand()
|
||||
repository.clear()
|
||||
}
|
||||
|
||||
private fun onTemperatureUnitSelected(event: OnTemperatureUnitSelected) {
|
||||
dataHolder.setTemperatureUnit(event.value)
|
||||
repository.setTemperatureUnit(event.value)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
repository.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package no.nordicsemi.android.hts
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package no.nordicsemi.android.prx
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
|
||||
@@ -5,3 +5,5 @@ internal sealed class PRXCommand
|
||||
internal object EnableAlarm : PRXCommand()
|
||||
|
||||
internal object DisableAlarm : PRXCommand()
|
||||
|
||||
internal object Disconnect : PRXCommand()
|
||||
|
||||
@@ -5,6 +5,8 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import no.nordicsemi.android.service.BleManagerStatus
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -17,6 +19,9 @@ internal class PRXRepository @Inject constructor() {
|
||||
private val _command = MutableSharedFlow<PRXCommand>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
val command = _command.asSharedFlow()
|
||||
|
||||
private val _status = MutableStateFlow(BleManagerStatus.CONNECTING)
|
||||
val status = _status.asStateFlow()
|
||||
|
||||
fun setBatteryLevel(batteryLevel: Int) {
|
||||
_data.tryEmit(_data.value.copy(batteryLevel = batteryLevel))
|
||||
}
|
||||
@@ -34,7 +39,12 @@ internal class PRXRepository @Inject constructor() {
|
||||
_command.tryEmit(command)
|
||||
}
|
||||
|
||||
fun clear(){
|
||||
fun setNewStatus(status: BleManagerStatus) {
|
||||
_status.value = status
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
_status.value = BleManagerStatus.CONNECTING
|
||||
_data.tryEmit(PRXData())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* 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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,9 +30,7 @@ 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.PRXRepository
|
||||
import no.nordicsemi.android.service.BatteryManager
|
||||
import java.util.*
|
||||
@@ -161,12 +159,6 @@ internal class PRXManager(
|
||||
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)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package no.nordicsemi.android.prx.service
|
||||
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import no.nordicsemi.android.prx.data.AlarmLevel
|
||||
import no.nordicsemi.android.prx.data.DisableAlarm
|
||||
import no.nordicsemi.android.prx.data.Disconnect
|
||||
import no.nordicsemi.android.prx.data.EnableAlarm
|
||||
import no.nordicsemi.android.prx.data.PRXRepository
|
||||
import no.nordicsemi.android.service.ForegroundBleService
|
||||
@@ -16,7 +16,7 @@ import javax.inject.Inject
|
||||
internal class PRXService : ForegroundBleService() {
|
||||
|
||||
@Inject
|
||||
lateinit var dataHolder: PRXRepository
|
||||
lateinit var repository: PRXRepository
|
||||
|
||||
@Inject
|
||||
lateinit var alarmHandler: AlarmHandler
|
||||
@@ -24,7 +24,7 @@ internal class PRXService : ForegroundBleService() {
|
||||
private var serverManager: ProximityServerManager = ProximityServerManager(this)
|
||||
|
||||
override val manager: PRXManager by lazy {
|
||||
PRXManager(this, dataHolder).apply {
|
||||
PRXManager(this, repository).apply {
|
||||
useServer(serverManager)
|
||||
}
|
||||
}
|
||||
@@ -34,14 +34,19 @@ internal class PRXService : ForegroundBleService() {
|
||||
|
||||
serverManager.open()
|
||||
|
||||
dataHolder.command.onEach {
|
||||
status.onEach {
|
||||
repository.setNewStatus(it)
|
||||
}.launchIn(scope)
|
||||
|
||||
repository.command.onEach {
|
||||
when (it) {
|
||||
DisableAlarm -> manager.writeImmediateAlert(false)
|
||||
EnableAlarm -> manager.writeImmediateAlert(true)
|
||||
Disconnect -> stopSelf()
|
||||
}.exhaustive
|
||||
}.launchIn(scope)
|
||||
|
||||
dataHolder.data.onEach {
|
||||
repository.data.onEach {
|
||||
if (it.localAlarmLevel != AlarmLevel.NONE) {
|
||||
alarmHandler.playAlarm()
|
||||
} else {
|
||||
|
||||
@@ -8,55 +8,42 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
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
|
||||
import no.nordicsemi.android.theme.view.DeviceConnectingView
|
||||
import no.nordicsemi.android.utils.exhaustive
|
||||
|
||||
@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)) {
|
||||
LaunchedEffect(state.isActive) {
|
||||
if (state.isActive) {
|
||||
val intent = Intent(context, PRXService::class.java)
|
||||
context.startService(intent)
|
||||
} else {
|
||||
finishAction()
|
||||
}
|
||||
}
|
||||
|
||||
PRXView(state) { viewModel.onEvent(it) }
|
||||
PRXView(state.viewState) { viewModel.onEvent(it) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PRXView(state: PRXData, onEvent: (PRXScreenViewEvent) -> Unit) {
|
||||
private fun PRXView(state: PRXViewState, onEvent: (PRXScreenViewEvent) -> Unit) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
BackIconAppBar(stringResource(id = R.string.prx_title)) {
|
||||
onEvent(DisconnectEvent)
|
||||
}
|
||||
|
||||
ContentView(state) { onEvent(it) }
|
||||
when (state) {
|
||||
is DisplayDataState -> ContentView(state.data) { onEvent(it) }
|
||||
LoadingState -> DeviceConnectingView()
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PRXViewPreview() {
|
||||
PRXView(PRXData()) { }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package no.nordicsemi.android.prx.view
|
||||
|
||||
import no.nordicsemi.android.prx.data.PRXData
|
||||
|
||||
internal data class PRXState(
|
||||
val viewState: PRXViewState,
|
||||
val isActive: Boolean = true
|
||||
)
|
||||
|
||||
internal sealed class PRXViewState
|
||||
|
||||
internal object LoadingState : PRXViewState()
|
||||
|
||||
internal data class DisplayDataState(val data: PRXData) : PRXViewState()
|
||||
@@ -1,34 +1,54 @@
|
||||
package no.nordicsemi.android.prx.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import no.nordicsemi.android.prx.data.DisableAlarm
|
||||
import no.nordicsemi.android.prx.data.Disconnect
|
||||
import no.nordicsemi.android.prx.data.EnableAlarm
|
||||
import no.nordicsemi.android.prx.data.PRXRepository
|
||||
import no.nordicsemi.android.prx.view.DisconnectEvent
|
||||
import no.nordicsemi.android.prx.view.DisplayDataState
|
||||
import no.nordicsemi.android.prx.view.LoadingState
|
||||
import no.nordicsemi.android.prx.view.PRXScreenViewEvent
|
||||
import no.nordicsemi.android.prx.view.PRXState
|
||||
import no.nordicsemi.android.prx.view.TurnOffAlert
|
||||
import no.nordicsemi.android.prx.view.TurnOnAlert
|
||||
import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
|
||||
import no.nordicsemi.android.service.BleManagerStatus
|
||||
import no.nordicsemi.android.utils.exhaustive
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class PRXViewModel @Inject constructor(
|
||||
private val dataHolder: PRXRepository
|
||||
) : CloseableViewModel() {
|
||||
private val repository: PRXRepository
|
||||
) : ViewModel() {
|
||||
|
||||
val state = dataHolder.data
|
||||
val state = repository.data.combine(repository.status) { data, status ->
|
||||
when (status) {
|
||||
BleManagerStatus.CONNECTING -> PRXState(LoadingState)
|
||||
BleManagerStatus.OK -> PRXState(DisplayDataState(data))
|
||||
BleManagerStatus.DISCONNECTED -> PRXState(DisplayDataState(data), false)
|
||||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.Lazily, PRXState(LoadingState))
|
||||
|
||||
fun onEvent(event: PRXScreenViewEvent) {
|
||||
when (event) {
|
||||
DisconnectEvent -> onDisconnectButtonClick()
|
||||
TurnOffAlert -> dataHolder.invokeCommand(DisableAlarm)
|
||||
TurnOnAlert -> dataHolder.invokeCommand(EnableAlarm)
|
||||
TurnOffAlert -> repository.invokeCommand(DisableAlarm)
|
||||
TurnOnAlert -> repository.invokeCommand(EnableAlarm)
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun onDisconnectButtonClick() {
|
||||
finish()
|
||||
dataHolder.clear()
|
||||
repository.invokeCommand(Disconnect)
|
||||
repository.clear()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
repository.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package no.nordicsemi.android.prx
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package no.nordicsemi.android.rscs
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
package no.nordicsemi.android.rscs.data
|
||||
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import no.nordicsemi.android.service.BleManagerStatus
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -9,7 +14,13 @@ import javax.inject.Singleton
|
||||
internal class RSCSRepository @Inject constructor() {
|
||||
|
||||
private val _data = MutableStateFlow(RSCSData())
|
||||
val data: StateFlow<RSCSData> = _data
|
||||
val data: StateFlow<RSCSData> = _data.asStateFlow()
|
||||
|
||||
private val _command = MutableSharedFlow<DisconnectCommand>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST)
|
||||
val command = _command.asSharedFlow()
|
||||
|
||||
private val _status = MutableStateFlow(BleManagerStatus.CONNECTING)
|
||||
val status = _status.asStateFlow()
|
||||
|
||||
fun setNewData(
|
||||
running: Boolean,
|
||||
@@ -27,11 +38,20 @@ internal class RSCSRepository @Inject constructor() {
|
||||
))
|
||||
}
|
||||
|
||||
fun setNewStatus(status: BleManagerStatus) {
|
||||
_status.value = status
|
||||
}
|
||||
|
||||
fun setBatteryLevel(batteryLevel: Int) {
|
||||
_data.tryEmit(_data.value.copy(batteryLevel = batteryLevel))
|
||||
}
|
||||
|
||||
fun sendDisconnectCommand() {
|
||||
_command.tryEmit(DisconnectCommand)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
_status.value = BleManagerStatus.CONNECTING
|
||||
_data.tryEmit(RSCSData())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package no.nordicsemi.android.rscs.data
|
||||
|
||||
internal object DisconnectCommand
|
||||
@@ -1,6 +1,8 @@
|
||||
package no.nordicsemi.android.rscs.service
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import no.nordicsemi.android.rscs.data.RSCSRepository
|
||||
import no.nordicsemi.android.service.ForegroundBleService
|
||||
import javax.inject.Inject
|
||||
@@ -9,7 +11,19 @@ import javax.inject.Inject
|
||||
internal class RSCSService : ForegroundBleService() {
|
||||
|
||||
@Inject
|
||||
lateinit var dataHolder: RSCSRepository
|
||||
lateinit var repository: RSCSRepository
|
||||
|
||||
override val manager: RSCSManager by lazy { RSCSManager(this, dataHolder) }
|
||||
override val manager: RSCSManager by lazy { RSCSManager(this, repository) }
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
status.onEach {
|
||||
repository.setNewStatus(it)
|
||||
}.launchIn(scope)
|
||||
|
||||
repository.command.onEach {
|
||||
stopSelf()
|
||||
}.launchIn(scope)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,46 +9,40 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import no.nordicsemi.android.rscs.R
|
||||
import no.nordicsemi.android.rscs.data.RSCSData
|
||||
import no.nordicsemi.android.rscs.service.RSCSService
|
||||
import no.nordicsemi.android.rscs.viewmodel.RSCSViewModel
|
||||
import no.nordicsemi.android.theme.view.BackIconAppBar
|
||||
import no.nordicsemi.android.utils.isServiceRunning
|
||||
import no.nordicsemi.android.theme.view.DeviceConnectingView
|
||||
import no.nordicsemi.android.utils.exhaustive
|
||||
|
||||
@Composable
|
||||
fun RSCSScreen(finishAction: () -> Unit) {
|
||||
val viewModel: RSCSViewModel = hiltViewModel()
|
||||
val state = viewModel.state.collectAsState().value
|
||||
val isScreenActive = viewModel.isActive.collectAsState().value
|
||||
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(isScreenActive) {
|
||||
if (!isScreenActive) {
|
||||
finishAction()
|
||||
}
|
||||
if (context.isServiceRunning(RSCSService::class.java.name)) {
|
||||
val intent = Intent(context, RSCSService::class.java)
|
||||
context.stopService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect("start-service") {
|
||||
if (!context.isServiceRunning(RSCSService::class.java.name)) {
|
||||
LaunchedEffect(state.isActive) {
|
||||
if (state.isActive) {
|
||||
val intent = Intent(context, RSCSService::class.java)
|
||||
context.startService(intent)
|
||||
} else {
|
||||
finishAction()
|
||||
}
|
||||
}
|
||||
|
||||
RSCSView(state) { viewModel.onEvent(it) }
|
||||
RSCSView(state.viewState) { viewModel.onEvent(it) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RSCSView(state: RSCSData, onEvent: (RSCScreenViewEvent) -> Unit) {
|
||||
private fun RSCSView(state: RSCSViewState, onEvent: (RSCScreenViewEvent) -> Unit) {
|
||||
Column {
|
||||
BackIconAppBar(stringResource(id = R.string.rscs_title)) {
|
||||
onEvent(DisconnectEvent)
|
||||
}
|
||||
|
||||
RSCSContentView(state) { onEvent(it) }
|
||||
when (state) {
|
||||
is DisplayDataState -> RSCSContentView(state.data) { onEvent(it) }
|
||||
LoadingState -> DeviceConnectingView()
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package no.nordicsemi.android.rscs.view
|
||||
|
||||
import no.nordicsemi.android.rscs.data.RSCSData
|
||||
|
||||
internal data class RSCSState(
|
||||
val viewState: RSCSViewState,
|
||||
val isActive: Boolean = true
|
||||
)
|
||||
|
||||
internal sealed class RSCSViewState
|
||||
|
||||
internal object LoadingState : RSCSViewState()
|
||||
|
||||
internal data class DisplayDataState(val data: RSCSData) : RSCSViewState()
|
||||
@@ -1,19 +1,33 @@
|
||||
package no.nordicsemi.android.rscs.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import no.nordicsemi.android.rscs.data.RSCSRepository
|
||||
import no.nordicsemi.android.rscs.view.DisconnectEvent
|
||||
import no.nordicsemi.android.rscs.view.DisplayDataState
|
||||
import no.nordicsemi.android.rscs.view.LoadingState
|
||||
import no.nordicsemi.android.rscs.view.RSCSState
|
||||
import no.nordicsemi.android.rscs.view.RSCScreenViewEvent
|
||||
import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
|
||||
import no.nordicsemi.android.service.BleManagerStatus
|
||||
import no.nordicsemi.android.utils.exhaustive
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class RSCSViewModel @Inject constructor(
|
||||
private val dataHolder: RSCSRepository
|
||||
) : CloseableViewModel() {
|
||||
private val repository: RSCSRepository
|
||||
) : ViewModel() {
|
||||
|
||||
val state = dataHolder.data
|
||||
val state = repository.data.combine(repository.status) { data, status ->
|
||||
when (status) {
|
||||
BleManagerStatus.CONNECTING -> RSCSState(LoadingState)
|
||||
BleManagerStatus.OK -> RSCSState(DisplayDataState(data))
|
||||
BleManagerStatus.DISCONNECTED -> RSCSState(DisplayDataState(data), false)
|
||||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.Lazily, RSCSState(LoadingState))
|
||||
|
||||
fun onEvent(event: RSCScreenViewEvent) {
|
||||
when (event) {
|
||||
@@ -22,7 +36,12 @@ internal class RSCSViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun onDisconnectButtonClick() {
|
||||
finish()
|
||||
dataHolder.clear()
|
||||
repository.sendDisconnectCommand()
|
||||
repository.clear()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
repository.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package no.nordicsemi.android.rscs
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user