Apply fixes to Toolbox

This commit is contained in:
Sylwester Zieliński
2022-01-17 12:44:59 +01:00
parent 8bc77aedcc
commit 4aa0a69256
28 changed files with 347 additions and 209 deletions

View File

@@ -0,0 +1,5 @@
package no.nordicsemi.android.service
enum class BleManagerStatus {
CONNECTING, OK, DISCONNECTED
}

View File

@@ -21,22 +21,34 @@
*/
package no.nordicsemi.android.service
import android.app.Service
import android.bluetooth.BluetoothDevice
import android.content.Intent
import android.os.Handler
import android.os.IBinder
import android.util.Log
import android.widget.Toast
import androidx.lifecycle.LifecycleService
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import no.nordicsemi.android.ble.BleManager
import no.nordicsemi.android.log.ILogSession
import no.nordicsemi.android.log.Logger
import javax.inject.Inject
@AndroidEntryPoint
abstract class BleProfileService : LifecycleService() {
abstract class BleProfileService : Service() {
protected val scope = CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
protected abstract val manager: BleManager
private val _status = MutableStateFlow(BleManagerStatus.CONNECTING)
val status = _status.asStateFlow()
@Inject
lateinit var bluetoothDeviceHolder: SelectedBluetoothDeviceHolder
@@ -71,6 +83,23 @@ abstract class BleProfileService : LifecycleService() {
override fun onCreate() {
super.onCreate()
handler = Handler()
manager.setConnectionObserver(object : ConnectionObserverAdapter() {
override fun onDeviceConnected(device: BluetoothDevice) {
super.onDeviceConnected(device)
_status.value = BleManagerStatus.OK
}
override fun onDeviceDisconnected(device: BluetoothDevice, reason: Int) {
super.onDeviceDisconnected(device, reason)
_status.value = BleManagerStatus.DISCONNECTED
scope.close()
}
})
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
/**
@@ -89,6 +118,7 @@ abstract class BleProfileService : LifecycleService() {
.useAutoConnect(shouldAutoConnect())
.retry(3, 100)
.enqueue()
return START_REDELIVER_INTENT
}

View File

@@ -0,0 +1,14 @@
package no.nordicsemi.android.service
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import java.io.Closeable
import kotlin.coroutines.CoroutineContext
class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun close() {
coroutineContext.cancel()
}
}

View File

@@ -0,0 +1,19 @@
package no.nordicsemi.android.service
import android.bluetooth.BluetoothDevice
import no.nordicsemi.android.ble.observer.ConnectionObserver
abstract class ConnectionObserverAdapter : ConnectionObserver {
override fun onDeviceConnecting(device: BluetoothDevice) { }
override fun onDeviceConnected(device: BluetoothDevice) { }
override fun onDeviceFailedToConnect(device: BluetoothDevice, reason: Int) { }
override fun onDeviceReady(device: BluetoothDevice) { }
override fun onDeviceDisconnecting(device: BluetoothDevice) { }
override fun onDeviceDisconnected(device: BluetoothDevice, reason: Int) { }
}

View File

@@ -6,6 +6,7 @@ dependencies {
implementation libs.nordic.theme
implementation libs.bundles.compose
implementation libs.bundles.icons
implementation libs.compose.lifecycle
implementation libs.compose.activity
}

View File

@@ -0,0 +1,73 @@
package no.nordicsemi.android.theme.view
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.HourglassTop
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.theme.R
@Composable
fun DeviceConnectingView() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
ScreenSection {
Icon(
imageVector = Icons.Default.HourglassTop,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSecondary,
modifier = Modifier
.background(
color = MaterialTheme.colorScheme.secondary,
shape = CircleShape
)
.padding(8.dp)
)
Spacer(modifier = Modifier.size(16.dp))
Text(
text = stringResource(id = R.string.device_connecting),
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.size(16.dp))
Text(
text = stringResource(id = R.string.device_explanation),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.size(16.dp))
Text(
text = stringResource(id = R.string.device_please_wait),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleLarge
)
}
}
}
@Preview
@Composable
fun DeviceConnectingView_Preview() {
DeviceConnectingView()
}

View File

@@ -10,4 +10,8 @@
<string name="disconnect">DISCONNECT</string>
<string name="field_battery">Battery</string>
<string name="device_connecting">Connecting</string>
<string name="device_explanation">The mobile is trying to connect to peripheral device.</string>
<string name="device_please_wait">Please wait.</string>
</resources>

View File

@@ -2,6 +2,7 @@ package no.nordicsemi.android.cgms.data
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.*
import no.nordicsemi.android.service.BleManagerStatus
import javax.inject.Inject
import javax.inject.Singleton
@@ -11,9 +12,12 @@ internal class CGMRepository @Inject constructor() {
private val _data = MutableStateFlow(CGMData())
val data: StateFlow<CGMData> = _data.asStateFlow()
private val _command = MutableSharedFlow<WorkingMode>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST)
private val _command = MutableSharedFlow<CGMServiceCommand>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST)
val command = _command.asSharedFlow()
private val _status = MutableStateFlow(BleManagerStatus.CONNECTING)
val status = _status.asStateFlow()
fun emitNewBatteryLevel(batterLevel: Int) {
_data.tryEmit(_data.value.copy(batteryLevel = batterLevel))
}
@@ -26,10 +30,14 @@ internal class CGMRepository @Inject constructor() {
_data.tryEmit(_data.value.copy(requestStatus = requestStatus))
}
fun requestNewWorkingMode(workingMode: WorkingMode) {
fun sendNewServiceCommand(workingMode: CGMServiceCommand) {
_command.tryEmit(workingMode)
}
fun setNewStatus(status: BleManagerStatus) {
_status.value = status
}
fun clear() {
_data.tryEmit(CGMData())
}

View File

@@ -0,0 +1,8 @@
package no.nordicsemi.android.cgms.data
internal enum class CGMServiceCommand {
REQUEST_ALL_RECORDS,
REQUEST_LAST_RECORD,
REQUEST_FIRST_RECORD,
DISCONNECT
}

View File

@@ -1,7 +0,0 @@
package no.nordicsemi.android.cgms.data
internal enum class WorkingMode {
ALL,
LAST,
FIRST
}

View File

@@ -1,11 +1,10 @@
package no.nordicsemi.android.cgms.repository
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.cgms.data.CGMRepository
import no.nordicsemi.android.cgms.data.WorkingMode
import no.nordicsemi.android.cgms.data.CGMServiceCommand
import no.nordicsemi.android.service.ForegroundBleService
import no.nordicsemi.android.utils.exhaustive
import javax.inject.Inject
@@ -14,19 +13,24 @@ import javax.inject.Inject
internal class CGMService : ForegroundBleService() {
@Inject
lateinit var dataHolder: CGMRepository
lateinit var repository: CGMRepository
override val manager: CGMManager by lazy { CGMManager(this, dataHolder) }
override val manager: CGMManager by lazy { CGMManager(this, repository) }
override fun onCreate() {
super.onCreate()
dataHolder.command.onEach {
status.onEach {
repository.setNewStatus(it)
}.launchIn(scope)
repository.command.onEach {
when (it) {
WorkingMode.ALL -> manager.requestAllRecords()
WorkingMode.LAST -> manager.requestLastRecord()
WorkingMode.FIRST -> manager.requestFirstRecord()
CGMServiceCommand.REQUEST_ALL_RECORDS -> manager.requestAllRecords()
CGMServiceCommand.REQUEST_LAST_RECORD -> manager.requestLastRecord()
CGMServiceCommand.REQUEST_FIRST_RECORD -> manager.requestFirstRecord()
CGMServiceCommand.DISCONNECT -> stopSelf()
}.exhaustive
}.launchIn(lifecycleScope)
}.launchIn(scope)
}
}

View File

@@ -18,7 +18,7 @@ import no.nordicsemi.android.cgms.R
import no.nordicsemi.android.cgms.data.CGMData
import no.nordicsemi.android.cgms.data.CGMRecord
import no.nordicsemi.android.cgms.data.RequestStatus
import no.nordicsemi.android.cgms.data.WorkingMode
import no.nordicsemi.android.cgms.data.CGMServiceCommand
import no.nordicsemi.android.material.you.CircularProgressIndicator
import no.nordicsemi.android.theme.view.BatteryLevelView
import no.nordicsemi.android.theme.view.ScreenSection
@@ -29,12 +29,10 @@ internal fun CGMContentView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp)
.padding(16.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(16.dp))
SettingsView(state, onEvent)
Spacer(modifier = Modifier.height(16.dp))
@@ -71,10 +69,14 @@ private fun SettingsView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) {
if (state.requestStatus == RequestStatus.PENDING) {
CircularProgressIndicator()
} else {
WorkingMode.values().forEach {
Button(onClick = { onEvent(OnWorkingModeSelected(it)) }) {
Text(it.toDisplayString())
Button(onClick = { onEvent(OnWorkingModeSelected(CGMServiceCommand.REQUEST_ALL_RECORDS)) }) {
Text(stringResource(id = R.string.cgms__working_mode__all))
}
Button(onClick = { onEvent(OnWorkingModeSelected(CGMServiceCommand.REQUEST_LAST_RECORD)) }) {
Text(stringResource(id = R.string.cgms__working_mode__last))
}
Button(onClick = { onEvent(OnWorkingModeSelected(CGMServiceCommand.REQUEST_FIRST_RECORD)) }) {
Text(stringResource(id = R.string.cgms__working_mode__first))
}
}
}

View File

@@ -4,19 +4,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import no.nordicsemi.android.cgms.R
import no.nordicsemi.android.cgms.data.CGMRecord
import no.nordicsemi.android.cgms.data.WorkingMode
import no.nordicsemi.android.cgms.data.CGMServiceCommand
import java.text.SimpleDateFormat
import java.util.*
@Composable
internal fun WorkingMode.toDisplayString(): String {
return when (this) {
WorkingMode.ALL -> stringResource(id = R.string.cgms__working_mode__all)
WorkingMode.LAST -> stringResource(id = R.string.cgms__working_mode__last)
WorkingMode.FIRST -> stringResource(id = R.string.cgms__working_mode__first)
}
}
internal fun CGMRecord.formattedTime(): String {
val timeFormat = SimpleDateFormat("dd.MM.yyyy HH:mm", Locale.US)
return timeFormat.format(Date(timestamp))

View File

@@ -9,49 +9,43 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.cgms.R
import no.nordicsemi.android.cgms.data.CGMData
import no.nordicsemi.android.cgms.repository.CGMService
import no.nordicsemi.android.cgms.viewmodel.CGMScreenViewModel
import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.utils.isServiceRunning
import no.nordicsemi.android.theme.view.DeviceConnectingView
import no.nordicsemi.android.utils.exhaustive
@Composable
fun CGMScreen(finishAction: () -> Unit) {
val viewModel: CGMScreenViewModel = hiltViewModel()
val state = viewModel.state.collectAsState().value
val isScreenActive = viewModel.isActive.collectAsState().value
val context = LocalContext.current
LaunchedEffect(isScreenActive) {
if (!isScreenActive) {
finishAction()
}
if (context.isServiceRunning(CGMService::class.java.name)) {
val intent = Intent(context, CGMService::class.java)
context.stopService(intent)
}
}
LaunchedEffect("start-service") {
if (!context.isServiceRunning(CGMService::class.java.name)) {
LaunchedEffect(state.isActive) {
if (state.isActive) {
val intent = Intent(context, CGMService::class.java)
context.startService(intent)
} else if (!state.isActive) {
finishAction()
}
}
CGMView(state) {
CGMView(state.viewState) {
viewModel.onEvent(it)
}
}
@Composable
private fun CGMView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) {
private fun CGMView(state: CGMViewState, onEvent: (CGMViewEvent) -> Unit) {
Column {
BackIconAppBar(stringResource(id = R.string.cgms_title)) {
onEvent(DisconnectEvent)
}
CGMContentView(state, onEvent)
when (state) {
is DisplayDataState -> CGMContentView(state.data, onEvent)
LoadingState -> DeviceConnectingView()
}.exhaustive
}
}

View File

@@ -1,9 +1,9 @@
package no.nordicsemi.android.cgms.view
import no.nordicsemi.android.cgms.data.WorkingMode
import no.nordicsemi.android.cgms.data.CGMServiceCommand
internal sealed class CGMViewEvent
internal data class OnWorkingModeSelected(val workingMode: WorkingMode) : CGMViewEvent()
internal data class OnWorkingModeSelected(val workingMode: CGMServiceCommand) : CGMViewEvent()
internal object DisconnectEvent : CGMViewEvent()

View File

@@ -0,0 +1,14 @@
package no.nordicsemi.android.cgms.view
import no.nordicsemi.android.cgms.data.CGMData
internal data class CGMState(
val viewState: CGMViewState,
val isActive: Boolean = true
)
internal sealed class CGMViewState
internal object LoadingState : CGMViewState()
internal data class DisplayDataState(val data: CGMData) : CGMViewState()

View File

@@ -1,30 +1,45 @@
package no.nordicsemi.android.cgms.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import no.nordicsemi.android.cgms.data.CGMRepository
import no.nordicsemi.android.cgms.data.CGMServiceCommand
import no.nordicsemi.android.cgms.view.CGMState
import no.nordicsemi.android.cgms.view.CGMViewEvent
import no.nordicsemi.android.cgms.view.DisconnectEvent
import no.nordicsemi.android.cgms.view.DisplayDataState
import no.nordicsemi.android.cgms.view.LoadingState
import no.nordicsemi.android.cgms.view.OnWorkingModeSelected
import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
import no.nordicsemi.android.service.BleManagerStatus
import no.nordicsemi.android.utils.exhaustive
import javax.inject.Inject
@HiltViewModel
internal class CGMScreenViewModel @Inject constructor(
private val dataHolder: CGMRepository
) : CloseableViewModel() {
private val repository: CGMRepository
) : ViewModel() {
val state = dataHolder.data
val state = repository.data.combine(repository.status) { data, status ->
when (status) {
BleManagerStatus.CONNECTING -> CGMState(LoadingState)
BleManagerStatus.OK -> CGMState(DisplayDataState(data))
BleManagerStatus.DISCONNECTED -> CGMState(DisplayDataState(data), false)
}
}.stateIn(viewModelScope, SharingStarted.Lazily, CGMState(LoadingState))
fun onEvent(event: CGMViewEvent) {
when (event) {
DisconnectEvent -> disconnect()
is OnWorkingModeSelected -> dataHolder.requestNewWorkingMode(event.workingMode)
is OnWorkingModeSelected -> repository.sendNewServiceCommand(event.workingMode)
}.exhaustive
}
private fun disconnect() {
finish()
dataHolder.clear()
repository.clear()
repository.sendNewServiceCommand(CGMServiceCommand.DISCONNECT)
}
}

View File

@@ -4,6 +4,8 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Button
@@ -26,12 +28,11 @@ internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
SelectWheelSizeDialog { onEvent(it) }
}
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(horizontal = 16.dp)
modifier = Modifier.padding(16.dp)
) {
Spacer(modifier = Modifier.height(16.dp))
SettingsSection(state, onEvent)
Spacer(modifier = Modifier.height(16.dp))
@@ -47,6 +48,7 @@ internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
}
}
}
}
@Composable
private fun SettingsSection(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {

View File

@@ -39,7 +39,7 @@ internal class PRXService : ForegroundBleService() {
DisableAlarm -> manager.writeImmediateAlert(false)
EnableAlarm -> manager.writeImmediateAlert(true)
}.exhaustive
}.launchIn(lifecycleScope)
}.launchIn(scope)
dataHolder.data.onEach {
if (it.localAlarmLevel != AlarmLevel.NONE) {
@@ -47,7 +47,7 @@ internal class PRXService : ForegroundBleService() {
} else {
alarmHandler.pauseAlarm()
}
}.launchIn(lifecycleScope)
}.launchIn(scope)
}
override fun onDestroy() {

View File

@@ -1,83 +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.rscs.service
import no.nordicsemi.android.ble.data.Data
import java.util.*
internal object RSCMeasurementParser {
private const val INSTANTANEOUS_STRIDE_LENGTH_PRESENT: Byte = 0x01 // 1 bit
private const val TOTAL_DISTANCE_PRESENT: Byte = 0x02 // 1 bit
private const val WALKING_OR_RUNNING_STATUS_BITS: Byte = 0x04 // 1 bit
fun parse(data: Data): String {
var offset = 0
val flags = data.value!![offset].toInt() // 1 byte
offset += 1
val islmPresent = flags and INSTANTANEOUS_STRIDE_LENGTH_PRESENT.toInt() > 0
val tdPreset = flags and TOTAL_DISTANCE_PRESENT.toInt() > 0
val running = flags and WALKING_OR_RUNNING_STATUS_BITS.toInt() > 0
val walking = !running
val instantaneousSpeed =
data.getIntValue(Data.FORMAT_UINT16, offset) as Float / 256.0f // 1/256 m/s
offset += 2
val instantaneousCadence = data.getIntValue(Data.FORMAT_UINT8, offset)!!
offset += 1
var instantaneousStrideLength = 0f
if (islmPresent) {
instantaneousStrideLength =
data.getIntValue(Data.FORMAT_UINT16, offset) as Float / 100.0f // 1/100 m
offset += 2
}
var totalDistance = 0f
if (tdPreset) {
totalDistance = data.getIntValue(Data.FORMAT_UINT32, offset) as Float / 10.0f
// offset += 4;
}
val builder = StringBuilder()
builder.append(
String.format(
Locale.US,
"Speed: %.2f m/s, Cadence: %d RPM,\n",
instantaneousSpeed,
instantaneousCadence
)
)
if (islmPresent) builder.append(
String.format(
Locale.US,
"Instantaneous Stride Length: %.2f m,\n",
instantaneousStrideLength
)
)
if (tdPreset) builder.append(
String.format(
Locale.US,
"Total Distance: %.1f m,\n",
totalDistance
)
)
if (walking) builder.append("Status: WALKING") else builder.append("Status: RUNNING")
return builder.toString()
}
}

View File

@@ -26,8 +26,6 @@ import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.content.Context
import no.nordicsemi.android.ble.common.callback.rsc.RunningSpeedAndCadenceMeasurementDataCallback
import no.nordicsemi.android.ble.data.Data
import no.nordicsemi.android.log.LogContract
import no.nordicsemi.android.rscs.data.RSCSRepository
import no.nordicsemi.android.service.BatteryManager
import java.util.*
@@ -46,13 +44,6 @@ internal class RSCSManager internal constructor(
private var rscMeasurementCharacteristic: BluetoothGattCharacteristic? = null
private val callback = object : RunningSpeedAndCadenceMeasurementDataCallback() {
override fun onDataReceived(device: BluetoothDevice, data: Data) {
log(
LogContract.Log.Level.APPLICATION,
"\"" + RSCMeasurementParser.parse(data).toString() + "\" received"
)
super.onDataReceived(device, data)
}
override fun onRSCMeasurementReceived(
device: BluetoothDevice,

View File

@@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import no.nordicsemi.android.service.BleManagerStatus
import javax.inject.Inject
import javax.inject.Singleton
@@ -17,6 +18,9 @@ internal class UARTRepository @Inject constructor() {
private val _command = MutableSharedFlow<UARTServiceCommand>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST)
val command = _command.asSharedFlow()
private val _status = MutableStateFlow(BleManagerStatus.CONNECTING)
val status = _status.asStateFlow()
fun addNewMacro(macro: UARTMacro) {
_data.tryEmit(_data.value.copy(macros = _data.value.macros + macro))
}
@@ -39,4 +43,8 @@ internal class UARTRepository @Inject constructor() {
fun sendNewCommand(command: UARTServiceCommand) {
_command.tryEmit(command)
}
fun setNewStatus(status: BleManagerStatus) {
_status.value = status
}
}

View File

@@ -1,3 +1,7 @@
package no.nordicsemi.android.uart.data
data class UARTServiceCommand(val command: String)
internal sealed class UARTServiceCommand
internal data class SendTextCommand(val command: String) : UARTServiceCommand()
internal object DisconnectCommand : UARTServiceCommand()

View File

@@ -1,26 +1,35 @@
package no.nordicsemi.android.uart.repository
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.service.ForegroundBleService
import no.nordicsemi.android.uart.data.DisconnectCommand
import no.nordicsemi.android.uart.data.SendTextCommand
import no.nordicsemi.android.uart.data.UARTRepository
import no.nordicsemi.android.utils.exhaustive
import javax.inject.Inject
@AndroidEntryPoint
internal class UARTService : ForegroundBleService() {
@Inject
lateinit var dataHolder: UARTRepository
lateinit var repository: UARTRepository
override val manager: UARTManager by lazy { UARTManager(this, dataHolder) }
override val manager: UARTManager by lazy { UARTManager(this, repository) }
override fun onCreate() {
super.onCreate()
dataHolder.command.onEach {
manager.send(it.command)
}.launchIn(lifecycleScope)
status.onEach {
repository.setNewStatus(it)
}.launchIn(scope)
repository.command.onEach {
when (it) {
DisconnectCommand -> stopSelf()
is SendTextCommand -> manager.send(it.command)
}.exhaustive
}.launchIn(scope)
}
}

View File

@@ -9,46 +9,40 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.DeviceConnectingView
import no.nordicsemi.android.uart.R
import no.nordicsemi.android.uart.data.UARTData
import no.nordicsemi.android.uart.repository.UARTService
import no.nordicsemi.android.uart.viewmodel.UARTViewModel
import no.nordicsemi.android.utils.isServiceRunning
import no.nordicsemi.android.utils.exhaustive
@Composable
fun UARTScreen(finishAction: () -> Unit) {
val viewModel: UARTViewModel = hiltViewModel()
val state = viewModel.state.collectAsState().value
val isScreenActive = viewModel.isActive.collectAsState().value
val context = LocalContext.current
LaunchedEffect(isScreenActive) {
if (!isScreenActive) {
finishAction()
}
if (context.isServiceRunning(UARTService::class.java.name)) {
val intent = Intent(context, UARTService::class.java)
context.stopService(intent)
}
}
LaunchedEffect("start-service") {
if (!context.isServiceRunning(UARTService::class.java.name)) {
LaunchedEffect(state.isActive) {
if (state.isActive) {
val intent = Intent(context, UARTService::class.java)
context.startService(intent)
} else if (!state.isActive) {
finishAction()
}
}
UARTView(state) { viewModel.onEvent(it) }
UARTView(state.viewState) { viewModel.onEvent(it) }
}
@Composable
private fun UARTView(state: UARTData, onEvent: (UARTViewEvent) -> Unit) {
private fun UARTView(state: UARTViewState, onEvent: (UARTViewEvent) -> Unit) {
Column {
BackIconAppBar(stringResource(id = R.string.uart_title)) {
onEvent(OnDisconnectButtonClick)
}
UARTContentView(state) { onEvent(it) }
when (state) {
is DisplayDataState -> UARTContentView(state.data) { onEvent(it) }
LoadingState -> DeviceConnectingView()
}.exhaustive
}
}

View File

@@ -0,0 +1,14 @@
package no.nordicsemi.android.uart.view
import no.nordicsemi.android.uart.data.UARTData
internal data class UARTState(
val viewState: UARTViewState,
val isActive: Boolean = true
)
internal sealed class UARTViewState
internal object LoadingState : UARTViewState()
internal data class DisplayDataState(val data: UARTData) : UARTViewState()

View File

@@ -1,26 +1,46 @@
package no.nordicsemi.android.uart.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import no.nordicsemi.android.service.BleManagerStatus
import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
import no.nordicsemi.android.uart.data.DisconnectCommand
import no.nordicsemi.android.uart.data.SendTextCommand
import no.nordicsemi.android.uart.data.UARTRepository
import no.nordicsemi.android.uart.data.UARTServiceCommand
import no.nordicsemi.android.uart.view.*
import no.nordicsemi.android.uart.view.DisplayDataState
import no.nordicsemi.android.uart.view.LoadingState
import no.nordicsemi.android.uart.view.OnCreateMacro
import no.nordicsemi.android.uart.view.OnDeleteMacro
import no.nordicsemi.android.uart.view.OnDisconnectButtonClick
import no.nordicsemi.android.uart.view.OnRunMacro
import no.nordicsemi.android.uart.view.UARTState
import no.nordicsemi.android.uart.view.UARTViewEvent
import no.nordicsemi.android.utils.exhaustive
import javax.inject.Inject
@HiltViewModel
internal class UARTViewModel @Inject constructor(
private val dataHolder: UARTRepository
) : CloseableViewModel() {
private val repository: UARTRepository
) : ViewModel() {
val state = dataHolder.data
val state = repository.data.combine(repository.status) { data, status ->
when (status) {
BleManagerStatus.CONNECTING -> UARTState(LoadingState)
BleManagerStatus.OK -> UARTState(DisplayDataState(data))
BleManagerStatus.DISCONNECTED -> UARTState(DisplayDataState(data), false)
}
}.stateIn(viewModelScope, SharingStarted.Lazily, UARTState(LoadingState))
fun onEvent(event: UARTViewEvent) {
when (event) {
is OnCreateMacro -> dataHolder.addNewMacro(event.macro)
is OnDeleteMacro -> dataHolder.deleteMacro(event.macro)
OnDisconnectButtonClick -> finish()
is OnRunMacro -> dataHolder.sendNewCommand(UARTServiceCommand(event.macro.command))
is OnCreateMacro -> repository.addNewMacro(event.macro)
is OnDeleteMacro -> repository.deleteMacro(event.macro)
OnDisconnectButtonClick -> repository.sendNewCommand(DisconnectCommand)
is OnRunMacro -> repository.sendNewCommand(SendTextCommand(event.macro.command))
}.exhaustive
}
}

View File

@@ -37,6 +37,10 @@ dependencyResolutionManagement {
alias('compose-navigation').to('androidx.navigation:navigation-compose:2.4.0-alpha09')
bundle('compose', ['compose-livedata', 'compose-ui', 'compose-material', 'compose-tooling-preview', 'compose-navigation'])
alias('material-icons').to('androidx.compose.material', 'material-icons-core').versionRef('compose')
alias('material-icons-extended').to('androidx.compose.material', 'material-icons-extended').versionRef('compose')
bundle('icons', ['material-icons', 'material-icons-extended'])
version('hilt', '2.38.1')
alias('hilt-android').to('com.google.dagger', 'hilt-android').versionRef('hilt')
alias('hilt-compiler').to('com.google.dagger', 'hilt-compiler').versionRef('hilt')