mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2025-12-19 15:34:26 +01:00
Add connecting view to profiles
This commit is contained in:
@@ -1,13 +1,11 @@
|
|||||||
package no.nordicsemi.android.nrftoolbox
|
package no.nordicsemi.android.nrftoolbox
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
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.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package no.nordicsemi.android.nrftoolbox
|
|||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.os.ParcelUuid
|
import android.os.ParcelUuid
|
||||||
import android.util.Log
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -46,15 +45,15 @@ internal fun HomeScreen() {
|
|||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = NavigationId.HOME.id
|
startDestination = NavigationId.HOME.id
|
||||||
) {
|
) {
|
||||||
|
composable(NavigationId.HOME.id) {
|
||||||
|
HomeView(viewModel)
|
||||||
|
}
|
||||||
composable(NavigationId.SCANNER.id) {
|
composable(NavigationId.SCANNER.id) {
|
||||||
val profile = viewModel.profile!!
|
val profile = viewModel.profile!!
|
||||||
FindDeviceScreen(ParcelUuid(profile.uuid)) {
|
FindDeviceScreen(ParcelUuid(profile.uuid)) {
|
||||||
viewModel.onScannerFlowResult(it)
|
viewModel.onScannerFlowResult(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable(NavigationId.HOME.id) {
|
|
||||||
HomeView(viewModel)
|
|
||||||
}
|
|
||||||
composable(NavigationId.CSC.id) {
|
composable(NavigationId.CSC.id) {
|
||||||
CSCScreen(navigateUp)
|
CSCScreen(navigateUp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
package no.nordicsemi.android.nrftoolbox
|
package no.nordicsemi.android.nrftoolbox
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package no.nordicsemi.android.service
|
package no.nordicsemi.android.service
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
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.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import java.util.*
|
|||||||
</T> */
|
</T> */
|
||||||
abstract class BatteryManager(context: Context) : BleManager(context) {
|
abstract class BatteryManager(context: Context) : BleManager(context) {
|
||||||
|
|
||||||
|
private val TAG = "BLE-MANAGER"
|
||||||
|
|
||||||
private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null
|
private var batteryLevelCharacteristic: BluetoothGattCharacteristic? = null
|
||||||
|
|
||||||
private val batteryLevelDataCallback: DataReceivedCallback =
|
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() {
|
protected abstract inner class BatteryManagerGattCallback : BleManagerGattCallback() {
|
||||||
override fun initialize() {
|
override fun initialize() {
|
||||||
readBatteryLevelCharacteristic()
|
readBatteryLevelCharacteristic()
|
||||||
|
|||||||
@@ -26,9 +26,7 @@ import android.bluetooth.BluetoothDevice
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.util.Log
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.lifecycle.LifecycleService
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
@@ -90,6 +88,13 @@ abstract class BleProfileService : Service() {
|
|||||||
_status.value = BleManagerStatus.OK
|
_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) {
|
override fun onDeviceDisconnected(device: BluetoothDevice, reason: Int) {
|
||||||
super.onDeviceDisconnected(device, reason)
|
super.onDeviceDisconnected(device, reason)
|
||||||
_status.value = BleManagerStatus.DISCONNECTED
|
_status.value = BleManagerStatus.DISCONNECTED
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package no.nordicsemi.android.service
|
package no.nordicsemi.android.service
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancelChildren
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
@@ -9,6 +9,6 @@ class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineS
|
|||||||
override val coroutineContext: CoroutineContext = context
|
override val coroutineContext: CoroutineContext = context
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
coroutineContext.cancel()
|
coroutineContext.cancelChildren()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,34 @@
|
|||||||
package no.nordicsemi.android.service
|
package no.nordicsemi.android.service
|
||||||
|
|
||||||
import android.bluetooth.BluetoothDevice
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.util.Log
|
||||||
import no.nordicsemi.android.ble.observer.ConnectionObserver
|
import no.nordicsemi.android.ble.observer.ConnectionObserver
|
||||||
|
|
||||||
abstract class ConnectionObserverAdapter : 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
|
package no.nordicsemi.android.service
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
* 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.annotation.DrawableRes
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
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.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -19,6 +18,7 @@ import androidx.compose.material3.TextButton
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -62,18 +62,18 @@ fun StringListView(config: StringListDialogConfig) {
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
config.items.forEachIndexed { i, entry ->
|
config.items.forEachIndexed { i, entry ->
|
||||||
Column(modifier = Modifier.clickable { config.onResult(ItemSelectedResult(i)) }) {
|
Column(
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(10.dp))
|
||||||
|
.clickable { config.onResult(ItemSelectedResult(i)) }
|
||||||
|
.padding(8.dp),
|
||||||
|
) {
|
||||||
Row {
|
Row {
|
||||||
config.leftIcon?.let {
|
config.leftIcon?.let {
|
||||||
Image(
|
Image(
|
||||||
modifier = Modifier.padding(horizontal = 4.dp),
|
modifier = Modifier.padding(horizontal = 4.dp),
|
||||||
painter = painterResource(it),
|
painter = painterResource(it),
|
||||||
contentDescription = "Content image",
|
contentDescription = "Content image",
|
||||||
// colorFilter = ColorFilter.tint(
|
|
||||||
// NordicColors.NordicDarkGray.value()
|
|
||||||
// )
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
@@ -83,10 +83,6 @@ fun StringListView(config: StringListDialogConfig) {
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i != config.items.size - 1) {
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package no.nordicsemi.android.utils
|
package no.nordicsemi.android.utils
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import org.junit.Assert.*
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
package no.nordicsemi.android.utils
|
package no.nordicsemi.android.utils
|
||||||
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
import org.junit.Assert.*
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package no.nordicsemi.android.bps
|
package no.nordicsemi.android.bps
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
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.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest package="no.nordicsemi.android.bps">
|
||||||
package="no.nordicsemi.android.bps">
|
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -63,6 +63,7 @@ internal class BPSRepository @Inject constructor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
|
_status.value = BleManagerStatus.CONNECTING
|
||||||
_data.tryEmit(BPSData())
|
_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.common.profile.bp.BloodPressureTypes
|
||||||
import no.nordicsemi.android.ble.data.Data
|
import no.nordicsemi.android.ble.data.Data
|
||||||
import no.nordicsemi.android.bps.data.BPSRepository
|
import no.nordicsemi.android.bps.data.BPSRepository
|
||||||
import no.nordicsemi.android.log.LogContract
|
|
||||||
import no.nordicsemi.android.service.BatteryManager
|
import no.nordicsemi.android.service.BatteryManager
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ internal class BPSViewModel @Inject constructor(
|
|||||||
repository.setNewStatus(BleManagerStatus.OK)
|
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) {
|
override fun onDeviceDisconnected(device: BluetoothDevice, reason: Int) {
|
||||||
super.onDeviceDisconnected(device, reason)
|
super.onDeviceDisconnected(device, reason)
|
||||||
repository.setNewStatus(BleManagerStatus.DISCONNECTED)
|
repository.setNewStatus(BleManagerStatus.DISCONNECTED)
|
||||||
@@ -63,7 +68,13 @@ internal class BPSViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onDisconnectButtonClick() {
|
private fun onDisconnectButtonClick() {
|
||||||
deviceHolder.forgetDevice()
|
|
||||||
bpsManager.disconnect().enqueue()
|
bpsManager.disconnect().enqueue()
|
||||||
|
deviceHolder.forgetDevice()
|
||||||
|
repository.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
repository.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
package no.nordicsemi.android.bps
|
package no.nordicsemi.android.bps
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package no.nordicsemi.android.cgms
|
package no.nordicsemi.android.cgms
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
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.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package no.nordicsemi.android.cgms.data
|
package no.nordicsemi.android.cgms.data
|
||||||
|
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
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 no.nordicsemi.android.service.BleManagerStatus
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
@@ -39,6 +43,7 @@ internal class CGMRepository @Inject constructor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
|
_status.value = BleManagerStatus.CONNECTING
|
||||||
_data.tryEmit(CGMData())
|
_data.tryEmit(CGMData())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
package no.nordicsemi.android.cgms.view
|
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.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
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.R
|
||||||
import no.nordicsemi.android.cgms.data.CGMData
|
import no.nordicsemi.android.cgms.data.CGMData
|
||||||
import no.nordicsemi.android.cgms.data.CGMRecord
|
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.CGMServiceCommand
|
||||||
|
import no.nordicsemi.android.cgms.data.RequestStatus
|
||||||
import no.nordicsemi.android.material.you.CircularProgressIndicator
|
import no.nordicsemi.android.material.you.CircularProgressIndicator
|
||||||
import no.nordicsemi.android.theme.view.BatteryLevelView
|
import no.nordicsemi.android.theme.view.BatteryLevelView
|
||||||
import no.nordicsemi.android.theme.view.ScreenSection
|
import no.nordicsemi.android.theme.view.ScreenSection
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import no.nordicsemi.android.cgms.R
|
import no.nordicsemi.android.cgms.R
|
||||||
import no.nordicsemi.android.cgms.data.CGMRecord
|
import no.nordicsemi.android.cgms.data.CGMRecord
|
||||||
import no.nordicsemi.android.cgms.data.CGMServiceCommand
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,12 @@ internal class CGMScreenViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun disconnect() {
|
private fun disconnect() {
|
||||||
repository.clear()
|
|
||||||
repository.sendNewServiceCommand(CGMServiceCommand.DISCONNECT)
|
repository.sendNewServiceCommand(CGMServiceCommand.DISCONNECT)
|
||||||
|
repository.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
repository.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
package no.nordicsemi.android.cgms
|
package no.nordicsemi.android.cgms
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package no.nordicsemi.android.csc
|
package no.nordicsemi.android.csc
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
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.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package no.nordicsemi.android.csc.data
|
package no.nordicsemi.android.csc.data
|
||||||
|
|
||||||
import no.nordicsemi.android.csc.view.CSCSettings
|
|
||||||
import no.nordicsemi.android.csc.view.SpeedUnit
|
import no.nordicsemi.android.csc.view.SpeedUnit
|
||||||
import no.nordicsemi.android.material.you.RadioButtonItem
|
import no.nordicsemi.android.material.you.RadioButtonItem
|
||||||
import no.nordicsemi.android.material.you.RadioGroupViewEntity
|
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"
|
private const val DISPLAY_MPH = "mph"
|
||||||
|
|
||||||
internal data class CSCData(
|
internal data class CSCData(
|
||||||
val showDialog: Boolean = false,
|
|
||||||
val scanDevices: Boolean = false,
|
val scanDevices: Boolean = false,
|
||||||
val selectedSpeedUnit: SpeedUnit = SpeedUnit.M_S,
|
val selectedSpeedUnit: SpeedUnit = SpeedUnit.M_S,
|
||||||
val speed: Float = 0f,
|
val speed: Float = 0f,
|
||||||
@@ -20,8 +18,7 @@ internal data class CSCData(
|
|||||||
val totalDistance: Float = 0f,
|
val totalDistance: Float = 0f,
|
||||||
val gearRatio: Float = 0f,
|
val gearRatio: Float = 0f,
|
||||||
val batteryLevel: Int = 0,
|
val batteryLevel: Int = 0,
|
||||||
val wheelSize: Int = CSCSettings.DefaultWheelSize.VALUE,
|
val wheelSize: WheelSize = WheelSize()
|
||||||
val wheelSizeDisplay: String = CSCSettings.DefaultWheelSize.NAME
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val speedWithUnit = when (selectedSpeedUnit) {
|
private val speedWithUnit = when (selectedSpeedUnit) {
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import no.nordicsemi.android.csc.view.SpeedUnit
|
import no.nordicsemi.android.csc.view.SpeedUnit
|
||||||
|
import no.nordicsemi.android.service.BleManagerStatus
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -13,37 +15,38 @@ import javax.inject.Singleton
|
|||||||
internal class CSCRepository @Inject constructor() {
|
internal class CSCRepository @Inject constructor() {
|
||||||
|
|
||||||
private val _data = MutableStateFlow(CSCData())
|
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)
|
private val _command = MutableSharedFlow<CSCServiceCommand>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST)
|
||||||
val command = _command.asSharedFlow()
|
val command = _command.asSharedFlow()
|
||||||
|
|
||||||
fun setWheelSize(wheelSize: Int, wheelSizeDisplay: String) {
|
private val _status = MutableStateFlow(BleManagerStatus.CONNECTING)
|
||||||
_data.tryEmit(_data.value.copy(
|
val status = _status.asStateFlow()
|
||||||
wheelSize = wheelSize,
|
|
||||||
wheelSizeDisplay = wheelSizeDisplay,
|
|
||||||
showDialog = false
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSpeedUnit(selectedSpeedUnit: SpeedUnit) {
|
fun setSpeedUnit(selectedSpeedUnit: SpeedUnit) {
|
||||||
_data.tryEmit(_data.value.copy(selectedSpeedUnit = selectedSpeedUnit))
|
_data.tryEmit(_data.value.copy(selectedSpeedUnit = selectedSpeedUnit))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setHideWheelSizeDialog() {
|
fun setNewDistance(
|
||||||
_data.tryEmit(_data.value.copy(showDialog = false))
|
totalDistance: Float,
|
||||||
|
distance: Float,
|
||||||
|
speed: Float,
|
||||||
|
wheelSize: WheelSize
|
||||||
|
) {
|
||||||
|
_data.tryEmit(_data.value.copy(
|
||||||
|
totalDistance = totalDistance,
|
||||||
|
distance = distance,
|
||||||
|
speed = speed,
|
||||||
|
wheelSize = wheelSize
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setDisplayWheelSizeDialog() {
|
fun setNewCrankCadence(
|
||||||
_data.tryEmit(_data.value.copy(showDialog = true))
|
crankCadence: Float,
|
||||||
}
|
gearRatio: Float,
|
||||||
|
wheelSize: WheelSize
|
||||||
fun setNewDistance(totalDistance: Float, distance: Float, speed: Float) {
|
) {
|
||||||
_data.tryEmit(_data.value.copy(totalDistance = totalDistance, distance = distance, speed = speed))
|
_data.tryEmit(_data.value.copy(cadence = crankCadence, gearRatio = gearRatio, wheelSize = wheelSize))
|
||||||
}
|
|
||||||
|
|
||||||
fun setNewCrankCadence(crankCadence: Float, gearRatio: Float) {
|
|
||||||
_data.tryEmit(_data.value.copy(cadence = crankCadence, gearRatio = gearRatio))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setBatteryLevel(batteryLevel: Int) {
|
fun setBatteryLevel(batteryLevel: Int) {
|
||||||
@@ -54,7 +57,12 @@ internal class CSCRepository @Inject constructor() {
|
|||||||
_command.tryEmit(workingMode)
|
_command.tryEmit(workingMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setNewStatus(status: BleManagerStatus) {
|
||||||
|
_status.value = status
|
||||||
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
|
_status.value = BleManagerStatus.CONNECTING
|
||||||
_data.tryEmit(CSCData())
|
_data.tryEmit(CSCData())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ package no.nordicsemi.android.csc.data
|
|||||||
|
|
||||||
internal sealed class CSCServiceCommand
|
internal sealed class CSCServiceCommand
|
||||||
|
|
||||||
internal data class SetWheelSizeCommand(val size: Int) : CSCServiceCommand()
|
internal data class SetWheelSizeCommand(val wheelSize: WheelSize) : CSCServiceCommand()
|
||||||
|
|
||||||
internal object DisconnectCommand : 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.common.callback.csc.CyclingSpeedAndCadenceMeasurementDataCallback
|
||||||
import no.nordicsemi.android.ble.data.Data
|
import no.nordicsemi.android.ble.data.Data
|
||||||
import no.nordicsemi.android.csc.data.CSCRepository
|
import no.nordicsemi.android.csc.data.CSCRepository
|
||||||
import no.nordicsemi.android.csc.repository.CSCMeasurementParser.parse
|
import no.nordicsemi.android.csc.data.WheelSize
|
||||||
import no.nordicsemi.android.csc.view.CSCSettings
|
|
||||||
import no.nordicsemi.android.log.LogContract
|
|
||||||
import no.nordicsemi.android.service.BatteryManager
|
import no.nordicsemi.android.service.BatteryManager
|
||||||
import java.util.*
|
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. */
|
/** Cycling Speed and Cadence Measurement characteristic UUID. */
|
||||||
private val CSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A5B-0000-1000-8000-00805f9b34fb")
|
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 cscMeasurementCharacteristic: BluetoothGattCharacteristic? = null
|
||||||
private var wheelSize = CSCSettings.DefaultWheelSize.VALUE
|
private var wheelSize: WheelSize = WheelSize()
|
||||||
|
|
||||||
override fun onBatteryLevelChanged(batteryLevel: Int) {
|
override fun onBatteryLevelChanged(batteryLevel: Int) {
|
||||||
dataHolder.setBatteryLevel(batteryLevel)
|
repository.setBatteryLevel(batteryLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getGattCallback(): BatteryManagerGattCallback {
|
override fun getGattCallback(): BatteryManagerGattCallback {
|
||||||
return CSCManagerGattCallback()
|
return CSCManagerGattCallback()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setWheelSize(value: Int) {
|
fun setWheelSize(value: WheelSize) {
|
||||||
wheelSize = value
|
wheelSize = value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +70,7 @@ internal class CSCManager(context: Context, private val dataHolder: CSCRepositor
|
|||||||
.with(object : CyclingSpeedAndCadenceMeasurementDataCallback() {
|
.with(object : CyclingSpeedAndCadenceMeasurementDataCallback() {
|
||||||
|
|
||||||
override fun getWheelCircumference(): Float {
|
override fun getWheelCircumference(): Float {
|
||||||
return wheelSize.toFloat()
|
return wheelSize.value.toFloat()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDistanceChanged(
|
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) distance: Float,
|
||||||
@FloatRange(from = 0.0) speed: Float
|
@FloatRange(from = 0.0) speed: Float
|
||||||
) {
|
) {
|
||||||
dataHolder.setNewDistance(totalDistance, distance, speed)
|
repository.setNewDistance(totalDistance, distance, speed, wheelSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCrankDataChanged(
|
override fun onCrankDataChanged(
|
||||||
@@ -89,7 +87,7 @@ internal class CSCManager(context: Context, private val dataHolder: CSCRepositor
|
|||||||
@FloatRange(from = 0.0) crankCadence: Float,
|
@FloatRange(from = 0.0) crankCadence: Float,
|
||||||
gearRatio: Float
|
gearRatio: Float
|
||||||
) {
|
) {
|
||||||
dataHolder.setNewCrankCadence(crankCadence, gearRatio)
|
repository.setNewCrankCadence(crankCadence, gearRatio, wheelSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onInvalidDataReceived(
|
override fun onInvalidDataReceived(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package no.nordicsemi.android.csc.repository
|
package no.nordicsemi.android.csc.repository
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
@@ -21,10 +22,14 @@ internal class CSCService : ForegroundBleService() {
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
|
status.onEach {
|
||||||
|
repository.setNewStatus(it)
|
||||||
|
}.launchIn(scope)
|
||||||
|
|
||||||
repository.command.onEach {
|
repository.command.onEach {
|
||||||
when (it) {
|
when (it) {
|
||||||
DisconnectCommand -> stopSelf()
|
DisconnectCommand -> stopSelf()
|
||||||
is SetWheelSizeCommand -> manager.setWheelSize(it.size)
|
is SetWheelSizeCommand -> manager.setWheelSize(it.wheelSize)
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}.launchIn(scope)
|
}.launchIn(scope)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,21 +11,41 @@ import androidx.compose.material.icons.filled.Settings
|
|||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringArrayResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import no.nordicsemi.android.csc.R
|
import no.nordicsemi.android.csc.R
|
||||||
import no.nordicsemi.android.csc.data.CSCData
|
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.material.you.RadioButtonGroup
|
||||||
import no.nordicsemi.android.theme.view.ScreenSection
|
import no.nordicsemi.android.theme.view.ScreenSection
|
||||||
import no.nordicsemi.android.theme.view.SectionTitle
|
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
|
@Composable
|
||||||
internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
||||||
if (state.showDialog) {
|
val showDialog = rememberSaveable { mutableStateOf(false) }
|
||||||
SelectWheelSizeDialog { onEvent(it) }
|
|
||||||
|
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())) {
|
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||||
@@ -33,7 +53,7 @@ internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier.padding(16.dp)
|
modifier = Modifier.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
SettingsSection(state, onEvent)
|
SettingsSection(state, onEvent) { showDialog.value = true }
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
@@ -51,7 +71,7 @@ internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SettingsSection(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
private fun SettingsSection(state: CSCData, onEvent: (CSCViewEvent) -> Unit, onWheelButtonClick: () -> Unit) {
|
||||||
ScreenSection {
|
ScreenSection {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
@@ -60,7 +80,7 @@ private fun SettingsSection(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
WheelSizeView(state, onEvent)
|
WheelSizeView(state, onWheelButtonClick)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
|||||||
@@ -9,46 +9,40 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import no.nordicsemi.android.csc.R
|
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.repository.CSCService
|
||||||
import no.nordicsemi.android.csc.viewmodel.CSCViewModel
|
import no.nordicsemi.android.csc.viewmodel.CSCViewModel
|
||||||
import no.nordicsemi.android.theme.view.BackIconAppBar
|
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
|
@Composable
|
||||||
fun CSCScreen(finishAction: () -> Unit) {
|
fun CSCScreen(finishAction: () -> Unit) {
|
||||||
val viewModel: CSCViewModel = hiltViewModel()
|
val viewModel: CSCViewModel = hiltViewModel()
|
||||||
val state = viewModel.state.collectAsState().value
|
val state = viewModel.state.collectAsState().value
|
||||||
val isScreenActive = viewModel.isActive.collectAsState().value
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
LaunchedEffect(isScreenActive) {
|
LaunchedEffect(state.isActive) {
|
||||||
if (!isScreenActive) {
|
if (state.isActive) {
|
||||||
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)) {
|
|
||||||
val intent = Intent(context, CSCService::class.java)
|
val intent = Intent(context, CSCService::class.java)
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
|
} else {
|
||||||
|
finishAction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CSCView(state) { viewModel.onEvent(it) }
|
CSCView(state.viewState) { viewModel.onEvent(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CSCView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
private fun CSCView(state: CSCViewState, onEvent: (CSCViewEvent) -> Unit) {
|
||||||
Column {
|
Column {
|
||||||
BackIconAppBar(stringResource(id = R.string.csc_title)) {
|
BackIconAppBar(stringResource(id = R.string.csc_title)) {
|
||||||
onEvent(OnDisconnectButtonClick)
|
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
|
package no.nordicsemi.android.csc.view
|
||||||
|
|
||||||
|
import no.nordicsemi.android.csc.data.WheelSize
|
||||||
|
|
||||||
internal sealed class CSCViewEvent
|
internal sealed class CSCViewEvent
|
||||||
|
|
||||||
internal object OnShowEditWheelSizeDialogButtonClick : CSCViewEvent()
|
internal data class OnWheelSizeSelected(val wheelSize: WheelSize) : CSCViewEvent()
|
||||||
|
|
||||||
internal data class OnWheelSizeSelected(val wheelSize: Int, val wheelSizeDisplayInfo: String) : CSCViewEvent()
|
|
||||||
|
|
||||||
internal object OnCloseSelectWheelSizeDialog : CSCViewEvent()
|
|
||||||
|
|
||||||
internal data class OnSelectedSpeedUnitSelected(val selectedSpeedUnit: SpeedUnit) : 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.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import no.nordicsemi.android.csc.R
|
import no.nordicsemi.android.csc.R
|
||||||
|
import no.nordicsemi.android.csc.data.WheelSize
|
||||||
import no.nordicsemi.android.material.you.NordicTheme
|
import no.nordicsemi.android.material.you.NordicTheme
|
||||||
import no.nordicsemi.android.theme.view.dialog.FlowCanceled
|
import no.nordicsemi.android.theme.view.dialog.FlowCanceled
|
||||||
import no.nordicsemi.android.theme.view.dialog.ItemSelectedResult
|
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
|
import no.nordicsemi.android.utils.exhaustive
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SelectWheelSizeDialog(onEvent: (CSCViewEvent) -> Unit) {
|
internal fun SelectWheelSizeDialog(onEvent: (StringListDialogResult) -> Unit) {
|
||||||
val wheelEntries = stringArrayResource(R.array.wheel_entries)
|
val wheelEntries = stringArrayResource(R.array.wheel_entries)
|
||||||
val wheelValues = stringArrayResource(R.array.wheel_values)
|
val wheelValues = stringArrayResource(R.array.wheel_values)
|
||||||
|
|
||||||
StringListDialog(createConfig(wheelEntries) {
|
StringListDialog(createConfig(wheelEntries) {
|
||||||
when (it) {
|
onEvent(it)
|
||||||
FlowCanceled -> onEvent(OnCloseSelectWheelSizeDialog)
|
|
||||||
is ItemSelectedResult ->
|
|
||||||
onEvent(OnWheelSizeSelected(wheelValues[it.index].toInt(), wheelEntries[it.index]))
|
|
||||||
}.exhaustive
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import no.nordicsemi.android.csc.R
|
|||||||
import no.nordicsemi.android.csc.data.CSCData
|
import no.nordicsemi.android.csc.data.CSCData
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun WheelSizeView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
internal fun WheelSizeView(state: CSCData, onClick: () -> Unit) {
|
||||||
OutlinedButton(onClick = { onEvent(OnShowEditWheelSizeDialogButtonClick) }) {
|
OutlinedButton(onClick = { onClick() }) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(0.5f),
|
modifier = Modifier.fillMaxWidth(0.5f),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -31,7 +31,7 @@ internal fun WheelSizeView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
|||||||
text = stringResource(id = R.string.csc_field_wheel_size),
|
text = stringResource(id = R.string.csc_field_wheel_size),
|
||||||
style = MaterialTheme.typography.labelSmall
|
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 = "")
|
Icon(Icons.Default.ArrowDropDown, contentDescription = "")
|
||||||
|
|||||||
@@ -1,52 +1,61 @@
|
|||||||
package no.nordicsemi.android.csc.viewmodel
|
package no.nordicsemi.android.csc.viewmodel
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
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.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.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.OnDisconnectButtonClick
|
||||||
import no.nordicsemi.android.csc.view.OnSelectedSpeedUnitSelected
|
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.csc.view.OnWheelSizeSelected
|
||||||
|
import no.nordicsemi.android.service.BleManagerStatus
|
||||||
import no.nordicsemi.android.utils.exhaustive
|
import no.nordicsemi.android.utils.exhaustive
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
internal class CSCViewModel @Inject constructor(
|
internal class CSCViewModel @Inject constructor(
|
||||||
private val dataHolder: CSCRepository
|
private val repository: CSCRepository
|
||||||
) : ViewModel() {
|
) : 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) {
|
fun onEvent(event: CSCViewEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is OnSelectedSpeedUnitSelected -> onSelectedSpeedUnit(event)
|
is OnSelectedSpeedUnitSelected -> onSelectedSpeedUnit(event)
|
||||||
OnShowEditWheelSizeDialogButtonClick -> onShowDialogEvent()
|
|
||||||
is OnWheelSizeSelected -> onWheelSizeChanged(event)
|
is OnWheelSizeSelected -> onWheelSizeChanged(event)
|
||||||
OnDisconnectButtonClick -> onDisconnectButtonClick()
|
OnDisconnectButtonClick -> onDisconnectButtonClick()
|
||||||
OnCloseSelectWheelSizeDialog -> onHideDialogEvent()
|
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSelectedSpeedUnit(event: OnSelectedSpeedUnitSelected) {
|
private fun onSelectedSpeedUnit(event: OnSelectedSpeedUnitSelected) {
|
||||||
dataHolder.setSpeedUnit(event.selectedSpeedUnit)
|
repository.setSpeedUnit(event.selectedSpeedUnit)
|
||||||
}
|
|
||||||
|
|
||||||
private fun onShowDialogEvent() {
|
|
||||||
dataHolder.setDisplayWheelSizeDialog()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onWheelSizeChanged(event: OnWheelSizeSelected) {
|
private fun onWheelSizeChanged(event: OnWheelSizeSelected) {
|
||||||
dataHolder.setWheelSize(event.wheelSize, event.wheelSizeDisplayInfo)
|
repository.sendNewServiceCommand(SetWheelSizeCommand(event.wheelSize))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onDisconnectButtonClick() {
|
private fun onDisconnectButtonClick() {
|
||||||
finish()
|
repository.sendNewServiceCommand(DisconnectCommand)
|
||||||
dataHolder.clear()
|
repository.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onHideDialogEvent() {
|
override fun onCleared() {
|
||||||
dataHolder.setHideWheelSizeDialog()
|
super.onCleared()
|
||||||
|
repository.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package no.nordicsemi.dfu
|
package no.nordicsemi.dfu
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
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.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
package no.nordicsemi.dfu.data
|
package no.nordicsemi.dfu.data
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import no.nordicsemi.android.service.BleManagerStatus
|
||||||
import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
|
import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
@@ -17,6 +21,12 @@ internal class DFURepository @Inject constructor(
|
|||||||
private val _data = MutableStateFlow<DFUData>(NoFileSelectedState())
|
private val _data = MutableStateFlow<DFUData>(NoFileSelectedState())
|
||||||
val data: StateFlow<DFUData> = _data.asStateFlow()
|
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) {
|
fun setZipFile(file: Uri) {
|
||||||
val currentState = _data.value as NoFileSelectedState
|
val currentState = _data.value as NoFileSelectedState
|
||||||
_data.value = fileManger.createFile(file)?.let {
|
_data.value = fileManger.createFile(file)?.let {
|
||||||
@@ -36,7 +46,16 @@ internal class DFURepository @Inject constructor(
|
|||||||
_data.value = FileInstallingState()
|
_data.value = FileInstallingState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sendNewCommand(command: DisconnectCommand) {
|
||||||
|
_command.tryEmit(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setNewStatus(status: BleManagerStatus) {
|
||||||
|
_status.value = status
|
||||||
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
|
_status.value = BleManagerStatus.CONNECTING
|
||||||
_data.value = NoFileSelectedState()
|
_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.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.RequiresApi
|
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.dfu.DfuBaseService
|
||||||
|
import no.nordicsemi.android.service.BleManagerStatus
|
||||||
|
import no.nordicsemi.android.service.CloseableCoroutineScope
|
||||||
import no.nordicsemi.dfu.R
|
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() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
@@ -39,6 +54,12 @@ class DFUService : DfuBaseService() {
|
|||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
createDfuNotificationChannel(this)
|
createDfuNotificationChannel(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
repository.command.onEach {
|
||||||
|
stopSelf()
|
||||||
|
}.launchIn(scope)
|
||||||
|
|
||||||
|
repository.setNewStatus(BleManagerStatus.OK)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getNotificationTarget(): Class<out Activity?>? {
|
override fun getNotificationTarget(): Class<out Activity?>? {
|
||||||
@@ -77,4 +98,10 @@ class DFUService : DfuBaseService() {
|
|||||||
context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||||
notificationManager?.createNotificationChannel(channel)
|
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.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import no.nordicsemi.android.utils.exhaustive
|
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
|
@Composable
|
||||||
internal fun DFUContentView(state: DFUData, onEvent: (DFUViewEvent) -> Unit) {
|
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.compose.ui.res.stringResource
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import no.nordicsemi.android.theme.view.BackIconAppBar
|
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.R
|
||||||
import no.nordicsemi.dfu.data.DFUData
|
|
||||||
import no.nordicsemi.dfu.repository.DFUService
|
import no.nordicsemi.dfu.repository.DFUService
|
||||||
import no.nordicsemi.dfu.viewmodel.DFUViewModel
|
import no.nordicsemi.dfu.viewmodel.DFUViewModel
|
||||||
|
|
||||||
@@ -19,36 +19,30 @@ import no.nordicsemi.dfu.viewmodel.DFUViewModel
|
|||||||
fun DFUScreen(finishAction: () -> Unit) {
|
fun DFUScreen(finishAction: () -> Unit) {
|
||||||
val viewModel: DFUViewModel = hiltViewModel()
|
val viewModel: DFUViewModel = hiltViewModel()
|
||||||
val state = viewModel.state.collectAsState().value
|
val state = viewModel.state.collectAsState().value
|
||||||
val isScreenActive = viewModel.isActive.collectAsState().value
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
LaunchedEffect(isScreenActive) {
|
LaunchedEffect(state.isActive) {
|
||||||
if (!isScreenActive) {
|
if (state.isActive) {
|
||||||
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)) {
|
|
||||||
val intent = Intent(context, DFUService::class.java)
|
val intent = Intent(context, DFUService::class.java)
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
|
} else {
|
||||||
|
finishAction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DFUView(state) { viewModel.onEvent(it) }
|
DFUView(state.viewState) { viewModel.onEvent(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DFUView(state: DFUData, onEvent: (DFUViewEvent) -> Unit) {
|
private fun DFUView(state: DFUViewState, onEvent: (DFUViewEvent) -> Unit) {
|
||||||
Column {
|
Column {
|
||||||
BackIconAppBar(stringResource(id = R.string.dfu_title)) {
|
BackIconAppBar(stringResource(id = R.string.dfu_title)) {
|
||||||
onEvent(OnDisconnectButtonClick)
|
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
|
package no.nordicsemi.dfu.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import no.nordicsemi.android.service.BleManagerStatus
|
||||||
import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
|
import no.nordicsemi.android.service.SelectedBluetoothDeviceHolder
|
||||||
import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
|
|
||||||
import no.nordicsemi.android.utils.exhaustive
|
import no.nordicsemi.android.utils.exhaustive
|
||||||
import no.nordicsemi.dfu.data.Completed
|
import no.nordicsemi.dfu.data.Completed
|
||||||
import no.nordicsemi.dfu.data.DFUManager
|
import no.nordicsemi.dfu.data.DFUManager
|
||||||
import no.nordicsemi.dfu.data.DFUProgressManager
|
import no.nordicsemi.dfu.data.DFUProgressManager
|
||||||
import no.nordicsemi.dfu.data.DFURepository
|
import no.nordicsemi.dfu.data.DFURepository
|
||||||
import no.nordicsemi.dfu.data.DFUServiceStatus
|
import no.nordicsemi.dfu.data.DFUServiceStatus
|
||||||
|
import no.nordicsemi.dfu.data.DisconnectCommand
|
||||||
import no.nordicsemi.dfu.data.Error
|
import no.nordicsemi.dfu.data.Error
|
||||||
import no.nordicsemi.dfu.data.FileInstallingState
|
import no.nordicsemi.dfu.data.FileInstallingState
|
||||||
import no.nordicsemi.dfu.data.FileReadyState
|
import no.nordicsemi.dfu.data.FileReadyState
|
||||||
import no.nordicsemi.dfu.data.NoFileSelectedState
|
|
||||||
import no.nordicsemi.dfu.data.ZipFile
|
import no.nordicsemi.dfu.data.ZipFile
|
||||||
|
import no.nordicsemi.dfu.view.DFUState
|
||||||
import no.nordicsemi.dfu.view.DFUViewEvent
|
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.OnDisconnectButtonClick
|
||||||
import no.nordicsemi.dfu.view.OnInstallButtonClick
|
import no.nordicsemi.dfu.view.OnInstallButtonClick
|
||||||
import no.nordicsemi.dfu.view.OnPauseButtonClick
|
import no.nordicsemi.dfu.view.OnPauseButtonClick
|
||||||
@@ -32,13 +36,19 @@ internal class DFUViewModel @Inject constructor(
|
|||||||
private val progressManager: DFUProgressManager,
|
private val progressManager: DFUProgressManager,
|
||||||
private val deviceHolder: SelectedBluetoothDeviceHolder,
|
private val deviceHolder: SelectedBluetoothDeviceHolder,
|
||||||
private val dfuManager: DFUManager
|
private val dfuManager: DFUManager
|
||||||
) : CloseableViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val state = repository.data.combine(progressManager.status) { state, status ->
|
val state = repository.data.combine(progressManager.status) { state, status ->
|
||||||
(state as? FileInstallingState)
|
(state as? FileInstallingState)
|
||||||
?.run { createInstallingStateWithNewStatus(state, status) }
|
?.run { createInstallingStateWithNewStatus(state, status) }
|
||||||
?: state
|
?: 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 {
|
init {
|
||||||
progressManager.registerListener()
|
progressManager.registerListener()
|
||||||
@@ -58,9 +68,9 @@ internal class DFUViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun closeScreen() {
|
private fun closeScreen() {
|
||||||
|
repository.sendNewCommand(DisconnectCommand)
|
||||||
repository.clear()
|
repository.clear()
|
||||||
deviceHolder.forgetDevice()
|
deviceHolder.forgetDevice()
|
||||||
finish()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requireFile(): ZipFile {
|
private fun requireFile(): ZipFile {
|
||||||
@@ -82,6 +92,7 @@ internal class DFUViewModel @Inject constructor(
|
|||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
|
repository.clear()
|
||||||
progressManager.unregisterListener()
|
progressManager.unregisterListener()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
package no.nordicsemi.dfu
|
package no.nordicsemi.dfu
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package no.nordicsemi.android.gls
|
package no.nordicsemi.android.gls
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
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.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* 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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import no.nordicsemi.android.service.BleManagerStatus
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -12,6 +13,9 @@ internal class GLSRepository @Inject constructor() {
|
|||||||
private val _data = MutableStateFlow(GLSData())
|
private val _data = MutableStateFlow(GLSData())
|
||||||
val data: StateFlow<GLSData> = _data.asStateFlow()
|
val data: StateFlow<GLSData> = _data.asStateFlow()
|
||||||
|
|
||||||
|
private val _status = MutableStateFlow(BleManagerStatus.CONNECTING)
|
||||||
|
val status = _status.asStateFlow()
|
||||||
|
|
||||||
fun addNewRecord(record: GLSRecord) {
|
fun addNewRecord(record: GLSRecord) {
|
||||||
val newRecords = _data.value.records.toMutableList().apply {
|
val newRecords = _data.value.records.toMutableList().apply {
|
||||||
add(record)
|
add(record)
|
||||||
@@ -40,7 +44,12 @@ internal class GLSRepository @Inject constructor() {
|
|||||||
_data.tryEmit(_data.value.copy(batteryLevel = batteryLevel))
|
_data.tryEmit(_data.value.copy(batteryLevel = batteryLevel))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setNewStatus(status: BleManagerStatus) {
|
||||||
|
_status.value = status
|
||||||
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
|
_status.value = BleManagerStatus.CONNECTING
|
||||||
_data.tryEmit(GLSData())
|
_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.RACPErrorCode
|
||||||
import no.nordicsemi.android.ble.common.profile.RecordAccessControlPointCallback.RACPOpCode
|
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.GlucoseMeasurementCallback.GlucoseStatus
|
||||||
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.*
|
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Carbohydrate
|
||||||
import no.nordicsemi.android.ble.data.Data
|
import no.nordicsemi.android.ble.common.profile.glucose.GlucoseMeasurementContextCallback.Health
|
||||||
import no.nordicsemi.android.gls.data.*
|
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.CarbohydrateId
|
||||||
import no.nordicsemi.android.gls.data.ConcentrationUnit
|
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.HealthStatus
|
||||||
|
import no.nordicsemi.android.gls.data.MeasurementContext
|
||||||
import no.nordicsemi.android.gls.data.MedicationId
|
import no.nordicsemi.android.gls.data.MedicationId
|
||||||
import no.nordicsemi.android.gls.data.MedicationUnit
|
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.TestType
|
||||||
import no.nordicsemi.android.gls.data.TypeOfMeal
|
import no.nordicsemi.android.gls.data.TypeOfMeal
|
||||||
import no.nordicsemi.android.log.LogContract
|
|
||||||
import no.nordicsemi.android.service.BatteryManager
|
import no.nordicsemi.android.service.BatteryManager
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -304,14 +310,7 @@ internal class GLSManager @Inject constructor(
|
|||||||
writeCharacteristic(
|
writeCharacteristic(
|
||||||
recordAccessControlPointCharacteristic,
|
recordAccessControlPointCharacteristic,
|
||||||
RecordAccessControlPointData.reportLastStoredRecord()
|
RecordAccessControlPointData.reportLastStoredRecord()
|
||||||
)
|
).enqueue()
|
||||||
.with { device: BluetoothDevice, data: Data ->
|
|
||||||
log(
|
|
||||||
LogContract.Log.Level.APPLICATION,
|
|
||||||
"\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.enqueue()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -327,14 +326,7 @@ internal class GLSManager @Inject constructor(
|
|||||||
writeCharacteristic(
|
writeCharacteristic(
|
||||||
recordAccessControlPointCharacteristic,
|
recordAccessControlPointCharacteristic,
|
||||||
RecordAccessControlPointData.reportFirstStoredRecord()
|
RecordAccessControlPointData.reportFirstStoredRecord()
|
||||||
)
|
).enqueue()
|
||||||
.with { device: BluetoothDevice, data: Data ->
|
|
||||||
log(
|
|
||||||
LogContract.Log.Level.APPLICATION,
|
|
||||||
"\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.enqueue()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -351,14 +343,7 @@ internal class GLSManager @Inject constructor(
|
|||||||
writeCharacteristic(
|
writeCharacteristic(
|
||||||
recordAccessControlPointCharacteristic,
|
recordAccessControlPointCharacteristic,
|
||||||
RecordAccessControlPointData.reportNumberOfAllStoredRecords()
|
RecordAccessControlPointData.reportNumberOfAllStoredRecords()
|
||||||
)
|
).enqueue()
|
||||||
.with { device: BluetoothDevice, data: Data ->
|
|
||||||
log(
|
|
||||||
LogContract.Log.Level.APPLICATION,
|
|
||||||
"\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.enqueue()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -385,14 +370,7 @@ internal class GLSManager @Inject constructor(
|
|||||||
writeCharacteristic(
|
writeCharacteristic(
|
||||||
recordAccessControlPointCharacteristic,
|
recordAccessControlPointCharacteristic,
|
||||||
RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber)
|
RecordAccessControlPointData.reportStoredRecordsGreaterThenOrEqualTo(sequenceNumber)
|
||||||
)
|
).enqueue()
|
||||||
.with { device: BluetoothDevice, data: Data ->
|
|
||||||
log(
|
|
||||||
LogContract.Log.Level.APPLICATION,
|
|
||||||
"\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.enqueue()
|
|
||||||
// Info:
|
// Info:
|
||||||
// Operators OPERATOR_LESS_THEN_OR_EQUAL and OPERATOR_RANGE are not supported by Nordic Semiconductor Glucose Service in SDK 4.4.2.
|
// 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(
|
writeCharacteristic(
|
||||||
recordAccessControlPointCharacteristic,
|
recordAccessControlPointCharacteristic,
|
||||||
RecordAccessControlPointData.abortOperation()
|
RecordAccessControlPointData.abortOperation()
|
||||||
)
|
).enqueue()
|
||||||
.with { device: BluetoothDevice, data: Data ->
|
|
||||||
log(
|
|
||||||
LogContract.Log.Level.APPLICATION,
|
|
||||||
"\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.enqueue()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -429,14 +400,7 @@ internal class GLSManager @Inject constructor(
|
|||||||
writeCharacteristic(
|
writeCharacteristic(
|
||||||
recordAccessControlPointCharacteristic,
|
recordAccessControlPointCharacteristic,
|
||||||
RecordAccessControlPointData.deleteAllStoredRecords()
|
RecordAccessControlPointData.deleteAllStoredRecords()
|
||||||
)
|
).enqueue()
|
||||||
.with { device: BluetoothDevice, data: Data ->
|
|
||||||
log(
|
|
||||||
LogContract.Log.Level.APPLICATION,
|
|
||||||
"\"" + GLSRecordAccessControlPointParser.parse(data) + "\" sent"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.enqueue()
|
|
||||||
|
|
||||||
val elements = listOf(1, 2, 3)
|
val elements = listOf(1, 2, 3)
|
||||||
val result = elements.all { it > 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
|
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.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package no.nordicsemi.android.gls.view
|
package no.nordicsemi.android.gls.view
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -7,40 +8,43 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import no.nordicsemi.android.gls.R
|
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.DisconnectEvent
|
||||||
import no.nordicsemi.android.gls.viewmodel.GLSScreenViewEvent
|
import no.nordicsemi.android.gls.viewmodel.GLSScreenViewEvent
|
||||||
import no.nordicsemi.android.gls.viewmodel.GLSViewModel
|
import no.nordicsemi.android.gls.viewmodel.GLSViewModel
|
||||||
import no.nordicsemi.android.theme.view.BackIconAppBar
|
import no.nordicsemi.android.theme.view.BackIconAppBar
|
||||||
|
import no.nordicsemi.android.theme.view.DeviceConnectingView
|
||||||
|
import no.nordicsemi.android.utils.exhaustive
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun GLSScreen(finishAction: () -> Unit) {
|
fun GLSScreen(finishAction: () -> Unit) {
|
||||||
val viewModel: GLSViewModel = hiltViewModel()
|
val viewModel: GLSViewModel = hiltViewModel()
|
||||||
val state = viewModel.state.collectAsState().value
|
val state = viewModel.state.collectAsState().value
|
||||||
val isScreenActive = viewModel.isActive.collectAsState().value
|
|
||||||
|
|
||||||
LaunchedEffect("connect") {
|
Log.d("AAATESTAAA", "$viewModel") //TODO fix screen rotation
|
||||||
|
|
||||||
|
LaunchedEffect(state.isActive) {
|
||||||
|
if (state.isActive) {
|
||||||
viewModel.connectDevice()
|
viewModel.connectDevice()
|
||||||
}
|
} else {
|
||||||
|
|
||||||
LaunchedEffect(isScreenActive) {
|
|
||||||
if (!isScreenActive) {
|
|
||||||
finishAction()
|
finishAction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GLSView(state) {
|
GLSView(state.viewState) {
|
||||||
viewModel.onEvent(it)
|
viewModel.onEvent(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun GLSView(state: GLSData, onEvent: (GLSScreenViewEvent) -> Unit) {
|
private fun GLSView(state: GLSViewState, onEvent: (GLSScreenViewEvent) -> Unit) {
|
||||||
Column {
|
Column {
|
||||||
BackIconAppBar(stringResource(id = R.string.gls_title)) {
|
BackIconAppBar(stringResource(id = R.string.gls_title)) {
|
||||||
onEvent(DisconnectEvent)
|
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
|
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 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.GLSRepository
|
||||||
import no.nordicsemi.android.gls.data.WorkingMode
|
import no.nordicsemi.android.gls.data.WorkingMode
|
||||||
import no.nordicsemi.android.gls.repository.GLSManager
|
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.service.SelectedBluetoothDeviceHolder
|
||||||
import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
|
|
||||||
import no.nordicsemi.android.utils.exhaustive
|
import no.nordicsemi.android.utils.exhaustive
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -13,10 +23,35 @@ import javax.inject.Inject
|
|||||||
internal class GLSViewModel @Inject constructor(
|
internal class GLSViewModel @Inject constructor(
|
||||||
private val glsManager: GLSManager,
|
private val glsManager: GLSManager,
|
||||||
private val deviceHolder: SelectedBluetoothDeviceHolder,
|
private val deviceHolder: SelectedBluetoothDeviceHolder,
|
||||||
private val dataHolder: GLSRepository
|
private val repository: GLSRepository
|
||||||
) : CloseableViewModel() {
|
) : 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) {
|
fun onEvent(event: GLSScreenViewEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
@@ -43,8 +78,12 @@ internal class GLSViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun disconnect() {
|
private fun disconnect() {
|
||||||
finish()
|
|
||||||
deviceHolder.forgetDevice()
|
deviceHolder.forgetDevice()
|
||||||
dataHolder.clear()
|
glsManager.disconnect().enqueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
repository.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
package no.nordicsemi.android.gls
|
package no.nordicsemi.android.gls
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package no.nordicsemi.android.hrs
|
package no.nordicsemi.android.hrs
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
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.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
package no.nordicsemi.android.hrs.data
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
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.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -11,6 +16,12 @@ internal class HRSRepository @Inject constructor() {
|
|||||||
private val _data = MutableStateFlow(HRSData())
|
private val _data = MutableStateFlow(HRSData())
|
||||||
val data: StateFlow<HRSData> = _data
|
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) {
|
fun addNewHeartRate(heartRate: Int) {
|
||||||
val result = _data.value.heartRates.toMutableList().apply {
|
val result = _data.value.heartRates.toMutableList().apply {
|
||||||
add(heartRate)
|
add(heartRate)
|
||||||
@@ -26,7 +37,16 @@ internal class HRSRepository @Inject constructor() {
|
|||||||
_data.tryEmit(_data.value.copy(batteryLevel = batteryLevel))
|
_data.tryEmit(_data.value.copy(batteryLevel = batteryLevel))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sendDisconnectCommand() {
|
||||||
|
_command.tryEmit(DisconnectCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setNewStatus(status: BleManagerStatus) {
|
||||||
|
_status.value = status
|
||||||
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
|
_status.value = BleManagerStatus.CONNECTING
|
||||||
_data.tryEmit(HRSData())
|
_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.BodySensorLocationDataCallback
|
||||||
import no.nordicsemi.android.ble.common.callback.hr.HeartRateMeasurementDataCallback
|
import no.nordicsemi.android.ble.common.callback.hr.HeartRateMeasurementDataCallback
|
||||||
import no.nordicsemi.android.ble.common.profile.hr.BodySensorLocation
|
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.hrs.data.HRSRepository
|
||||||
import no.nordicsemi.android.log.LogContract
|
|
||||||
import no.nordicsemi.android.service.BatteryManager
|
import no.nordicsemi.android.service.BatteryManager
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -53,11 +51,6 @@ internal class HRSManager(context: Context, private val dataHolder: HRSRepositor
|
|||||||
|
|
||||||
private val bodySensorLocationDataCallback = object : BodySensorLocationDataCallback() {
|
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(
|
override fun onBodySensorLocationReceived(
|
||||||
device: BluetoothDevice,
|
device: BluetoothDevice,
|
||||||
@BodySensorLocation sensorLocation: Int
|
@BodySensorLocation sensorLocation: Int
|
||||||
@@ -68,11 +61,6 @@ internal class HRSManager(context: Context, private val dataHolder: HRSRepositor
|
|||||||
|
|
||||||
private val heartRateMeasurementDataCallback = object : HeartRateMeasurementDataCallback() {
|
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(
|
override fun onHeartRateMeasurementReceived(
|
||||||
device: BluetoothDevice,
|
device: BluetoothDevice,
|
||||||
@IntRange(from = 0) heartRate: Int,
|
@IntRange(from = 0) heartRate: Int,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package no.nordicsemi.android.hrs.service
|
package no.nordicsemi.android.hrs.service
|
||||||
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
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.hrs.data.HRSRepository
|
||||||
import no.nordicsemi.android.service.ForegroundBleService
|
import no.nordicsemi.android.service.ForegroundBleService
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -9,7 +11,19 @@ import javax.inject.Inject
|
|||||||
internal class HRSService : ForegroundBleService() {
|
internal class HRSService : ForegroundBleService() {
|
||||||
|
|
||||||
@Inject
|
@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.compose.ui.res.stringResource
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import no.nordicsemi.android.hrs.R
|
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.service.HRSService
|
||||||
import no.nordicsemi.android.hrs.viewmodel.HRSViewModel
|
import no.nordicsemi.android.hrs.viewmodel.HRSViewModel
|
||||||
import no.nordicsemi.android.theme.view.BackIconAppBar
|
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
|
@Composable
|
||||||
fun HRSScreen(finishAction: () -> Unit) {
|
fun HRSScreen(finishAction: () -> Unit) {
|
||||||
val viewModel: HRSViewModel = hiltViewModel()
|
val viewModel: HRSViewModel = hiltViewModel()
|
||||||
val state = viewModel.state.collectAsState().value
|
val state = viewModel.state.collectAsState().value
|
||||||
val isActive = viewModel.isActive.collectAsState().value
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
LaunchedEffect(isActive) {
|
LaunchedEffect(state.isActive) {
|
||||||
if (!isActive) {
|
if (state.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)) {
|
|
||||||
val intent = Intent(context, HRSService::class.java)
|
val intent = Intent(context, HRSService::class.java)
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
|
} else {
|
||||||
|
finishAction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HRSView(state) { viewModel.onEvent(it) }
|
HRSView(state.viewState) { viewModel.onEvent(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun HRSView(state: HRSData, onEvent: (HRSScreenViewEvent) -> Unit) {
|
private fun HRSView(state: HRSViewState, onEvent: (HRSScreenViewEvent) -> Unit) {
|
||||||
Column {
|
Column {
|
||||||
BackIconAppBar(stringResource(id = R.string.hrs_title)) {
|
BackIconAppBar(stringResource(id = R.string.hrs_title)) {
|
||||||
onEvent(DisconnectEvent)
|
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
|
package no.nordicsemi.android.hrs.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
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.data.HRSRepository
|
||||||
import no.nordicsemi.android.hrs.view.DisconnectEvent
|
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.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
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
internal class HRSViewModel @Inject constructor(
|
internal class HRSViewModel @Inject constructor(
|
||||||
private val dataHolder: HRSRepository
|
private val repository: HRSRepository
|
||||||
) : CloseableViewModel() {
|
) : 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) {
|
fun onEvent(event: HRSScreenViewEvent) {
|
||||||
(event as? DisconnectEvent)?.let {
|
(event as? DisconnectEvent)?.let {
|
||||||
@@ -21,7 +35,12 @@ internal class HRSViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onDisconnectButtonClick() {
|
private fun onDisconnectButtonClick() {
|
||||||
finish()
|
repository.sendDisconnectCommand()
|
||||||
dataHolder.clear()
|
repository.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
repository.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
package no.nordicsemi.android.hrs
|
package no.nordicsemi.android.hrs
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package no.nordicsemi.android.hts
|
package no.nordicsemi.android.hts
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
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.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
package no.nordicsemi.android.hts.data
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
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.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -11,6 +16,12 @@ internal class HTSRepository @Inject constructor() {
|
|||||||
private val _data = MutableStateFlow(HTSData())
|
private val _data = MutableStateFlow(HTSData())
|
||||||
val data: StateFlow<HTSData> = _data
|
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) {
|
fun setNewTemperature(temperature: Float) {
|
||||||
_data.tryEmit(_data.value.copy(temperatureValue = temperature))
|
_data.tryEmit(_data.value.copy(temperatureValue = temperature))
|
||||||
}
|
}
|
||||||
@@ -23,7 +34,16 @@ internal class HTSRepository @Inject constructor() {
|
|||||||
_data.tryEmit(_data.value.copy(temperatureUnit = unit))
|
_data.tryEmit(_data.value.copy(temperatureUnit = unit))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sendDisconnectCommand() {
|
||||||
|
_command.tryEmit(DisconnectCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setNewStatus(status: BleManagerStatus) {
|
||||||
|
_status.value = status
|
||||||
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
|
_status.value = BleManagerStatus.CONNECTING
|
||||||
_data.tryEmit(HTSData())
|
_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.callback.ht.TemperatureMeasurementDataCallback
|
||||||
import no.nordicsemi.android.ble.common.profile.ht.TemperatureType
|
import no.nordicsemi.android.ble.common.profile.ht.TemperatureType
|
||||||
import no.nordicsemi.android.ble.common.profile.ht.TemperatureUnit
|
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.hts.data.HTSRepository
|
||||||
import no.nordicsemi.android.log.LogContract
|
|
||||||
import no.nordicsemi.android.service.BatteryManager
|
import no.nordicsemi.android.service.BatteryManager
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -50,13 +48,6 @@ internal class HTSManager internal constructor(
|
|||||||
private var htCharacteristic: BluetoothGattCharacteristic? = null
|
private var htCharacteristic: BluetoothGattCharacteristic? = null
|
||||||
|
|
||||||
private val temperatureMeasurementDataCallback = object : TemperatureMeasurementDataCallback() {
|
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(
|
override fun onTemperatureMeasurementReceived(
|
||||||
device: BluetoothDevice,
|
device: BluetoothDevice,
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package no.nordicsemi.android.hts.repository
|
package no.nordicsemi.android.hts.repository
|
||||||
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
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.hts.data.HTSRepository
|
||||||
import no.nordicsemi.android.service.ForegroundBleService
|
import no.nordicsemi.android.service.ForegroundBleService
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -9,7 +11,19 @@ import javax.inject.Inject
|
|||||||
internal class HTSService : ForegroundBleService() {
|
internal class HTSService : ForegroundBleService() {
|
||||||
|
|
||||||
@Inject
|
@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
|
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.Button
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|||||||
@@ -9,46 +9,40 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import no.nordicsemi.android.hts.R
|
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.repository.HTSService
|
||||||
import no.nordicsemi.android.hts.viewmodel.HTSViewModel
|
import no.nordicsemi.android.hts.viewmodel.HTSViewModel
|
||||||
import no.nordicsemi.android.theme.view.BackIconAppBar
|
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
|
@Composable
|
||||||
fun HTSScreen(finishAction: () -> Unit) {
|
fun HTSScreen(finishAction: () -> Unit) {
|
||||||
val viewModel: HTSViewModel = hiltViewModel()
|
val viewModel: HTSViewModel = hiltViewModel()
|
||||||
val state = viewModel.state.collectAsState().value
|
val state = viewModel.state.collectAsState().value
|
||||||
val isActive = viewModel.isActive.collectAsState().value
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
LaunchedEffect(isActive) {
|
LaunchedEffect(state.isActive) {
|
||||||
if (!isActive) {
|
if (state.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)) {
|
|
||||||
val intent = Intent(context, HTSService::class.java)
|
val intent = Intent(context, HTSService::class.java)
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
|
} else {
|
||||||
|
finishAction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HTSView(state) { viewModel.onEvent(it) }
|
HTSView(state.viewState) { viewModel.onEvent(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun HTSView(state: HTSData, onEvent: (HTSScreenViewEvent) -> Unit) {
|
private fun HTSView(state: HTSViewState, onEvent: (HTSScreenViewEvent) -> Unit) {
|
||||||
Column {
|
Column {
|
||||||
BackIconAppBar(stringResource(id = R.string.hts_title)) {
|
BackIconAppBar(stringResource(id = R.string.hts_title)) {
|
||||||
onEvent(DisconnectEvent)
|
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
|
package no.nordicsemi.android.hts.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
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.data.HTSRepository
|
||||||
import no.nordicsemi.android.hts.view.DisconnectEvent
|
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.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.hts.view.OnTemperatureUnitSelected
|
||||||
import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
|
import no.nordicsemi.android.service.BleManagerStatus
|
||||||
import no.nordicsemi.android.utils.exhaustive
|
import no.nordicsemi.android.utils.exhaustive
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
internal class HTSViewModel @Inject constructor(
|
internal class HTSViewModel @Inject constructor(
|
||||||
private val dataHolder: HTSRepository
|
private val repository: HTSRepository
|
||||||
) : CloseableViewModel() {
|
) : 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) {
|
fun onEvent(event: HTSScreenViewEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
@@ -24,11 +38,16 @@ internal class HTSViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onDisconnectButtonClick() {
|
private fun onDisconnectButtonClick() {
|
||||||
finish()
|
repository.sendDisconnectCommand()
|
||||||
dataHolder.clear()
|
repository.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onTemperatureUnitSelected(event: OnTemperatureUnitSelected) {
|
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
|
package no.nordicsemi.android.hts
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package no.nordicsemi.android.prx
|
package no.nordicsemi.android.prx
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
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.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -5,3 +5,5 @@ internal sealed class PRXCommand
|
|||||||
internal object EnableAlarm : PRXCommand()
|
internal object EnableAlarm : PRXCommand()
|
||||||
|
|
||||||
internal object DisableAlarm : 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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import no.nordicsemi.android.service.BleManagerStatus
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -17,6 +19,9 @@ internal class PRXRepository @Inject constructor() {
|
|||||||
private val _command = MutableSharedFlow<PRXCommand>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
private val _command = MutableSharedFlow<PRXCommand>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||||
val command = _command.asSharedFlow()
|
val command = _command.asSharedFlow()
|
||||||
|
|
||||||
|
private val _status = MutableStateFlow(BleManagerStatus.CONNECTING)
|
||||||
|
val status = _status.asStateFlow()
|
||||||
|
|
||||||
fun setBatteryLevel(batteryLevel: Int) {
|
fun setBatteryLevel(batteryLevel: Int) {
|
||||||
_data.tryEmit(_data.value.copy(batteryLevel = batteryLevel))
|
_data.tryEmit(_data.value.copy(batteryLevel = batteryLevel))
|
||||||
}
|
}
|
||||||
@@ -34,7 +39,12 @@ internal class PRXRepository @Inject constructor() {
|
|||||||
_command.tryEmit(command)
|
_command.tryEmit(command)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clear(){
|
fun setNewStatus(status: BleManagerStatus) {
|
||||||
|
_status.value = status
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
_status.value = BleManagerStatus.CONNECTING
|
||||||
_data.tryEmit(PRXData())
|
_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.callback.FailCallback
|
||||||
import no.nordicsemi.android.ble.common.callback.alert.AlertLevelDataCallback
|
import no.nordicsemi.android.ble.common.callback.alert.AlertLevelDataCallback
|
||||||
import no.nordicsemi.android.ble.common.data.alert.AlertLevelData
|
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.ble.error.GattError
|
||||||
import no.nordicsemi.android.log.LogContract
|
|
||||||
import no.nordicsemi.android.prx.data.PRXRepository
|
import no.nordicsemi.android.prx.data.PRXRepository
|
||||||
import no.nordicsemi.android.service.BatteryManager
|
import no.nordicsemi.android.service.BatteryManager
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -161,12 +159,6 @@ internal class PRXManager(
|
|||||||
if (on) "Setting alarm to HIGH..." else "Disabling alarm..."
|
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? ->
|
.done { device: BluetoothDevice? ->
|
||||||
isAlertEnabled = on
|
isAlertEnabled = on
|
||||||
dataHolder.setRemoteAlarmLevel(on)
|
dataHolder.setRemoteAlarmLevel(on)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package no.nordicsemi.android.prx.service
|
package no.nordicsemi.android.prx.service
|
||||||
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import no.nordicsemi.android.prx.data.AlarmLevel
|
import no.nordicsemi.android.prx.data.AlarmLevel
|
||||||
import no.nordicsemi.android.prx.data.DisableAlarm
|
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.EnableAlarm
|
||||||
import no.nordicsemi.android.prx.data.PRXRepository
|
import no.nordicsemi.android.prx.data.PRXRepository
|
||||||
import no.nordicsemi.android.service.ForegroundBleService
|
import no.nordicsemi.android.service.ForegroundBleService
|
||||||
@@ -16,7 +16,7 @@ import javax.inject.Inject
|
|||||||
internal class PRXService : ForegroundBleService() {
|
internal class PRXService : ForegroundBleService() {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var dataHolder: PRXRepository
|
lateinit var repository: PRXRepository
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var alarmHandler: AlarmHandler
|
lateinit var alarmHandler: AlarmHandler
|
||||||
@@ -24,7 +24,7 @@ internal class PRXService : ForegroundBleService() {
|
|||||||
private var serverManager: ProximityServerManager = ProximityServerManager(this)
|
private var serverManager: ProximityServerManager = ProximityServerManager(this)
|
||||||
|
|
||||||
override val manager: PRXManager by lazy {
|
override val manager: PRXManager by lazy {
|
||||||
PRXManager(this, dataHolder).apply {
|
PRXManager(this, repository).apply {
|
||||||
useServer(serverManager)
|
useServer(serverManager)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,14 +34,19 @@ internal class PRXService : ForegroundBleService() {
|
|||||||
|
|
||||||
serverManager.open()
|
serverManager.open()
|
||||||
|
|
||||||
dataHolder.command.onEach {
|
status.onEach {
|
||||||
|
repository.setNewStatus(it)
|
||||||
|
}.launchIn(scope)
|
||||||
|
|
||||||
|
repository.command.onEach {
|
||||||
when (it) {
|
when (it) {
|
||||||
DisableAlarm -> manager.writeImmediateAlert(false)
|
DisableAlarm -> manager.writeImmediateAlert(false)
|
||||||
EnableAlarm -> manager.writeImmediateAlert(true)
|
EnableAlarm -> manager.writeImmediateAlert(true)
|
||||||
|
Disconnect -> stopSelf()
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}.launchIn(scope)
|
}.launchIn(scope)
|
||||||
|
|
||||||
dataHolder.data.onEach {
|
repository.data.onEach {
|
||||||
if (it.localAlarmLevel != AlarmLevel.NONE) {
|
if (it.localAlarmLevel != AlarmLevel.NONE) {
|
||||||
alarmHandler.playAlarm()
|
alarmHandler.playAlarm()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,55 +8,42 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import no.nordicsemi.android.prx.R
|
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.service.PRXService
|
||||||
import no.nordicsemi.android.prx.viewmodel.PRXViewModel
|
import no.nordicsemi.android.prx.viewmodel.PRXViewModel
|
||||||
import no.nordicsemi.android.theme.view.BackIconAppBar
|
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
|
@Composable
|
||||||
fun PRXScreen(finishAction: () -> Unit) {
|
fun PRXScreen(finishAction: () -> Unit) {
|
||||||
val viewModel: PRXViewModel = hiltViewModel()
|
val viewModel: PRXViewModel = hiltViewModel()
|
||||||
val state = viewModel.state.collectAsState().value
|
val state = viewModel.state.collectAsState().value
|
||||||
val isActive = viewModel.isActive.collectAsState().value
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
LaunchedEffect(isActive) {
|
LaunchedEffect(state.isActive) {
|
||||||
if (!isActive) {
|
if (state.isActive) {
|
||||||
finishAction()
|
|
||||||
}
|
|
||||||
if (context.isServiceRunning(PRXService::class.java.name)) {
|
|
||||||
val intent = Intent(context, PRXService::class.java)
|
|
||||||
context.stopService(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect("start-service") {
|
|
||||||
if (!context.isServiceRunning(PRXService::class.java.name)) {
|
|
||||||
val intent = Intent(context, PRXService::class.java)
|
val intent = Intent(context, PRXService::class.java)
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
|
} else {
|
||||||
|
finishAction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PRXView(state) { viewModel.onEvent(it) }
|
PRXView(state.viewState) { viewModel.onEvent(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PRXView(state: PRXData, onEvent: (PRXScreenViewEvent) -> Unit) {
|
private fun PRXView(state: PRXViewState, onEvent: (PRXScreenViewEvent) -> Unit) {
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
BackIconAppBar(stringResource(id = R.string.prx_title)) {
|
BackIconAppBar(stringResource(id = R.string.prx_title)) {
|
||||||
onEvent(DisconnectEvent)
|
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
|
package no.nordicsemi.android.prx.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
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.DisableAlarm
|
||||||
|
import no.nordicsemi.android.prx.data.Disconnect
|
||||||
import no.nordicsemi.android.prx.data.EnableAlarm
|
import no.nordicsemi.android.prx.data.EnableAlarm
|
||||||
import no.nordicsemi.android.prx.data.PRXRepository
|
import no.nordicsemi.android.prx.data.PRXRepository
|
||||||
import no.nordicsemi.android.prx.view.DisconnectEvent
|
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.PRXScreenViewEvent
|
||||||
|
import no.nordicsemi.android.prx.view.PRXState
|
||||||
import no.nordicsemi.android.prx.view.TurnOffAlert
|
import no.nordicsemi.android.prx.view.TurnOffAlert
|
||||||
import no.nordicsemi.android.prx.view.TurnOnAlert
|
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 no.nordicsemi.android.utils.exhaustive
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
internal class PRXViewModel @Inject constructor(
|
internal class PRXViewModel @Inject constructor(
|
||||||
private val dataHolder: PRXRepository
|
private val repository: PRXRepository
|
||||||
) : CloseableViewModel() {
|
) : 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) {
|
fun onEvent(event: PRXScreenViewEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
DisconnectEvent -> onDisconnectButtonClick()
|
DisconnectEvent -> onDisconnectButtonClick()
|
||||||
TurnOffAlert -> dataHolder.invokeCommand(DisableAlarm)
|
TurnOffAlert -> repository.invokeCommand(DisableAlarm)
|
||||||
TurnOnAlert -> dataHolder.invokeCommand(EnableAlarm)
|
TurnOnAlert -> repository.invokeCommand(EnableAlarm)
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onDisconnectButtonClick() {
|
private fun onDisconnectButtonClick() {
|
||||||
finish()
|
repository.invokeCommand(Disconnect)
|
||||||
dataHolder.clear()
|
repository.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
repository.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
package no.nordicsemi.android.prx
|
package no.nordicsemi.android.prx
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package no.nordicsemi.android.rscs
|
package no.nordicsemi.android.rscs
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
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.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
package no.nordicsemi.android.rscs.data
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
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.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -9,7 +14,13 @@ import javax.inject.Singleton
|
|||||||
internal class RSCSRepository @Inject constructor() {
|
internal class RSCSRepository @Inject constructor() {
|
||||||
|
|
||||||
private val _data = MutableStateFlow(RSCSData())
|
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(
|
fun setNewData(
|
||||||
running: Boolean,
|
running: Boolean,
|
||||||
@@ -27,11 +38,20 @@ internal class RSCSRepository @Inject constructor() {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setNewStatus(status: BleManagerStatus) {
|
||||||
|
_status.value = status
|
||||||
|
}
|
||||||
|
|
||||||
fun setBatteryLevel(batteryLevel: Int) {
|
fun setBatteryLevel(batteryLevel: Int) {
|
||||||
_data.tryEmit(_data.value.copy(batteryLevel = batteryLevel))
|
_data.tryEmit(_data.value.copy(batteryLevel = batteryLevel))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sendDisconnectCommand() {
|
||||||
|
_command.tryEmit(DisconnectCommand)
|
||||||
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
|
_status.value = BleManagerStatus.CONNECTING
|
||||||
_data.tryEmit(RSCSData())
|
_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
|
package no.nordicsemi.android.rscs.service
|
||||||
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
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.rscs.data.RSCSRepository
|
||||||
import no.nordicsemi.android.service.ForegroundBleService
|
import no.nordicsemi.android.service.ForegroundBleService
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -9,7 +11,19 @@ import javax.inject.Inject
|
|||||||
internal class RSCSService : ForegroundBleService() {
|
internal class RSCSService : ForegroundBleService() {
|
||||||
|
|
||||||
@Inject
|
@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.compose.ui.res.stringResource
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import no.nordicsemi.android.rscs.R
|
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.service.RSCSService
|
||||||
import no.nordicsemi.android.rscs.viewmodel.RSCSViewModel
|
import no.nordicsemi.android.rscs.viewmodel.RSCSViewModel
|
||||||
import no.nordicsemi.android.theme.view.BackIconAppBar
|
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
|
@Composable
|
||||||
fun RSCSScreen(finishAction: () -> Unit) {
|
fun RSCSScreen(finishAction: () -> Unit) {
|
||||||
val viewModel: RSCSViewModel = hiltViewModel()
|
val viewModel: RSCSViewModel = hiltViewModel()
|
||||||
val state = viewModel.state.collectAsState().value
|
val state = viewModel.state.collectAsState().value
|
||||||
val isScreenActive = viewModel.isActive.collectAsState().value
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
LaunchedEffect(isScreenActive) {
|
LaunchedEffect(state.isActive) {
|
||||||
if (!isScreenActive) {
|
if (state.isActive) {
|
||||||
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)) {
|
|
||||||
val intent = Intent(context, RSCSService::class.java)
|
val intent = Intent(context, RSCSService::class.java)
|
||||||
context.startService(intent)
|
context.startService(intent)
|
||||||
|
} else {
|
||||||
|
finishAction()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RSCSView(state) { viewModel.onEvent(it) }
|
RSCSView(state.viewState) { viewModel.onEvent(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RSCSView(state: RSCSData, onEvent: (RSCScreenViewEvent) -> Unit) {
|
private fun RSCSView(state: RSCSViewState, onEvent: (RSCScreenViewEvent) -> Unit) {
|
||||||
Column {
|
Column {
|
||||||
BackIconAppBar(stringResource(id = R.string.rscs_title)) {
|
BackIconAppBar(stringResource(id = R.string.rscs_title)) {
|
||||||
onEvent(DisconnectEvent)
|
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
|
package no.nordicsemi.android.rscs.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
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.data.RSCSRepository
|
||||||
import no.nordicsemi.android.rscs.view.DisconnectEvent
|
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.rscs.view.RSCScreenViewEvent
|
||||||
import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
|
import no.nordicsemi.android.service.BleManagerStatus
|
||||||
import no.nordicsemi.android.utils.exhaustive
|
import no.nordicsemi.android.utils.exhaustive
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
internal class RSCSViewModel @Inject constructor(
|
internal class RSCSViewModel @Inject constructor(
|
||||||
private val dataHolder: RSCSRepository
|
private val repository: RSCSRepository
|
||||||
) : CloseableViewModel() {
|
) : 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) {
|
fun onEvent(event: RSCScreenViewEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
@@ -22,7 +36,12 @@ internal class RSCSViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onDisconnectButtonClick() {
|
private fun onDisconnectButtonClick() {
|
||||||
finish()
|
repository.sendDisconnectCommand()
|
||||||
dataHolder.clear()
|
repository.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
repository.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
package no.nordicsemi.android.rscs
|
package no.nordicsemi.android.rscs
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
* 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