mirror of
https://github.com/aljazceru/Android-nRF-Toolbox.git
synced 2025-12-23 09:24:23 +01:00
Apply fixes to Toolbox
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
package no.nordicsemi.android.service
|
||||
|
||||
enum class BleManagerStatus {
|
||||
CONNECTING, OK, DISCONNECTED
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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) { }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package no.nordicsemi.android.cgms.data
|
||||
|
||||
internal enum class CGMServiceCommand {
|
||||
REQUEST_ALL_RECORDS,
|
||||
REQUEST_LAST_RECORD,
|
||||
REQUEST_FIRST_RECORD,
|
||||
DISCONNECT
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package no.nordicsemi.android.cgms.data
|
||||
|
||||
internal enum class WorkingMode {
|
||||
ALL,
|
||||
LAST,
|
||||
FIRST
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,24 +28,24 @@ internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
|
||||
SelectWheelSizeDialog { onEvent(it) }
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
SettingsSection(state, onEvent)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
SensorsReadingView(state = state)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Button(
|
||||
onClick = { onEvent(OnDisconnectButtonClick) }
|
||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.disconnect))
|
||||
SettingsSection(state, onEvent)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
SensorsReadingView(state = state)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Button(
|
||||
onClick = { onEvent(OnDisconnectButtonClick) }
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.disconnect))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user