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 package no.nordicsemi.android.service
import android.app.Service
import android.bluetooth.BluetoothDevice 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.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.lifecycle.LifecycleService import androidx.lifecycle.LifecycleService
import dagger.hilt.android.AndroidEntryPoint 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.ble.BleManager
import no.nordicsemi.android.log.ILogSession import no.nordicsemi.android.log.ILogSession
import no.nordicsemi.android.log.Logger import no.nordicsemi.android.log.Logger
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
abstract class BleProfileService : LifecycleService() { abstract class BleProfileService : Service() {
protected val scope = CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
protected abstract val manager: BleManager protected abstract val manager: BleManager
private val _status = MutableStateFlow(BleManagerStatus.CONNECTING)
val status = _status.asStateFlow()
@Inject @Inject
lateinit var bluetoothDeviceHolder: SelectedBluetoothDeviceHolder lateinit var bluetoothDeviceHolder: SelectedBluetoothDeviceHolder
@@ -71,6 +83,23 @@ abstract class BleProfileService : LifecycleService() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
handler = Handler() 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()) .useAutoConnect(shouldAutoConnect())
.retry(3, 100) .retry(3, 100)
.enqueue() .enqueue()
return START_REDELIVER_INTENT 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.nordic.theme
implementation libs.bundles.compose implementation libs.bundles.compose
implementation libs.bundles.icons
implementation libs.compose.lifecycle implementation libs.compose.lifecycle
implementation libs.compose.activity 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="disconnect">DISCONNECT</string>
<string name="field_battery">Battery</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> </resources>

View File

@@ -2,6 +2,7 @@ package no.nordicsemi.android.cgms.data
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import no.nordicsemi.android.service.BleManagerStatus
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -11,9 +12,12 @@ internal class CGMRepository @Inject constructor() {
private val _data = MutableStateFlow(CGMData()) private val _data = MutableStateFlow(CGMData())
val data: StateFlow<CGMData> = _data.asStateFlow() 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() val command = _command.asSharedFlow()
private val _status = MutableStateFlow(BleManagerStatus.CONNECTING)
val status = _status.asStateFlow()
fun emitNewBatteryLevel(batterLevel: Int) { fun emitNewBatteryLevel(batterLevel: Int) {
_data.tryEmit(_data.value.copy(batteryLevel = batterLevel)) _data.tryEmit(_data.value.copy(batteryLevel = batterLevel))
} }
@@ -26,10 +30,14 @@ internal class CGMRepository @Inject constructor() {
_data.tryEmit(_data.value.copy(requestStatus = requestStatus)) _data.tryEmit(_data.value.copy(requestStatus = requestStatus))
} }
fun requestNewWorkingMode(workingMode: WorkingMode) { fun sendNewServiceCommand(workingMode: CGMServiceCommand) {
_command.tryEmit(workingMode) _command.tryEmit(workingMode)
} }
fun setNewStatus(status: BleManagerStatus) {
_status.value = status
}
fun clear() { fun clear() {
_data.tryEmit(CGMData()) _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 package no.nordicsemi.android.cgms.repository
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.cgms.data.CGMRepository 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.service.ForegroundBleService
import no.nordicsemi.android.utils.exhaustive import no.nordicsemi.android.utils.exhaustive
import javax.inject.Inject import javax.inject.Inject
@@ -14,19 +13,24 @@ import javax.inject.Inject
internal class CGMService : ForegroundBleService() { internal class CGMService : ForegroundBleService() {
@Inject @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() { override fun onCreate() {
super.onCreate() super.onCreate()
dataHolder.command.onEach { status.onEach {
repository.setNewStatus(it)
}.launchIn(scope)
repository.command.onEach {
when (it) { when (it) {
WorkingMode.ALL -> manager.requestAllRecords() CGMServiceCommand.REQUEST_ALL_RECORDS -> manager.requestAllRecords()
WorkingMode.LAST -> manager.requestLastRecord() CGMServiceCommand.REQUEST_LAST_RECORD -> manager.requestLastRecord()
WorkingMode.FIRST -> manager.requestFirstRecord() CGMServiceCommand.REQUEST_FIRST_RECORD -> manager.requestFirstRecord()
CGMServiceCommand.DISCONNECT -> stopSelf()
}.exhaustive }.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.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.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.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
@@ -29,12 +29,10 @@ internal fun CGMContentView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(horizontal = 16.dp) .padding(16.dp)
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Spacer(modifier = Modifier.height(16.dp))
SettingsView(state, onEvent) SettingsView(state, onEvent)
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@@ -71,10 +69,14 @@ private fun SettingsView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) {
if (state.requestStatus == RequestStatus.PENDING) { if (state.requestStatus == RequestStatus.PENDING) {
CircularProgressIndicator() CircularProgressIndicator()
} else { } else {
WorkingMode.values().forEach { Button(onClick = { onEvent(OnWorkingModeSelected(CGMServiceCommand.REQUEST_ALL_RECORDS)) }) {
Button(onClick = { onEvent(OnWorkingModeSelected(it)) }) { Text(stringResource(id = R.string.cgms__working_mode__all))
Text(it.toDisplayString())
} }
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 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.WorkingMode import no.nordicsemi.android.cgms.data.CGMServiceCommand
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* 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 { internal fun CGMRecord.formattedTime(): String {
val timeFormat = SimpleDateFormat("dd.MM.yyyy HH:mm", Locale.US) val timeFormat = SimpleDateFormat("dd.MM.yyyy HH:mm", Locale.US)
return timeFormat.format(Date(timestamp)) 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.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.cgms.R 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.repository.CGMService
import no.nordicsemi.android.cgms.viewmodel.CGMScreenViewModel import no.nordicsemi.android.cgms.viewmodel.CGMScreenViewModel
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 CGMScreen(finishAction: () -> Unit) { fun CGMScreen(finishAction: () -> Unit) {
val viewModel: CGMScreenViewModel = hiltViewModel() val viewModel: CGMScreenViewModel = 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(CGMService::class.java.name)) {
val intent = Intent(context, CGMService::class.java)
context.stopService(intent)
}
}
LaunchedEffect("start-service") {
if (!context.isServiceRunning(CGMService::class.java.name)) {
val intent = Intent(context, CGMService::class.java) val intent = Intent(context, CGMService::class.java)
context.startService(intent) context.startService(intent)
} else if (!state.isActive) {
finishAction()
} }
} }
CGMView(state) { CGMView(state.viewState) {
viewModel.onEvent(it) viewModel.onEvent(it)
} }
} }
@Composable @Composable
private fun CGMView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) { private fun CGMView(state: CGMViewState, onEvent: (CGMViewEvent) -> Unit) {
Column { Column {
BackIconAppBar(stringResource(id = R.string.cgms_title)) { BackIconAppBar(stringResource(id = R.string.cgms_title)) {
onEvent(DisconnectEvent) 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 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 sealed class CGMViewEvent
internal data class OnWorkingModeSelected(val workingMode: WorkingMode) : CGMViewEvent() internal data class OnWorkingModeSelected(val workingMode: CGMServiceCommand) : CGMViewEvent()
internal object DisconnectEvent : 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 package no.nordicsemi.android.cgms.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.cgms.data.CGMRepository 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.CGMViewEvent
import no.nordicsemi.android.cgms.view.DisconnectEvent 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.cgms.view.OnWorkingModeSelected
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 CGMScreenViewModel @Inject constructor( internal class CGMScreenViewModel @Inject constructor(
private val dataHolder: CGMRepository private val repository: CGMRepository
) : CloseableViewModel() { ) : 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) { fun onEvent(event: CGMViewEvent) {
when (event) { when (event) {
DisconnectEvent -> disconnect() DisconnectEvent -> disconnect()
is OnWorkingModeSelected -> dataHolder.requestNewWorkingMode(event.workingMode) is OnWorkingModeSelected -> repository.sendNewServiceCommand(event.workingMode)
}.exhaustive }.exhaustive
} }
private fun disconnect() { private fun disconnect() {
finish() repository.clear()
dataHolder.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.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.Icons
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Button import androidx.compose.material3.Button
@@ -26,12 +28,11 @@ internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
SelectWheelSizeDialog { onEvent(it) } SelectWheelSizeDialog { onEvent(it) }
} }
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(horizontal = 16.dp) modifier = Modifier.padding(16.dp)
) { ) {
Spacer(modifier = Modifier.height(16.dp))
SettingsSection(state, onEvent) SettingsSection(state, onEvent)
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@@ -46,6 +47,7 @@ internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
Text(text = stringResource(id = R.string.disconnect)) Text(text = stringResource(id = R.string.disconnect))
} }
} }
}
} }
@Composable @Composable

View File

@@ -39,7 +39,7 @@ internal class PRXService : ForegroundBleService() {
DisableAlarm -> manager.writeImmediateAlert(false) DisableAlarm -> manager.writeImmediateAlert(false)
EnableAlarm -> manager.writeImmediateAlert(true) EnableAlarm -> manager.writeImmediateAlert(true)
}.exhaustive }.exhaustive
}.launchIn(lifecycleScope) }.launchIn(scope)
dataHolder.data.onEach { dataHolder.data.onEach {
if (it.localAlarmLevel != AlarmLevel.NONE) { if (it.localAlarmLevel != AlarmLevel.NONE) {
@@ -47,7 +47,7 @@ internal class PRXService : ForegroundBleService() {
} else { } else {
alarmHandler.pauseAlarm() alarmHandler.pauseAlarm()
} }
}.launchIn(lifecycleScope) }.launchIn(scope)
} }
override fun onDestroy() { 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.bluetooth.BluetoothGattCharacteristic
import android.content.Context import android.content.Context
import no.nordicsemi.android.ble.common.callback.rsc.RunningSpeedAndCadenceMeasurementDataCallback 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.rscs.data.RSCSRepository
import no.nordicsemi.android.service.BatteryManager import no.nordicsemi.android.service.BatteryManager
import java.util.* import java.util.*
@@ -46,13 +44,6 @@ internal class RSCSManager internal constructor(
private var rscMeasurementCharacteristic: BluetoothGattCharacteristic? = null private var rscMeasurementCharacteristic: BluetoothGattCharacteristic? = null
private val callback = object : RunningSpeedAndCadenceMeasurementDataCallback() { 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( override fun onRSCMeasurementReceived(
device: BluetoothDevice, device: BluetoothDevice,

View File

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

View File

@@ -1,3 +1,7 @@
package no.nordicsemi.android.uart.data 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 package no.nordicsemi.android.uart.repository
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.service.ForegroundBleService 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.uart.data.UARTRepository
import no.nordicsemi.android.utils.exhaustive
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
internal class UARTService : ForegroundBleService() { internal class UARTService : ForegroundBleService() {
@Inject @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() { override fun onCreate() {
super.onCreate() super.onCreate()
dataHolder.command.onEach { status.onEach {
manager.send(it.command) repository.setNewStatus(it)
}.launchIn(lifecycleScope) }.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.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.theme.view.DeviceConnectingView
import no.nordicsemi.android.uart.R 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.repository.UARTService
import no.nordicsemi.android.uart.viewmodel.UARTViewModel import no.nordicsemi.android.uart.viewmodel.UARTViewModel
import no.nordicsemi.android.utils.isServiceRunning import no.nordicsemi.android.utils.exhaustive
@Composable @Composable
fun UARTScreen(finishAction: () -> Unit) { fun UARTScreen(finishAction: () -> Unit) {
val viewModel: UARTViewModel = hiltViewModel() val viewModel: UARTViewModel = 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(UARTService::class.java.name)) {
val intent = Intent(context, UARTService::class.java)
context.stopService(intent)
}
}
LaunchedEffect("start-service") {
if (!context.isServiceRunning(UARTService::class.java.name)) {
val intent = Intent(context, UARTService::class.java) val intent = Intent(context, UARTService::class.java)
context.startService(intent) context.startService(intent)
} else if (!state.isActive) {
finishAction()
} }
} }
UARTView(state) { viewModel.onEvent(it) } UARTView(state.viewState) { viewModel.onEvent(it) }
} }
@Composable @Composable
private fun UARTView(state: UARTData, onEvent: (UARTViewEvent) -> Unit) { private fun UARTView(state: UARTViewState, onEvent: (UARTViewEvent) -> Unit) {
Column { Column {
BackIconAppBar(stringResource(id = R.string.uart_title)) { BackIconAppBar(stringResource(id = R.string.uart_title)) {
onEvent(OnDisconnectButtonClick) 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 package no.nordicsemi.android.uart.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.service.BleManagerStatus
import no.nordicsemi.android.theme.viewmodel.CloseableViewModel 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.UARTRepository
import no.nordicsemi.android.uart.data.UARTServiceCommand import no.nordicsemi.android.uart.view.DisplayDataState
import no.nordicsemi.android.uart.view.* 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 no.nordicsemi.android.utils.exhaustive
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
internal class UARTViewModel @Inject constructor( internal class UARTViewModel @Inject constructor(
private val dataHolder: UARTRepository private val repository: UARTRepository
) : CloseableViewModel() { ) : 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) { fun onEvent(event: UARTViewEvent) {
when (event) { when (event) {
is OnCreateMacro -> dataHolder.addNewMacro(event.macro) is OnCreateMacro -> repository.addNewMacro(event.macro)
is OnDeleteMacro -> dataHolder.deleteMacro(event.macro) is OnDeleteMacro -> repository.deleteMacro(event.macro)
OnDisconnectButtonClick -> finish() OnDisconnectButtonClick -> repository.sendNewCommand(DisconnectCommand)
is OnRunMacro -> dataHolder.sendNewCommand(UARTServiceCommand(event.macro.command)) is OnRunMacro -> repository.sendNewCommand(SendTextCommand(event.macro.command))
}.exhaustive }.exhaustive
} }
} }

View File

@@ -37,6 +37,10 @@ dependencyResolutionManagement {
alias('compose-navigation').to('androidx.navigation:navigation-compose:2.4.0-alpha09') 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']) 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') version('hilt', '2.38.1')
alias('hilt-android').to('com.google.dagger', 'hilt-android').versionRef('hilt') alias('hilt-android').to('com.google.dagger', 'hilt-android').versionRef('hilt')
alias('hilt-compiler').to('com.google.dagger', 'hilt-compiler').versionRef('hilt') alias('hilt-compiler').to('com.google.dagger', 'hilt-compiler').versionRef('hilt')