Merge pull request #113 from NordicSemiconductor/feature/uart_improvement

Feature/uart improvement
This commit is contained in:
Sylwester Zieliński
2022-05-10 15:43:46 +02:00
committed by GitHub
72 changed files with 1070 additions and 712 deletions

View File

@@ -34,6 +34,7 @@
android:exported="true" android:exported="true"
android:label="@string/app_name" android:label="@string/app_name"
android:launchMode="singleTask" android:launchMode="singleTask"
android:windowSoftInputMode="stateVisible|adjustResize"
android:theme="@style/AppTheme.SplashScreen"> android:theme="@style/AppTheme.SplashScreen">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@@ -2,7 +2,6 @@ package no.nordicsemi.android.nrftoolbox
import android.os.Bundle import android.os.Bundle
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@@ -13,7 +12,6 @@ import no.nordicsemi.android.material.you.NordicActivity
import no.nordicsemi.android.material.you.NordicTheme import no.nordicsemi.android.material.you.NordicTheme
import no.nordicsemi.android.navigation.NavigationView import no.nordicsemi.android.navigation.NavigationView
import no.nordicsemi.android.nrftoolbox.repository.ActivitySignals import no.nordicsemi.android.nrftoolbox.repository.ActivitySignals
import no.nordicsemi.android.nrftoolbox.viewmodel.HomeViewModel
import no.nordicsemi.ui.scanner.ScannerDestinations import no.nordicsemi.ui.scanner.ScannerDestinations
import javax.inject.Inject import javax.inject.Inject

View File

@@ -1,5 +1,8 @@
package no.nordicsemi.android.service package no.nordicsemi.android.service
import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice
sealed class BleManagerResult <T> { sealed class BleManagerResult <T> {
fun isRunning(): Boolean { fun isRunning(): Boolean {
@@ -15,8 +18,13 @@ sealed class BleManagerResult <T> {
} }
} }
class IdleResult<T> : BleManagerResult<T>()
class ConnectingResult<T> : BleManagerResult<T>() class ConnectingResult<T> : BleManagerResult<T>()
data class SuccessResult<T>(val data: T) : BleManagerResult<T>() data class SuccessResult<T>(val device: BluetoothDevice, val data: T) : BleManagerResult<T>() {
@SuppressLint("MissingPermission")
fun deviceName(): String = device.name ?: device.address
}
class LinkLossResult<T>(val data: T) : BleManagerResult<T>() class LinkLossResult<T>(val data: T) : BleManagerResult<T>()
class DisconnectedResult<T> : BleManagerResult<T>() class DisconnectedResult<T> : BleManagerResult<T>()

View File

@@ -34,7 +34,7 @@ class ConnectionObserverAdapter<T> : ConnectionObserver {
override fun onDeviceReady(device: BluetoothDevice) { override fun onDeviceReady(device: BluetoothDevice) {
Log.d(TAG, "onDeviceReady()") Log.d(TAG, "onDeviceReady()")
_status.value = SuccessResult(lastValue!!) _status.value = SuccessResult(device, lastValue!!)
} }
override fun onDeviceDisconnecting(device: BluetoothDevice) { override fun onDeviceDisconnecting(device: BluetoothDevice) {
@@ -53,8 +53,8 @@ class ConnectionObserverAdapter<T> : ConnectionObserver {
fun setValue(value: T) { fun setValue(value: T) {
lastValue = value lastValue = value
if (_status.value.isRunning()) { (_status.value as? SuccessResult)?.let {
_status.value = SuccessResult(value) _status.value = SuccessResult(it.device, value)
} }
} }
} }

View File

@@ -4,16 +4,10 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Icon import androidx.compose.material3.*
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SmallTopAppBar
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -56,6 +50,39 @@ fun TitleAppBar(text: String) {
) )
} }
@Composable
fun LoggerBackIconAppBar(text: String, onClick: () -> Unit) {
SmallTopAppBar(
title = { Text(text) },
colors = TopAppBarDefaults.smallTopAppBarColors(
scrolledContainerColor = MaterialTheme.colorScheme.primary,
containerColor = colorResource(id = R.color.appBarColor),
titleContentColor = MaterialTheme.colorScheme.onPrimary,
actionIconContentColor = MaterialTheme.colorScheme.onPrimary,
navigationIconContentColor = MaterialTheme.colorScheme.onPrimary,
),
navigationIcon = {
IconButton(onClick = { onClick() }) {
Icon(
Icons.Default.ArrowBack,
tint = MaterialTheme.colorScheme.onPrimary,
contentDescription = stringResource(id = R.string.back_screen),
)
}
},
actions = {
IconButton(onClick = { onClick() }) {
Icon(
painterResource(id = R.drawable.ic_logger),
contentDescription = stringResource(id = R.string.open_logger),
tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.size(24.dp)
)
}
}
)
}
@Composable @Composable
fun BackIconAppBar(text: String, onClick: () -> Unit) { fun BackIconAppBar(text: String, onClick: () -> Unit) {
SmallTopAppBar( SmallTopAppBar(
@@ -71,25 +98,16 @@ fun BackIconAppBar(text: String, onClick: () -> Unit) {
IconButton(onClick = { onClick() }) { IconButton(onClick = { onClick() }) {
Icon( Icon(
Icons.Default.ArrowBack, Icons.Default.ArrowBack,
tint = MaterialTheme.colorScheme.onPrimary,
contentDescription = stringResource(id = R.string.back_screen), contentDescription = stringResource(id = R.string.back_screen),
) )
} }
}, },
actions = {
IconButton(onClick = { onClick() }) {
Icon(
painterResource(id = R.drawable.ic_logger),
contentDescription = stringResource(id = R.string.back_screen),
tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.size(24.dp)
)
}
}
) )
} }
@Composable @Composable
fun LoggerIconAppBar(text: String, onClick: () -> Unit, onLoggerClick: () -> Unit) { fun LoggerIconAppBar(text: String, onClick: () -> Unit, onDisconnectClick: () -> Unit, onLoggerClick: () -> Unit) {
SmallTopAppBar( SmallTopAppBar(
title = { Text(text) }, title = { Text(text) },
colors = TopAppBarDefaults.smallTopAppBarColors( colors = TopAppBarDefaults.smallTopAppBarColors(
@@ -103,15 +121,25 @@ fun LoggerIconAppBar(text: String, onClick: () -> Unit, onLoggerClick: () -> Uni
IconButton(onClick = { onClick() }) { IconButton(onClick = { onClick() }) {
Icon( Icon(
Icons.Default.ArrowBack, Icons.Default.ArrowBack,
tint = MaterialTheme.colorScheme.onPrimary,
contentDescription = stringResource(id = R.string.back_screen), contentDescription = stringResource(id = R.string.back_screen),
) )
} }
}, },
actions = { actions = {
TextButton(
onClick = { onDisconnectClick() },
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onPrimary
)
) {
Text(stringResource(id = R.string.disconnect))
}
IconButton(onClick = { onLoggerClick() }) { IconButton(onClick = { onLoggerClick() }) {
Icon( Icon(
painterResource(id = R.drawable.ic_logger), painterResource(id = R.drawable.ic_logger),
contentDescription = stringResource(id = R.string.back_screen), contentDescription = stringResource(id = R.string.open_logger),
tint = MaterialTheme.colorScheme.onPrimary, tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.size(24.dp) modifier = Modifier.size(24.dp)
) )

View File

@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@@ -17,43 +18,16 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import no.nordicsemi.android.material.you.Card
import no.nordicsemi.android.theme.R import no.nordicsemi.android.theme.R
@Composable @Composable
fun StringListDialog(config: StringListDialogConfig) { fun StringListDialog(config: StringListDialogConfig) {
Dialog(onDismissRequest = { config.onResult(FlowCanceled) }) { AlertDialog(
StringListView(config) onDismissRequest = { config.onResult(FlowCanceled) },
} title = { Text(text = config.title ?: stringResource(id = R.string.dialog).toAnnotatedString()) },
} text = {
@Composable
fun StringListView(config: StringListDialogConfig) {
Card(
modifier = Modifier.height(300.dp),
backgroundColor = MaterialTheme.colorScheme.surfaceVariant,
shape = RoundedCornerShape(10.dp),
elevation = 0.dp
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.SpaceBetween
) {
Column(
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = config.title ?: stringResource(id = R.string.dialog).toAnnotatedString(),
style = MaterialTheme.typography.headlineMedium
)
}
Spacer(modifier = Modifier.size(8.dp))
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxHeight(0.8f)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
@@ -81,17 +55,13 @@ fun StringListView(config: StringListDialogConfig) {
} }
} }
} }
},
Column( confirmButton = {
modifier = Modifier.fillMaxWidth(), TextButton(onClick = { config.onResult(FlowCanceled) }) {
horizontalAlignment = Alignment.End Text(
) { text = stringResource(id = R.string.cancel),
TextButton(onClick = { config.onResult(FlowCanceled) }) { )
Text(
text = stringResource(id = R.string.cancel),
)
}
} }
} }
} )
} }

View File

@@ -3,12 +3,13 @@
<string name="app_name">nRF Toolbox</string> <string name="app_name">nRF Toolbox</string>
<string name="dialog">Dialog</string> <string name="dialog">Dialog</string>
<string name="cancel">CANCEL</string> <string name="cancel">Cancel</string>
<string name="go_up">Back</string> <string name="go_up">Back</string>
<string name="close_app">Close the application.</string> <string name="close_app">Close the application.</string>
<string name="back_screen">Close the current screen.</string> <string name="back_screen">Close the current screen.</string>
<string name="open_logger">Open logger application.</string>
<string name="disconnect">Disconnect</string> <string name="disconnect">Disconnect</string>
<string name="field_battery">Battery</string> <string name="field_battery">Battery</string>

View File

@@ -61,7 +61,7 @@ internal class BPSManager(
val dataHolder = ConnectionObserverAdapter<BPSData>() val dataHolder = ConnectionObserverAdapter<BPSData>()
init { init {
setConnectionObserver(dataHolder) connectionObserver = dataHolder
data.onEach { data.onEach {
dataHolder.setValue(it) dataHolder.setValue(it)

View File

@@ -1,6 +1,5 @@
package no.nordicsemi.android.bps.repository package no.nordicsemi.android.bps.repository
import android.bluetooth.BluetoothDevice
import android.content.Context import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ViewModelScoped import dagger.hilt.android.scopes.ViewModelScoped
@@ -14,6 +13,7 @@ import no.nordicsemi.android.bps.data.BPSManager
import no.nordicsemi.android.logger.ToolboxLogger import no.nordicsemi.android.logger.ToolboxLogger
import no.nordicsemi.android.logger.ToolboxLoggerFactory import no.nordicsemi.android.logger.ToolboxLoggerFactory
import no.nordicsemi.android.service.BleManagerResult import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject import javax.inject.Inject
@ViewModelScoped @ViewModelScoped
@@ -25,9 +25,9 @@ internal class BPSRepository @Inject constructor(
private var logger: ToolboxLogger? = null private var logger: ToolboxLogger? = null
fun downloadData(device: BluetoothDevice): Flow<BleManagerResult<BPSData>> = callbackFlow { fun downloadData(device: DiscoveredBluetoothDevice): Flow<BleManagerResult<BPSData>> = callbackFlow {
val scope = this val scope = this
val createdLogger = toolboxLoggerFactory.create("BPS", device.address).also { val createdLogger = toolboxLoggerFactory.create("BPS", device.address()).also {
logger = it logger = it
} }
val manager = BPSManager(context, scope, createdLogger) val manager = BPSManager(context, scope, createdLogger)
@@ -36,7 +36,7 @@ internal class BPSRepository @Inject constructor(
trySend(it) trySend(it)
}.launchIn(scope) }.launchIn(scope)
manager.connect(device) manager.connect(device.device)
.useAutoConnect(false) .useAutoConnect(false)
.retry(3, 100) .retry(3, 100)
.enqueue() .enqueue()

View File

@@ -15,7 +15,7 @@ import no.nordicsemi.android.bps.R
import no.nordicsemi.android.bps.data.BPSData import no.nordicsemi.android.bps.data.BPSData
@Composable @Composable
internal fun BPSContentView(state: BPSData, onEvent: (BPSScreenViewEvent) -> Unit) { internal fun BPSContentView(state: BPSData, onEvent: (BPSViewEvent) -> Unit) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(16.dp)

View File

@@ -1,5 +1,6 @@
package no.nordicsemi.android.bps.view package no.nordicsemi.android.bps.view
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
@@ -9,14 +10,15 @@ import androidx.compose.ui.Modifier
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.bps.R import no.nordicsemi.android.bps.R
import no.nordicsemi.android.bps.data.BPSData
import no.nordicsemi.android.bps.viewmodel.BPSViewModel import no.nordicsemi.android.bps.viewmodel.BPSViewModel
import no.nordicsemi.android.service.* import no.nordicsemi.android.service.*
import no.nordicsemi.android.theme.view.BackIconAppBar import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.LoggerIconAppBar import no.nordicsemi.android.theme.view.LoggerIconAppBar
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.android.utils.exhaustive import no.nordicsemi.android.utils.exhaustive
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.DeviceDisconnectedView import no.nordicsemi.ui.scanner.ui.DeviceDisconnectedView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.ui.scanner.ui.Reason import no.nordicsemi.ui.scanner.ui.Reason
@Composable @Composable
@@ -27,16 +29,13 @@ fun BPSScreen() {
Column { Column {
val navigateUp = { viewModel.onEvent(DisconnectEvent) } val navigateUp = { viewModel.onEvent(DisconnectEvent) }
LoggerIconAppBar(stringResource(id = R.string.bps_title), { AppBar(state = state, navigateUp = navigateUp, viewModel = viewModel)
viewModel.onEvent(DisconnectEvent)
}) {
viewModel.onEvent(OpenLoggerEvent)
}
Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) { when (state) {
NoDeviceState -> NoDeviceView() NoDeviceState -> NoDeviceView()
is WorkingState -> when (state.result) { is WorkingState -> when (state.result) {
is IdleResult,
is ConnectingResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) } is ConnectingResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) }
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp) is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp)
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp) is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp)
@@ -48,3 +47,20 @@ fun BPSScreen() {
} }
} }
} }
@Composable
private fun AppBar(state: BPSViewState, navigateUp: () -> Unit, viewModel: BPSViewModel) {
val toolbarName = (state as? WorkingState)?.let {
(it.result as? SuccessResult<BPSData>)?.deviceName()
}
if (toolbarName == null) {
BackIconAppBar(stringResource(id = R.string.bps_title), navigateUp)
} else {
LoggerIconAppBar(toolbarName, {
viewModel.onEvent(DisconnectEvent)
}, { viewModel.onEvent(DisconnectEvent) }) {
viewModel.onEvent(OpenLoggerEvent)
}
}
}

View File

@@ -1,7 +0,0 @@
package no.nordicsemi.android.bps.view
internal sealed class BPSScreenViewEvent
internal object DisconnectEvent : BPSScreenViewEvent()
internal object OpenLoggerEvent : BPSScreenViewEvent()

View File

@@ -0,0 +1,7 @@
package no.nordicsemi.android.bps.view
internal sealed class BPSViewEvent
internal object DisconnectEvent : BPSViewEvent()
internal object OpenLoggerEvent : BPSViewEvent()

View File

@@ -2,8 +2,12 @@ package no.nordicsemi.android.bps.view
import no.nordicsemi.android.bps.data.BPSData import no.nordicsemi.android.bps.data.BPSData
import no.nordicsemi.android.service.BleManagerResult import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
internal sealed class BPSViewState internal sealed class BPSViewState
internal data class WorkingState(val result: BleManagerResult<BPSData>) : BPSViewState() internal data class WorkingState(
val result: BleManagerResult<BPSData>
) : BPSViewState()
internal object NoDeviceState : BPSViewState() internal object NoDeviceState : BPSViewState()

View File

@@ -43,7 +43,7 @@ internal class BPSViewModel @Inject constructor(
}.exhaustive }.exhaustive
} }
fun onEvent(event: BPSScreenViewEvent) { fun onEvent(event: BPSViewEvent) {
when (event) { when (event) {
DisconnectEvent -> navigationManager.navigateUp() DisconnectEvent -> navigationManager.navigateUp()
OpenLoggerEvent -> repository.openLogger() OpenLoggerEvent -> repository.openLogger()
@@ -51,7 +51,7 @@ internal class BPSViewModel @Inject constructor(
} }
private fun connectDevice(device: DiscoveredBluetoothDevice) { private fun connectDevice(device: DiscoveredBluetoothDevice) {
repository.downloadData(device.device).onEach { repository.downloadData(device).onEach {
_state.value = WorkingState(it) _state.value = WorkingState(it)
}.launchIn(viewModelScope) }.launchIn(viewModelScope)
} }

View File

@@ -85,7 +85,7 @@ internal class CGMManager(
val dataHolder = ConnectionObserverAdapter<CGMData>() val dataHolder = ConnectionObserverAdapter<CGMData>()
init { init {
setConnectionObserver(dataHolder) connectionObserver = dataHolder
data.onEach { data.onEach {
dataHolder.setValue(it) dataHolder.setValue(it)

View File

@@ -14,6 +14,7 @@ import no.nordicsemi.android.logger.ToolboxLoggerFactory
import no.nordicsemi.android.service.BleManagerResult import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.service.ConnectingResult import no.nordicsemi.android.service.ConnectingResult
import no.nordicsemi.android.service.ServiceManager import no.nordicsemi.android.service.ServiceManager
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -33,12 +34,12 @@ class CGMRepository @Inject constructor(
val isRunning = data.map { it.isRunning() } val isRunning = data.map { it.isRunning() }
val hasBeenDisconnected = data.map { it.hasBeenDisconnected() } val hasBeenDisconnected = data.map { it.hasBeenDisconnected() }
fun launch(device: BluetoothDevice) { fun launch(device: DiscoveredBluetoothDevice) {
serviceManager.startService(CGMService::class.java, device) serviceManager.startService(CGMService::class.java, device)
} }
fun start(device: BluetoothDevice, scope: CoroutineScope) { fun start(device: DiscoveredBluetoothDevice, scope: CoroutineScope) {
val createdLogger = toolboxLoggerFactory.create("CGMS", device.address).also { val createdLogger = toolboxLoggerFactory.create("CGMS", device.address()).also {
logger = it logger = it
} }
val manager = CGMManager(context, scope, createdLogger) val manager = CGMManager(context, scope, createdLogger)
@@ -53,9 +54,9 @@ class CGMRepository @Inject constructor(
} }
} }
private suspend fun CGMManager.start(device: BluetoothDevice) { private suspend fun CGMManager.start(device: DiscoveredBluetoothDevice) {
try { try {
connect(device) connect(device.device)
.useAutoConnect(false) .useAutoConnect(false)
.retry(3, 100) .retry(3, 100)
.suspend() .suspend()

View File

@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.service.DEVICE_DATA import no.nordicsemi.android.service.DEVICE_DATA
import no.nordicsemi.android.service.NotificationService import no.nordicsemi.android.service.NotificationService
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@@ -19,7 +20,7 @@ internal class CGMService : NotificationService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId) super.onStartCommand(intent, flags, startId)
val device = intent!!.getParcelableExtra<BluetoothDevice>(DEVICE_DATA)!! val device = intent!!.getParcelableExtra<DiscoveredBluetoothDevice>(DEVICE_DATA)!!
repository.start(device, lifecycleScope) repository.start(device, lifecycleScope)

View File

@@ -9,32 +9,32 @@ import androidx.compose.ui.Modifier
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.viewmodel.CGMScreenViewModel import no.nordicsemi.android.cgms.data.CGMData
import no.nordicsemi.android.cgms.viewmodel.CGMViewModel
import no.nordicsemi.android.service.* import no.nordicsemi.android.service.*
import no.nordicsemi.android.theme.view.BackIconAppBar import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.LoggerIconAppBar import no.nordicsemi.android.theme.view.LoggerIconAppBar
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.android.utils.exhaustive import no.nordicsemi.android.utils.exhaustive
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.DeviceDisconnectedView import no.nordicsemi.ui.scanner.ui.DeviceDisconnectedView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.ui.scanner.ui.Reason import no.nordicsemi.ui.scanner.ui.Reason
@Composable @Composable
fun CGMScreen() { fun CGMScreen() {
val viewModel: CGMScreenViewModel = hiltViewModel() val viewModel: CGMViewModel = hiltViewModel()
val state = viewModel.state.collectAsState().value val state = viewModel.state.collectAsState().value
Column { Column {
val navigateUp = { viewModel.onEvent(NavigateUp) } val navigateUp = { viewModel.onEvent(NavigateUp) }
LoggerIconAppBar(stringResource(id = R.string.cgms_title), navigateUp) { AppBar(state, navigateUp, viewModel)
viewModel.onEvent(OpenLoggerEvent)
}
Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) { when (state) {
NoDeviceState -> NoDeviceView() NoDeviceState -> NoDeviceView()
is WorkingState -> when (state.result) { is WorkingState -> when (state.result) {
is IdleResult,
is ConnectingResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) } is ConnectingResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) }
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp) is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp)
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp) is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp)
@@ -46,3 +46,18 @@ fun CGMScreen() {
} }
} }
} }
@Composable
private fun AppBar(state: CGMViewState, navigateUp: () -> Unit, viewModel: CGMViewModel) {
val toolbarName = (state as? WorkingState)?.let {
(it.result as? SuccessResult<CGMData>)?.deviceName()
}
if (toolbarName == null) {
BackIconAppBar(stringResource(id = R.string.cgms_title), navigateUp)
} else {
LoggerIconAppBar(toolbarName, navigateUp, { viewModel.onEvent(DisconnectEvent) }) {
viewModel.onEvent(OpenLoggerEvent)
}
}
}

View File

@@ -3,7 +3,7 @@ package no.nordicsemi.android.cgms.view
import no.nordicsemi.android.cgms.data.CGMData import no.nordicsemi.android.cgms.data.CGMData
import no.nordicsemi.android.service.BleManagerResult import no.nordicsemi.android.service.BleManagerResult
internal sealed class BPSViewState internal sealed class CGMViewState
internal data class WorkingState(val result: BleManagerResult<CGMData>) : BPSViewState() internal data class WorkingState(val result: BleManagerResult<CGMData>) : CGMViewState()
internal object NoDeviceState : BPSViewState() internal object NoDeviceState : CGMViewState()

View File

@@ -16,12 +16,12 @@ import no.nordicsemi.ui.scanner.ScannerDestinationId
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
internal class CGMScreenViewModel @Inject constructor( internal class CGMViewModel @Inject constructor(
private val repository: CGMRepository, private val repository: CGMRepository,
private val navigationManager: NavigationManager private val navigationManager: NavigationManager
) : ViewModel() { ) : ViewModel() {
private val _state = MutableStateFlow<BPSViewState>(NoDeviceState) private val _state = MutableStateFlow<CGMViewState>(NoDeviceState)
val state = _state.asStateFlow() val state = _state.asStateFlow()
init { init {
@@ -58,7 +58,7 @@ internal class CGMScreenViewModel @Inject constructor(
private fun handleArgs(args: DestinationResult) { private fun handleArgs(args: DestinationResult) {
when (args) { when (args) {
is CancelDestinationResult -> navigationManager.navigateUp() is CancelDestinationResult -> navigationManager.navigateUp()
is SuccessDestinationResult -> repository.launch(args.getDevice().device) is SuccessDestinationResult -> repository.launch(args.getDevice())
}.exhaustive }.exhaustive
} }

View File

@@ -59,7 +59,7 @@ internal class CSCManager(
val dataHolder = ConnectionObserverAdapter<CSCData>() val dataHolder = ConnectionObserverAdapter<CSCData>()
init { init {
setConnectionObserver(dataHolder) connectionObserver = dataHolder
data.onEach { data.onEach {
dataHolder.setValue(it) dataHolder.setValue(it)

View File

@@ -15,6 +15,7 @@ import no.nordicsemi.android.logger.ToolboxLoggerFactory
import no.nordicsemi.android.service.BleManagerResult import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.service.ConnectingResult import no.nordicsemi.android.service.ConnectingResult
import no.nordicsemi.android.service.ServiceManager import no.nordicsemi.android.service.ServiceManager
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -34,12 +35,12 @@ class CSCRepository @Inject constructor(
val isRunning = data.map { it.isRunning() } val isRunning = data.map { it.isRunning() }
val hasBeenDisconnected = data.map { it.hasBeenDisconnected() } val hasBeenDisconnected = data.map { it.hasBeenDisconnected() }
fun launch(device: BluetoothDevice) { fun launch(device: DiscoveredBluetoothDevice) {
serviceManager.startService(CSCService::class.java, device) serviceManager.startService(CSCService::class.java, device)
} }
fun start(device: BluetoothDevice, scope: CoroutineScope) { fun start(device: DiscoveredBluetoothDevice, scope: CoroutineScope) {
val createdLogger = toolboxLoggerFactory.create("CSC", device.address).also { val createdLogger = toolboxLoggerFactory.create("CSC", device.address()).also {
logger = it logger = it
} }
val manager = CSCManager(context, scope, createdLogger) val manager = CSCManager(context, scope, createdLogger)
@@ -58,9 +59,9 @@ class CSCRepository @Inject constructor(
manager?.setWheelSize(wheelSize) manager?.setWheelSize(wheelSize)
} }
private suspend fun CSCManager.start(device: BluetoothDevice) { private suspend fun CSCManager.start(device: DiscoveredBluetoothDevice) {
try { try {
connect(device) connect(device.device)
.useAutoConnect(false) .useAutoConnect(false)
.retry(3, 100) .retry(3, 100)
.suspend() .suspend()

View File

@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.service.DEVICE_DATA import no.nordicsemi.android.service.DEVICE_DATA
import no.nordicsemi.android.service.NotificationService import no.nordicsemi.android.service.NotificationService
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@@ -19,7 +20,7 @@ internal class CSCService : NotificationService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId) super.onStartCommand(intent, flags, startId)
val device = intent!!.getParcelableExtra<BluetoothDevice>(DEVICE_DATA)!! val device = intent!!.getParcelableExtra<DiscoveredBluetoothDevice>(DEVICE_DATA)!!
repository.start(device, lifecycleScope) repository.start(device, lifecycleScope)

View File

@@ -9,14 +9,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.csc.R import no.nordicsemi.android.csc.R
import no.nordicsemi.android.csc.data.CSCData
import no.nordicsemi.android.csc.viewmodel.CSCViewModel import no.nordicsemi.android.csc.viewmodel.CSCViewModel
import no.nordicsemi.android.service.* import no.nordicsemi.android.service.*
import no.nordicsemi.android.theme.view.BackIconAppBar import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.LoggerIconAppBar import no.nordicsemi.android.theme.view.LoggerIconAppBar
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.android.utils.exhaustive import no.nordicsemi.android.utils.exhaustive
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.DeviceDisconnectedView import no.nordicsemi.ui.scanner.ui.DeviceDisconnectedView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.ui.scanner.ui.Reason import no.nordicsemi.ui.scanner.ui.Reason
@Composable @Composable
@@ -27,14 +28,13 @@ fun CSCScreen() {
Column { Column {
val navigateUp = { viewModel.onEvent(NavigateUp) } val navigateUp = { viewModel.onEvent(NavigateUp) }
LoggerIconAppBar(stringResource(id = R.string.csc_title), navigateUp) { AppBar(state, navigateUp, viewModel)
viewModel.onEvent(OpenLogger)
}
Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state.cscManagerState) { when (state.cscManagerState) {
NoDeviceState -> NoDeviceView() NoDeviceState -> NoDeviceView()
is WorkingState -> when (state.cscManagerState.result) { is WorkingState -> when (state.cscManagerState.result) {
is IdleResult,
is ConnectingResult -> DeviceConnectingView { viewModel.onEvent(OnDisconnectButtonClick) } is ConnectingResult -> DeviceConnectingView { viewModel.onEvent(OnDisconnectButtonClick) }
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp) is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp)
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp) is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp)
@@ -46,3 +46,18 @@ fun CSCScreen() {
} }
} }
} }
@Composable
private fun AppBar(state: CSCViewState, navigateUp: () -> Unit, viewModel: CSCViewModel) {
val toolbarName = (state.cscManagerState as? WorkingState)?.let {
(it.result as? SuccessResult<CSCData>)?.deviceName()
}
if (toolbarName == null) {
BackIconAppBar(stringResource(id = R.string.csc_title), navigateUp)
} else {
LoggerIconAppBar(toolbarName, navigateUp, { viewModel.onEvent(OnDisconnectButtonClick) }) {
viewModel.onEvent(OpenLogger)
}
}
}

View File

@@ -48,7 +48,7 @@ internal class CSCViewModel @Inject constructor(
private fun handleArgs(args: DestinationResult) { private fun handleArgs(args: DestinationResult) {
when (args) { when (args) {
is CancelDestinationResult -> navigationManager.navigateUp() is CancelDestinationResult -> navigationManager.navigateUp()
is SuccessDestinationResult -> repository.launch(args.getDevice().device) is SuccessDestinationResult -> repository.launch(args.getDevice())
}.exhaustive }.exhaustive
} }

View File

@@ -71,7 +71,7 @@ internal class GLSManager(
val dataHolder = ConnectionObserverAdapter<GLSData>() val dataHolder = ConnectionObserverAdapter<GLSData>()
init { init {
setConnectionObserver(dataHolder) connectionObserver = dataHolder
data.onEach { data.onEach {
dataHolder.setValue(it) dataHolder.setValue(it)

View File

@@ -6,7 +6,7 @@ import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.gls.R import no.nordicsemi.android.gls.R
import no.nordicsemi.android.gls.details.viewmodel.GLSDetailsViewModel import no.nordicsemi.android.gls.details.viewmodel.GLSDetailsViewModel
import no.nordicsemi.android.theme.view.BackIconAppBar import no.nordicsemi.android.theme.view.LoggerBackIconAppBar
@Composable @Composable
internal fun GLSDetailsScreen() { internal fun GLSDetailsScreen() {
@@ -14,7 +14,7 @@ internal fun GLSDetailsScreen() {
val record = viewModel.record val record = viewModel.record
Column { Column {
BackIconAppBar(stringResource(id = R.string.gls_title)) { LoggerBackIconAppBar(stringResource(id = R.string.gls_title)) {
viewModel.navigateBack() viewModel.navigateBack()
} }

View File

@@ -9,14 +9,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.gls.R import no.nordicsemi.android.gls.R
import no.nordicsemi.android.gls.data.GLSData
import no.nordicsemi.android.gls.main.viewmodel.GLSViewModel import no.nordicsemi.android.gls.main.viewmodel.GLSViewModel
import no.nordicsemi.android.service.* import no.nordicsemi.android.service.*
import no.nordicsemi.android.theme.view.BackIconAppBar import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.LoggerIconAppBar import no.nordicsemi.android.theme.view.LoggerIconAppBar
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.android.utils.exhaustive import no.nordicsemi.android.utils.exhaustive
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.DeviceDisconnectedView import no.nordicsemi.ui.scanner.ui.DeviceDisconnectedView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.ui.scanner.ui.Reason import no.nordicsemi.ui.scanner.ui.Reason
@Composable @Composable
@@ -27,16 +28,13 @@ fun GLSScreen() {
Column { Column {
val navigateUp = { viewModel.onEvent(DisconnectEvent) } val navigateUp = { viewModel.onEvent(DisconnectEvent) }
LoggerIconAppBar(stringResource(id = R.string.gls_title), { AppBar(state, navigateUp, viewModel)
viewModel.onEvent(DisconnectEvent)
}) {
viewModel.onEvent(OpenLoggerEvent)
}
Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) { when (state) {
NoDeviceState -> NoDeviceView() NoDeviceState -> NoDeviceView()
is WorkingState -> when (state.result) { is WorkingState -> when (state.result) {
is IdleResult,
is ConnectingResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) } is ConnectingResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) }
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp) is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp)
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp) is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp)
@@ -48,3 +46,20 @@ fun GLSScreen() {
} }
} }
} }
@Composable
private fun AppBar(state: GLSViewState, navigateUp: () -> Unit, viewModel: GLSViewModel) {
val toolbarName = (state as? WorkingState)?.let {
(it.result as? SuccessResult<GLSData>)?.deviceName()
}
if (toolbarName == null) {
BackIconAppBar(stringResource(id = R.string.gls_title), navigateUp)
} else {
LoggerIconAppBar(toolbarName, {
viewModel.onEvent(DisconnectEvent)
}, { viewModel.onEvent(DisconnectEvent) }) {
viewModel.onEvent(OpenLoggerEvent)
}
}
}

View File

@@ -3,7 +3,7 @@ package no.nordicsemi.android.gls.main.view
import no.nordicsemi.android.gls.data.GLSData import no.nordicsemi.android.gls.data.GLSData
import no.nordicsemi.android.service.BleManagerResult import no.nordicsemi.android.service.BleManagerResult
internal sealed class BPSViewState internal sealed class GLSViewState
internal data class WorkingState(val result: BleManagerResult<GLSData>) : BPSViewState() internal data class WorkingState(val result: BleManagerResult<GLSData>) : GLSViewState()
internal object NoDeviceState : BPSViewState() internal object NoDeviceState : GLSViewState()

View File

@@ -21,7 +21,7 @@ internal class GLSViewModel @Inject constructor(
private val navigationManager: NavigationManager private val navigationManager: NavigationManager
) : ViewModel() { ) : ViewModel() {
private val _state = MutableStateFlow<BPSViewState>(NoDeviceState) private val _state = MutableStateFlow<GLSViewState>(NoDeviceState)
val state = _state.asStateFlow() val state = _state.asStateFlow()
init { init {
@@ -52,7 +52,7 @@ internal class GLSViewModel @Inject constructor(
} }
private fun connectDevice(device: DiscoveredBluetoothDevice) { private fun connectDevice(device: DiscoveredBluetoothDevice) {
repository.downloadData(device.device).onEach { repository.downloadData(device).onEach {
_state.value = WorkingState(it) _state.value = WorkingState(it)
}.launchIn(viewModelScope) }.launchIn(viewModelScope)
} }

View File

@@ -18,6 +18,7 @@ import no.nordicsemi.android.logger.ToolboxLogger
import no.nordicsemi.android.logger.ToolboxLoggerFactory import no.nordicsemi.android.logger.ToolboxLoggerFactory
import no.nordicsemi.android.service.BleManagerResult import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.utils.exhaustive import no.nordicsemi.android.utils.exhaustive
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject import javax.inject.Inject
@ViewModelScoped @ViewModelScoped
@@ -30,14 +31,14 @@ internal class GLSRepository @Inject constructor(
private var manager: GLSManager? = null private var manager: GLSManager? = null
private var logger: ToolboxLogger? = null private var logger: ToolboxLogger? = null
fun downloadData(device: BluetoothDevice): Flow<BleManagerResult<GLSData>> = callbackFlow { fun downloadData(device: DiscoveredBluetoothDevice): Flow<BleManagerResult<GLSData>> = callbackFlow {
val scope = this val scope = this
val createdLogger = toolboxLoggerFactory.create("GLS", device.address).also { val createdLogger = toolboxLoggerFactory.create("GLS", device.address()).also {
logger = it logger = it
} }
val managerInstance = manager ?: GLSManager(context, scope, createdLogger).apply { val managerInstance = manager ?: GLSManager(context, scope, createdLogger).apply {
try { try {
connect(device) connect(device.device)
.useAutoConnect(false) .useAutoConnect(false)
.retry(3, 100) .retry(3, 100)
.suspend() .suspend()

View File

@@ -61,7 +61,7 @@ internal class HRSManager(
val dataHolder = ConnectionObserverAdapter<HRSData>() val dataHolder = ConnectionObserverAdapter<HRSData>()
init { init {
setConnectionObserver(dataHolder) connectionObserver = dataHolder
data.onEach { data.onEach {
dataHolder.setValue(it) dataHolder.setValue(it)

View File

@@ -14,6 +14,7 @@ import no.nordicsemi.android.logger.ToolboxLoggerFactory
import no.nordicsemi.android.service.BleManagerResult import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.service.ConnectingResult import no.nordicsemi.android.service.ConnectingResult
import no.nordicsemi.android.service.ServiceManager import no.nordicsemi.android.service.ServiceManager
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -33,12 +34,12 @@ class HRSRepository @Inject constructor(
val isRunning = data.map { it.isRunning() } val isRunning = data.map { it.isRunning() }
val hasBeenDisconnected = data.map { it.hasBeenDisconnected() } val hasBeenDisconnected = data.map { it.hasBeenDisconnected() }
fun launch(device: BluetoothDevice) { fun launch(device: DiscoveredBluetoothDevice) {
serviceManager.startService(HRSService::class.java, device) serviceManager.startService(HRSService::class.java, device)
} }
fun start(device: BluetoothDevice, scope: CoroutineScope) { fun start(device: DiscoveredBluetoothDevice, scope: CoroutineScope) {
val createdLogger = toolboxLoggerFactory.create("HRS", device.address).also { val createdLogger = toolboxLoggerFactory.create("HRS", device.address()).also {
logger = it logger = it
} }
val manager = HRSManager(context, scope, createdLogger) val manager = HRSManager(context, scope, createdLogger)
@@ -57,9 +58,9 @@ class HRSRepository @Inject constructor(
logger?.openLogger() logger?.openLogger()
} }
private suspend fun HRSManager.start(device: BluetoothDevice) { private suspend fun HRSManager.start(device: DiscoveredBluetoothDevice) {
try { try {
connect(device) connect(device.device)
.useAutoConnect(false) .useAutoConnect(false)
.retry(3, 100) .retry(3, 100)
.suspend() .suspend()

View File

@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.service.DEVICE_DATA import no.nordicsemi.android.service.DEVICE_DATA
import no.nordicsemi.android.service.NotificationService import no.nordicsemi.android.service.NotificationService
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@@ -19,7 +20,7 @@ internal class HRSService : NotificationService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId) super.onStartCommand(intent, flags, startId)
val device = intent!!.getParcelableExtra<BluetoothDevice>(DEVICE_DATA)!! val device = intent!!.getParcelableExtra<DiscoveredBluetoothDevice>(DEVICE_DATA)!!
repository.start(device, lifecycleScope) repository.start(device, lifecycleScope)

View File

@@ -9,8 +9,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.hrs.R import no.nordicsemi.android.hrs.R
import no.nordicsemi.android.hrs.data.HRSData
import no.nordicsemi.android.hrs.viewmodel.HRSViewModel import no.nordicsemi.android.hrs.viewmodel.HRSViewModel
import no.nordicsemi.android.service.* import no.nordicsemi.android.service.*
import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.LoggerIconAppBar import no.nordicsemi.android.theme.view.LoggerIconAppBar
import no.nordicsemi.android.utils.exhaustive import no.nordicsemi.android.utils.exhaustive
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
@@ -26,14 +28,13 @@ fun HRSScreen() {
Column { Column {
val navigateUp = { viewModel.onEvent(NavigateUpEvent) } val navigateUp = { viewModel.onEvent(NavigateUpEvent) }
LoggerIconAppBar(stringResource(id = R.string.hrs_title), navigateUp) { AppBar(state, navigateUp, viewModel)
viewModel.onEvent(OpenLoggerEvent)
}
Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) { when (state) {
NoDeviceState -> NoDeviceView() NoDeviceState -> NoDeviceView()
is WorkingState -> when (state.result) { is WorkingState -> when (state.result) {
is IdleResult,
is ConnectingResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) } is ConnectingResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) }
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp) is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp)
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp) is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp)
@@ -45,3 +46,18 @@ fun HRSScreen() {
} }
} }
} }
@Composable
private fun AppBar(state: HRSViewState, navigateUp: () -> Unit, viewModel: HRSViewModel) {
val toolbarName = (state as? WorkingState)?.let {
(it.result as? SuccessResult<HRSData>)?.deviceName()
}
if (toolbarName == null) {
BackIconAppBar(stringResource(id = R.string.hrs_title), navigateUp)
} else {
LoggerIconAppBar(toolbarName, navigateUp, { viewModel.onEvent(DisconnectEvent) }) {
viewModel.onEvent(OpenLoggerEvent)
}
}
}

View File

@@ -49,7 +49,7 @@ internal class HRSViewModel @Inject constructor(
private fun handleArgs(args: DestinationResult) { private fun handleArgs(args: DestinationResult) {
when (args) { when (args) {
is CancelDestinationResult -> navigationManager.navigateUp() is CancelDestinationResult -> navigationManager.navigateUp()
is SuccessDestinationResult -> repository.launch(args.getDevice().device) is SuccessDestinationResult -> repository.launch(args.getDevice())
}.exhaustive }.exhaustive
} }

View File

@@ -56,7 +56,7 @@ internal class HTSManager internal constructor(
val dataHolder = ConnectionObserverAdapter<HTSData>() val dataHolder = ConnectionObserverAdapter<HTSData>()
init { init {
setConnectionObserver(dataHolder) connectionObserver = dataHolder
data.onEach { data.onEach {
dataHolder.setValue(it) dataHolder.setValue(it)

View File

@@ -14,6 +14,7 @@ import no.nordicsemi.android.logger.ToolboxLoggerFactory
import no.nordicsemi.android.service.BleManagerResult import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.service.ConnectingResult import no.nordicsemi.android.service.ConnectingResult
import no.nordicsemi.android.service.ServiceManager import no.nordicsemi.android.service.ServiceManager
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -33,12 +34,12 @@ class HTSRepository @Inject constructor(
val isRunning = data.map { it.isRunning() } val isRunning = data.map { it.isRunning() }
val hasBeenDisconnected = data.map { it.hasBeenDisconnected() } val hasBeenDisconnected = data.map { it.hasBeenDisconnected() }
fun launch(device: BluetoothDevice) { fun launch(device: DiscoveredBluetoothDevice) {
serviceManager.startService(HTSService::class.java, device) serviceManager.startService(HTSService::class.java, device)
} }
fun start(device: BluetoothDevice, scope: CoroutineScope) { fun start(device: DiscoveredBluetoothDevice, scope: CoroutineScope) {
val createdLogger = toolboxLoggerFactory.create("HTS", device.address).also { val createdLogger = toolboxLoggerFactory.create("HTS", device.address()).also {
logger = it logger = it
} }
val manager = HTSManager(context, scope, createdLogger) val manager = HTSManager(context, scope, createdLogger)
@@ -57,9 +58,9 @@ class HTSRepository @Inject constructor(
logger?.openLogger() logger?.openLogger()
} }
private suspend fun HTSManager.start(device: BluetoothDevice) { private suspend fun HTSManager.start(device: DiscoveredBluetoothDevice) {
try { try {
connect(device) connect(device.device)
.useAutoConnect(false) .useAutoConnect(false)
.retry(3, 100) .retry(3, 100)
.suspend() .suspend()

View File

@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.service.DEVICE_DATA import no.nordicsemi.android.service.DEVICE_DATA
import no.nordicsemi.android.service.NotificationService import no.nordicsemi.android.service.NotificationService
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@@ -19,7 +20,7 @@ internal class HTSService : NotificationService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId) super.onStartCommand(intent, flags, startId)
val device = intent!!.getParcelableExtra<BluetoothDevice>(DEVICE_DATA)!! val device = intent!!.getParcelableExtra<DiscoveredBluetoothDevice>(DEVICE_DATA)!!
repository.start(device, lifecycleScope) repository.start(device, lifecycleScope)

View File

@@ -9,14 +9,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.hts.R import no.nordicsemi.android.hts.R
import no.nordicsemi.android.hts.data.HTSData
import no.nordicsemi.android.hts.viewmodel.HTSViewModel import no.nordicsemi.android.hts.viewmodel.HTSViewModel
import no.nordicsemi.android.service.* import no.nordicsemi.android.service.*
import no.nordicsemi.android.theme.view.BackIconAppBar import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.LoggerIconAppBar import no.nordicsemi.android.theme.view.LoggerIconAppBar
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.android.utils.exhaustive import no.nordicsemi.android.utils.exhaustive
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.DeviceDisconnectedView import no.nordicsemi.ui.scanner.ui.DeviceDisconnectedView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.ui.scanner.ui.Reason import no.nordicsemi.ui.scanner.ui.Reason
@Composable @Composable
@@ -27,14 +28,13 @@ fun HTSScreen() {
Column { Column {
val navigateUp = { viewModel.onEvent(NavigateUp) } val navigateUp = { viewModel.onEvent(NavigateUp) }
LoggerIconAppBar(stringResource(id = R.string.hts_title), navigateUp) { AppBar(state, navigateUp, viewModel)
viewModel.onEvent(OpenLoggerEvent)
}
Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state.htsManagerState) { when (state.htsManagerState) {
NoDeviceState -> NoDeviceView() NoDeviceState -> NoDeviceView()
is WorkingState -> when (state.htsManagerState.result) { is WorkingState -> when (state.htsManagerState.result) {
is IdleResult,
is ConnectingResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) } is ConnectingResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) }
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp) is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp)
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp) is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp)
@@ -46,3 +46,19 @@ fun HTSScreen() {
} }
} }
} }
@Composable
private fun AppBar(state: HTSViewState, navigateUp: () -> Unit, viewModel: HTSViewModel) {
val toolbarName = (state.htsManagerState as? WorkingState)?.let {
(it.result as? SuccessResult<HTSData>)?.deviceName()
}
if (toolbarName == null) {
BackIconAppBar(stringResource(id = R.string.hts_title), navigateUp)
} else {
LoggerIconAppBar(toolbarName, navigateUp, { viewModel.onEvent(DisconnectEvent) }) {
viewModel.onEvent(OpenLoggerEvent)
}
}
}

View File

@@ -48,7 +48,7 @@ internal class HTSViewModel @Inject constructor(
private fun handleArgs(args: DestinationResult) { private fun handleArgs(args: DestinationResult) {
when (args) { when (args) {
is CancelDestinationResult -> navigationManager.navigateUp() is CancelDestinationResult -> navigationManager.navigateUp()
is SuccessDestinationResult -> repository.launch(args.getDevice().device) is SuccessDestinationResult -> repository.launch(args.getDevice())
}.exhaustive }.exhaustive
} }

View File

@@ -68,7 +68,7 @@ internal class PRXManager(
val dataHolder = ConnectionObserverAdapter<PRXData>() val dataHolder = ConnectionObserverAdapter<PRXData>()
init { init {
setConnectionObserver(dataHolder) connectionObserver = dataHolder
data.onEach { data.onEach {
dataHolder.setValue(it) dataHolder.setValue(it)

View File

@@ -12,6 +12,7 @@ import no.nordicsemi.android.prx.data.PRXData
import no.nordicsemi.android.prx.data.PRXManager import no.nordicsemi.android.prx.data.PRXManager
import no.nordicsemi.android.prx.data.ProximityServerManager import no.nordicsemi.android.prx.data.ProximityServerManager
import no.nordicsemi.android.service.* import no.nordicsemi.android.service.*
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -34,13 +35,13 @@ class PRXRepository @Inject internal constructor(
val isRunning = data.map { it.isRunning() } val isRunning = data.map { it.isRunning() }
val hasBeenDisconnectedWithoutLinkLoss = data.map { it.hasBeenDisconnectedWithoutLinkLoss() } val hasBeenDisconnectedWithoutLinkLoss = data.map { it.hasBeenDisconnectedWithoutLinkLoss() }
fun launch(device: BluetoothDevice) { fun launch(device: DiscoveredBluetoothDevice) {
serviceManager.startService(PRXService::class.java, device) serviceManager.startService(PRXService::class.java, device)
proximityServerManager.open() proximityServerManager.open()
} }
fun start(device: BluetoothDevice, scope: CoroutineScope) { fun start(device: DiscoveredBluetoothDevice, scope: CoroutineScope) {
val createdLogger = toolboxLoggerFactory.create("PRX", device.address).also { val createdLogger = toolboxLoggerFactory.create("PRX", device.address()).also {
logger = it logger = it
} }
val manager = PRXManager(context, scope, createdLogger) val manager = PRXManager(context, scope, createdLogger)
@@ -52,7 +53,7 @@ class PRXRepository @Inject internal constructor(
handleLocalAlarm(it) handleLocalAlarm(it)
}.launchIn(scope) }.launchIn(scope)
manager.connect(device) manager.connect(device.device)
.useAutoConnect(true) .useAutoConnect(true)
.retry(3, 100) .retry(3, 100)
.enqueue() .enqueue()

View File

@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.service.DEVICE_DATA import no.nordicsemi.android.service.DEVICE_DATA
import no.nordicsemi.android.service.NotificationService import no.nordicsemi.android.service.NotificationService
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@@ -19,7 +20,7 @@ internal class PRXService : NotificationService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId) super.onStartCommand(intent, flags, startId)
val device = intent!!.getParcelableExtra<BluetoothDevice>(DEVICE_DATA)!! val device = intent!!.getParcelableExtra<DiscoveredBluetoothDevice>(DEVICE_DATA)!!
repository.start(device, lifecycleScope) repository.start(device, lifecycleScope)

View File

@@ -10,14 +10,15 @@ import androidx.compose.ui.Modifier
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.prx.R import no.nordicsemi.android.prx.R
import no.nordicsemi.android.prx.data.PRXData
import no.nordicsemi.android.prx.viewmodel.PRXViewModel import no.nordicsemi.android.prx.viewmodel.PRXViewModel
import no.nordicsemi.android.service.* import no.nordicsemi.android.service.*
import no.nordicsemi.android.theme.view.BackIconAppBar import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.LoggerIconAppBar import no.nordicsemi.android.theme.view.LoggerIconAppBar
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.android.utils.exhaustive import no.nordicsemi.android.utils.exhaustive
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.DeviceDisconnectedView import no.nordicsemi.ui.scanner.ui.DeviceDisconnectedView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.ui.scanner.ui.Reason import no.nordicsemi.ui.scanner.ui.Reason
@Composable @Composable
@@ -28,14 +29,13 @@ fun PRXScreen() {
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
val navigateUp = { viewModel.onEvent(NavigateUpEvent) } val navigateUp = { viewModel.onEvent(NavigateUpEvent) }
LoggerIconAppBar(stringResource(id = R.string.prx_title), navigateUp) { AppBar(state, navigateUp, viewModel)
viewModel.onEvent(OpenLoggerEvent)
}
Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) { when (state) {
NoDeviceState -> NoDeviceView() NoDeviceState -> NoDeviceView()
is WorkingState -> when (state.result) { is WorkingState -> when (state.result) {
is IdleResult,
is ConnectingResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) } is ConnectingResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) }
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp) is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp)
is LinkLossResult -> DeviceOutOfRangeView { viewModel.onEvent(DisconnectEvent) } is LinkLossResult -> DeviceOutOfRangeView { viewModel.onEvent(DisconnectEvent) }
@@ -47,3 +47,18 @@ fun PRXScreen() {
} }
} }
} }
@Composable
private fun AppBar(state: PRXViewState, navigateUp: () -> Unit, viewModel: PRXViewModel) {
val toolbarName = (state as? WorkingState)?.let {
(it.result as? SuccessResult<PRXData>)?.deviceName()
}
if (toolbarName == null) {
BackIconAppBar(stringResource(id = R.string.prx_title), navigateUp)
} else {
LoggerIconAppBar(toolbarName, navigateUp, { viewModel.onEvent(DisconnectEvent) }) {
viewModel.onEvent(OpenLoggerEvent)
}
}
}

View File

@@ -48,7 +48,7 @@ internal class PRXViewModel @Inject constructor(
private fun handleArgs(args: DestinationResult) { private fun handleArgs(args: DestinationResult) {
when (args) { when (args) {
is CancelDestinationResult -> navigationManager.navigateUp() is CancelDestinationResult -> navigationManager.navigateUp()
is SuccessDestinationResult -> repository.launch(args.getDevice().device) is SuccessDestinationResult -> repository.launch(args.getDevice())
}.exhaustive }.exhaustive
} }

View File

@@ -56,7 +56,7 @@ internal class RSCSManager internal constructor(
val dataHolder = ConnectionObserverAdapter<RSCSData>() val dataHolder = ConnectionObserverAdapter<RSCSData>()
init { init {
setConnectionObserver(dataHolder) connectionObserver = dataHolder
data.onEach { data.onEach {
dataHolder.setValue(it) dataHolder.setValue(it)

View File

@@ -14,6 +14,7 @@ import no.nordicsemi.android.rscs.data.RSCSManager
import no.nordicsemi.android.service.BleManagerResult import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.service.ConnectingResult import no.nordicsemi.android.service.ConnectingResult
import no.nordicsemi.android.service.ServiceManager import no.nordicsemi.android.service.ServiceManager
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -33,12 +34,12 @@ class RSCSRepository @Inject constructor(
val isRunning = data.map { it.isRunning() } val isRunning = data.map { it.isRunning() }
val hasBeenDisconnected = data.map { it.hasBeenDisconnected() } val hasBeenDisconnected = data.map { it.hasBeenDisconnected() }
fun launch(device: BluetoothDevice) { fun launch(device: DiscoveredBluetoothDevice) {
serviceManager.startService(RSCSService::class.java, device) serviceManager.startService(RSCSService::class.java, device)
} }
fun start(device: BluetoothDevice, scope: CoroutineScope) { fun start(device: DiscoveredBluetoothDevice, scope: CoroutineScope) {
val createdLogger = toolboxLoggerFactory.create("RSCS", device.address).also { val createdLogger = toolboxLoggerFactory.create("RSCS", device.address()).also {
logger = it logger = it
} }
val manager = RSCSManager(context, scope, createdLogger) val manager = RSCSManager(context, scope, createdLogger)
@@ -57,9 +58,9 @@ class RSCSRepository @Inject constructor(
logger?.openLogger() logger?.openLogger()
} }
private suspend fun RSCSManager.start(device: BluetoothDevice) { private suspend fun RSCSManager.start(device: DiscoveredBluetoothDevice) {
try { try {
connect(device) connect(device.device)
.useAutoConnect(false) .useAutoConnect(false)
.retry(3, 100) .retry(3, 100)
.suspend() .suspend()

View File

@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.service.DEVICE_DATA import no.nordicsemi.android.service.DEVICE_DATA
import no.nordicsemi.android.service.NotificationService import no.nordicsemi.android.service.NotificationService
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@@ -19,7 +20,7 @@ internal class RSCSService : NotificationService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId) super.onStartCommand(intent, flags, startId)
val device = intent!!.getParcelableExtra<BluetoothDevice>(DEVICE_DATA)!! val device = intent!!.getParcelableExtra<DiscoveredBluetoothDevice>(DEVICE_DATA)!!
repository.start(device, lifecycleScope) repository.start(device, lifecycleScope)

View File

@@ -9,14 +9,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.rscs.R import no.nordicsemi.android.rscs.R
import no.nordicsemi.android.rscs.data.RSCSData
import no.nordicsemi.android.rscs.viewmodel.RSCSViewModel import no.nordicsemi.android.rscs.viewmodel.RSCSViewModel
import no.nordicsemi.android.service.* import no.nordicsemi.android.service.*
import no.nordicsemi.android.theme.view.BackIconAppBar import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.LoggerIconAppBar import no.nordicsemi.android.theme.view.LoggerIconAppBar
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.android.utils.exhaustive import no.nordicsemi.android.utils.exhaustive
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.DeviceDisconnectedView import no.nordicsemi.ui.scanner.ui.DeviceDisconnectedView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.ui.scanner.ui.Reason import no.nordicsemi.ui.scanner.ui.Reason
@Composable @Composable
@@ -27,14 +28,13 @@ fun RSCSScreen() {
Column { Column {
val navigateUp = { viewModel.onEvent(NavigateUpEvent) } val navigateUp = { viewModel.onEvent(NavigateUpEvent) }
LoggerIconAppBar(stringResource(id = R.string.rscs_title), navigateUp) { AppBar(state, navigateUp, viewModel)
viewModel.onEvent(OpenLoggerEvent)
}
Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
when (state) { when (state) {
NoDeviceState -> NoDeviceView() NoDeviceState -> NoDeviceView()
is WorkingState -> when (state.result) { is WorkingState -> when (state.result) {
is IdleResult,
is ConnectingResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) } is ConnectingResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) }
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp) is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp)
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp) is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp)
@@ -46,3 +46,18 @@ fun RSCSScreen() {
} }
} }
} }
@Composable
private fun AppBar(state: RSCSViewState, navigateUp: () -> Unit, viewModel: RSCSViewModel) {
val toolbarName = (state as? WorkingState)?.let {
(it.result as? SuccessResult<RSCSData>)?.deviceName()
}
if (toolbarName == null) {
BackIconAppBar(stringResource(id = R.string.rscs_title), navigateUp)
} else {
LoggerIconAppBar(toolbarName, navigateUp, { viewModel.onEvent(DisconnectEvent) }) {
viewModel.onEvent(OpenLoggerEvent)
}
}
}

View File

@@ -48,7 +48,7 @@ internal class RSCSViewModel @Inject constructor(
private fun handleArgs(args: DestinationResult) { private fun handleArgs(args: DestinationResult) {
when (args) { when (args) {
is CancelDestinationResult -> navigationManager.navigateUp() is CancelDestinationResult -> navigationManager.navigateUp()
is SuccessDestinationResult -> repository.launch(args.getDevice().device) is SuccessDestinationResult -> repository.launch(args.getDevice())
}.exhaustive }.exhaustive
} }

View File

@@ -1,14 +1,19 @@
package no.nordicsemi.android.uart.data package no.nordicsemi.android.uart.data
internal data class UARTData( internal data class UARTData(
val messages: List<UARTOutputRecord> = emptyList(), val messages: List<UARTRecord> = emptyList(),
val batteryLevel: Int? = null, val batteryLevel: Int? = null,
) { ) {
val displayMessages = messages.reversed().take(10) val displayMessages = messages
} }
internal data class UARTOutputRecord( internal data class UARTRecord(
val text: String, val text: String,
val type: UARTRecordType,
val timestamp: Long = System.currentTimeMillis() val timestamp: Long = System.currentTimeMillis()
) )
enum class UARTRecordType {
INPUT, OUTPUT
}

View File

@@ -29,9 +29,9 @@ import android.content.Context
import android.text.TextUtils import android.text.TextUtils
import android.util.Log import android.util.Log
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext
import no.nordicsemi.android.ble.BleManager import no.nordicsemi.android.ble.BleManager
import no.nordicsemi.android.ble.WriteRequest import no.nordicsemi.android.ble.WriteRequest
import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelResponse import no.nordicsemi.android.ble.common.callback.battery.BatteryLevelResponse
@@ -49,7 +49,8 @@ private val UART_RX_CHARACTERISTIC_UUID = UUID.fromString("6E400002-B5A3-F393-E0
private val UART_TX_CHARACTERISTIC_UUID = UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E") private val UART_TX_CHARACTERISTIC_UUID = UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E")
private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb") private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb")
private val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb") private val BATTERY_LEVEL_CHARACTERISTIC_UUID =
UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb")
internal class UARTManager( internal class UARTManager(
context: Context, context: Context,
@@ -68,7 +69,7 @@ internal class UARTManager(
val dataHolder = ConnectionObserverAdapter<UARTData>() val dataHolder = ConnectionObserverAdapter<UARTData>()
init { init {
setConnectionObserver(dataHolder) connectionObserver = dataHolder
data.onEach { data.onEach {
dataHolder.setValue(it) dataHolder.setValue(it)
@@ -87,18 +88,25 @@ internal class UARTManager(
@SuppressLint("WrongConstant") @SuppressLint("WrongConstant")
override fun initialize() { override fun initialize() {
setNotificationCallback(txCharacteristic).asFlow().onEach { setNotificationCallback(txCharacteristic).asFlow()
val text: String = it.getStringValue(0) ?: String.EMPTY .flowOn(Dispatchers.IO)
log(10, "\"$text\" received") .map {
data.value = data.value.copy(messages = data.value.messages + UARTOutputRecord(text)) val text: String = it.getStringValue(0) ?: String.EMPTY
}.launchIn(scope) log(10, "\"$text\" received")
val messages = data.value.messages + UARTRecord(text, UARTRecordType.OUTPUT)
messages.takeLast(50)
}
.onEach {
data.value = data.value.copy(messages = it)
}.launchIn(scope)
requestMtu(517).enqueue() requestMtu(517).enqueue()
enableNotifications(txCharacteristic).enqueue() enableNotifications(txCharacteristic).enqueue()
setNotificationCallback(batteryLevelCharacteristic).asValidResponseFlow<BatteryLevelResponse>().onEach { setNotificationCallback(batteryLevelCharacteristic).asValidResponseFlow<BatteryLevelResponse>()
data.value = data.value.copy(batteryLevel = it.batteryLevel) .onEach {
}.launchIn(scope) data.value = data.value.copy(batteryLevel = it.batteryLevel)
}.launchIn(scope)
enableNotifications(batteryLevelCharacteristic).enqueue() enableNotifications(batteryLevelCharacteristic).enqueue()
} }
@@ -114,7 +122,8 @@ internal class UARTManager(
rxCharacteristic?.let { rxCharacteristic?.let {
val rxProperties: Int = it.properties val rxProperties: Int = it.properties
writeRequest = rxProperties and BluetoothGattCharacteristic.PROPERTY_WRITE > 0 writeRequest = rxProperties and BluetoothGattCharacteristic.PROPERTY_WRITE > 0
writeCommand = rxProperties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE > 0 writeCommand =
rxProperties and BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE > 0
// Set the WRITE REQUEST type when the characteristic supports it. // Set the WRITE REQUEST type when the characteristic supports it.
// This will allow to send long write (also if the characteristic support it). // This will allow to send long write (also if the characteristic support it).
@@ -141,20 +150,25 @@ internal class UARTManager(
@SuppressLint("WrongConstant") @SuppressLint("WrongConstant")
fun send(text: String) { fun send(text: String) {
if (rxCharacteristic == null) return if (rxCharacteristic == null) return
if (!TextUtils.isEmpty(text)) { scope.launchWithCatch {
scope.launchWithCatch { val writeType = if (useLongWrite) {
val writeType = if (useLongWrite) { BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT } else {
} else { BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
}
val request: WriteRequest = writeCharacteristic(rxCharacteristic, text.toByteArray(), writeType)
if (!useLongWrite) {
request.split()
}
request.suspend()
log(10, "\"$text\" sent")
} }
val request: WriteRequest =
writeCharacteristic(rxCharacteristic, text.toByteArray(), writeType)
if (!useLongWrite) {
request.split()
}
request.suspend()
data.value = data.value.copy(
messages = data.value.messages + UARTRecord(
text,
UARTRecordType.INPUT
)
)
log(10, "\"$text\" sent")
} }
} }

View File

@@ -10,9 +10,11 @@ import no.nordicsemi.android.ble.ktx.suspend
import no.nordicsemi.android.logger.ToolboxLogger import no.nordicsemi.android.logger.ToolboxLogger
import no.nordicsemi.android.logger.ToolboxLoggerFactory import no.nordicsemi.android.logger.ToolboxLoggerFactory
import no.nordicsemi.android.service.BleManagerResult import no.nordicsemi.android.service.BleManagerResult
import no.nordicsemi.android.service.ConnectingResult import no.nordicsemi.android.service.IdleResult
import no.nordicsemi.android.service.ServiceManager import no.nordicsemi.android.service.ServiceManager
import no.nordicsemi.android.uart.data.* import no.nordicsemi.android.uart.data.*
import no.nordicsemi.android.utils.EMPTY
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -22,12 +24,12 @@ class UARTRepository @Inject internal constructor(
private val context: Context, private val context: Context,
private val serviceManager: ServiceManager, private val serviceManager: ServiceManager,
private val configurationDataSource: ConfigurationDataSource, private val configurationDataSource: ConfigurationDataSource,
private val toolboxLoggerFactory: ToolboxLoggerFactory private val toolboxLoggerFactory: ToolboxLoggerFactory,
) { ) {
private var manager: UARTManager? = null private var manager: UARTManager? = null
private var logger: ToolboxLogger? = null private var logger: ToolboxLogger? = null
private val _data = MutableStateFlow<BleManagerResult<UARTData>>(ConnectingResult()) private val _data = MutableStateFlow<BleManagerResult<UARTData>>(IdleResult())
internal val data = _data.asStateFlow() internal val data = _data.asStateFlow()
val isRunning = data.map { it.isRunning() } val isRunning = data.map { it.isRunning() }
@@ -35,12 +37,12 @@ class UARTRepository @Inject internal constructor(
val lastConfigurationName = configurationDataSource.lastConfigurationName val lastConfigurationName = configurationDataSource.lastConfigurationName
fun launch(device: BluetoothDevice) { fun launch(device: DiscoveredBluetoothDevice) {
serviceManager.startService(UARTService::class.java, device) serviceManager.startService(UARTService::class.java, device)
} }
fun start(device: BluetoothDevice, scope: CoroutineScope) { fun start(device: DiscoveredBluetoothDevice, scope: CoroutineScope) {
val createdLogger = toolboxLoggerFactory.create("UART", device.address).also { val createdLogger = toolboxLoggerFactory.create("UART", device.address()).also {
logger = it logger = it
} }
val manager = UARTManager(context, scope, createdLogger) val manager = UARTManager(context, scope, createdLogger)
@@ -60,9 +62,8 @@ class UARTRepository @Inject internal constructor(
} }
fun runMacro(macro: UARTMacro) { fun runMacro(macro: UARTMacro) {
macro.command?.parseWithNewLineChar(macro.newLineChar)?.let { val command = macro.command?.parseWithNewLineChar(macro.newLineChar)
manager?.send(it) manager?.send(command ?: String.EMPTY)
}
} }
fun clearItems() { fun clearItems() {
@@ -77,9 +78,9 @@ class UARTRepository @Inject internal constructor(
configurationDataSource.saveConfigurationName(name) configurationDataSource.saveConfigurationName(name)
} }
private suspend fun UARTManager.start(device: BluetoothDevice) { private suspend fun UARTManager.start(device: DiscoveredBluetoothDevice) {
try { try {
connect(device) connect(device.device)
.useAutoConnect(false) .useAutoConnect(false)
.retry(3, 100) .retry(3, 100)
.suspend() .suspend()

View File

@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.service.DEVICE_DATA import no.nordicsemi.android.service.DEVICE_DATA
import no.nordicsemi.android.service.NotificationService import no.nordicsemi.android.service.NotificationService
import no.nordicsemi.ui.scanner.DiscoveredBluetoothDevice
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@@ -19,7 +20,7 @@ internal class UARTService : NotificationService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId) super.onStartCommand(intent, flags, startId)
val device = intent!!.getParcelableExtra<BluetoothDevice>(DEVICE_DATA)!! val device = intent!!.getParcelableExtra<DiscoveredBluetoothDevice>(DEVICE_DATA)!!
repository.start(device, lifecycleScope) repository.start(device, lifecycleScope)

View File

@@ -0,0 +1,101 @@
package no.nordicsemi.android.uart.view
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.material.you.RadioButtonGroup
import no.nordicsemi.android.material.you.RadioButtonItem
import no.nordicsemi.android.material.you.RadioGroupViewEntity
import no.nordicsemi.android.material.you.ScreenSection
import no.nordicsemi.android.theme.view.SectionTitle
import no.nordicsemi.android.uart.R
import no.nordicsemi.android.uart.data.MacroEol
import no.nordicsemi.android.utils.EMPTY
@Composable
internal fun InputSection(onEvent: (UARTViewEvent) -> Unit) {
val text = rememberSaveable { mutableStateOf(String.EMPTY) }
val hint = stringResource(id = R.string.uart_input_hint)
val checkedItem = rememberSaveable { mutableStateOf(MacroEol.values()[0]) }
Row(verticalAlignment = Alignment.CenterVertically) {
Box(modifier = Modifier.weight(1f)) {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.height(60.dp)
.verticalScroll(rememberScrollState()),
value = text.value,
label = { Text(hint) },
onValueChange = { newValue: String ->
text.value = newValue
}
)
}
Spacer(modifier = Modifier.size(16.dp))
Button(
onClick = {
onEvent(OnRunInput(text.value, checkedItem.value))
text.value = String.EMPTY
},
modifier = Modifier.padding(top = 6.dp)
) {
Text(text = stringResource(id = R.string.uart_send))
}
}
}
@Composable
internal fun EditInputSection(onEvent: (UARTViewEvent) -> Unit) {
val checkedItem = rememberSaveable { mutableStateOf(MacroEol.values()[0]) }
val items = MacroEol.values().map {
RadioButtonItem(it.toDisplayString(), it == checkedItem.value)
}
val viewEntity = RadioGroupViewEntity(items)
ScreenSection {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
SectionTitle(
resId = R.drawable.ic_input,
title = stringResource(R.string.uart_input),
menu = {
IconButton(onClick = { onEvent(MacroInputSwitchClick) }) {
Icon(
painterResource(id = R.drawable.ic_macro),
contentDescription = stringResource(id = R.string.uart_input_macro),
)
}
}
)
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = stringResource(id = R.string.uart_macro_dialog_eol),
style = MaterialTheme.typography.labelLarge
)
RadioButtonGroup(viewEntity) {
val i = items.indexOf(it)
checkedItem.value = MacroEol.values()[i]
}
}
Spacer(modifier = Modifier.size(16.dp))
}
}
}

View File

@@ -0,0 +1,131 @@
package no.nordicsemi.android.uart.view
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import no.nordicsemi.android.material.you.ScreenSection
import no.nordicsemi.android.theme.view.SectionTitle
import no.nordicsemi.android.uart.R
@Composable
internal fun MacroSection(viewState: UARTViewState, onEvent: (UARTViewEvent) -> Unit) {
val showAddDialog = rememberSaveable { mutableStateOf(false) }
val showDeleteDialog = rememberSaveable { mutableStateOf(false) }
if (showAddDialog.value) {
UARTAddConfigurationDialog(onEvent) { showAddDialog.value = false }
}
if (showDeleteDialog.value) {
DeleteConfigurationDialog(onEvent) { showDeleteDialog.value = false }
}
if (viewState.showEditDialog) {
UARTAddMacroDialog(viewState.selectedMacro) { onEvent(it) }
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
ScreenSection {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
SectionTitle(
resId = R.drawable.ic_macro,
title = stringResource(R.string.uart_macros),
menu = {
viewState.selectedConfiguration?.let {
if (!viewState.isConfigurationEdited) {
IconButton(onClick = { onEvent(OnEditConfiguration) }) {
Icon(
Icons.Default.Edit,
stringResource(id = R.string.uart_configuration_edit)
)
}
} else {
IconButton(onClick = { onEvent(OnEditConfiguration) }) {
Icon(
painterResource(id = R.drawable.ic_pencil_off),
stringResource(id = R.string.uart_configuration_edit)
)
}
}
IconButton(onClick = { showDeleteDialog.value = true }) {
Icon(
Icons.Default.Delete,
stringResource(id = R.string.uart_configuration_delete)
)
}
}
}
)
Spacer(modifier = Modifier.height(16.dp))
Row {
Box(modifier = Modifier.weight(1f)) {
UARTConfigurationPicker(viewState, onEvent)
}
Spacer(modifier = Modifier.size(16.dp))
Button(onClick = { showAddDialog.value = true }) {
Text(stringResource(id = R.string.uart_configuration_add))
}
}
viewState.selectedConfiguration?.let {
Spacer(modifier = Modifier.height(16.dp))
UARTMacroView(it, viewState.isConfigurationEdited, onEvent)
}
}
}
}
}
@Composable
private fun DeleteConfigurationDialog(onEvent: (UARTViewEvent) -> Unit, onDismiss: () -> Unit) {
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
text = stringResource(id = R.string.uart_delete_dialog_title),
style = MaterialTheme.typography.headlineSmall
)
},
text = {
Text(text = stringResource(id = R.string.uart_delete_dialog_info))
},
confirmButton = {
TextButton(onClick = {
onDismiss()
onEvent(OnDeleteConfiguration)
}) {
Text(text = stringResource(id = R.string.uart_delete_dialog_confirm))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(text = stringResource(id = R.string.uart_delete_dialog_cancel))
}
}
)
}

View File

@@ -0,0 +1,162 @@
package no.nordicsemi.android.uart.view
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import no.nordicsemi.android.theme.view.SectionTitle
import no.nordicsemi.android.uart.R
import no.nordicsemi.android.uart.data.UARTRecord
import no.nordicsemi.android.uart.data.UARTRecordType
import java.text.SimpleDateFormat
import java.util.*
@Composable
internal fun OutputSection(records: List<UARTRecord>, onEvent: (UARTViewEvent) -> Unit) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
SectionTitle(
resId = R.drawable.ic_output,
title = stringResource(R.string.uart_output),
modifier = Modifier,
menu = { Menu(onEvent) }
)
}
Spacer(modifier = Modifier.size(16.dp))
val scrollState = rememberLazyListState()
val scrollDown = remember {
derivedStateOf { scrollState.isScrolledToTheEnd() }
}
LazyColumn(
modifier = Modifier.fillMaxWidth(),
state = scrollState
) {
if (records.isEmpty()) {
item { Text(text = stringResource(id = R.string.uart_output_placeholder)) }
} else {
records.forEach {
item {
when (it.type) {
UARTRecordType.INPUT -> MessageItemInput(record = it)
UARTRecordType.OUTPUT -> MessageItemOutput(record = it)
}
Spacer(modifier = Modifier.height(16.dp))
}
}
}
}
LaunchedEffect(records, scrollDown.value) {
if (!scrollDown.value || records.isEmpty()) {
return@LaunchedEffect
}
launch {
scrollState.scrollToItem(records.lastIndex)
}
}
}
}
fun LazyListState.isScrolledToTheEnd() = layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1
@Composable
private fun MessageItemInput(record: UARTRecord) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.End
) {
Text(
text = record.timeToString(),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(4.dp))
Column(
modifier = Modifier
.clip(RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp, bottomStart = 10.dp))
.background(MaterialTheme.colorScheme.secondary)
.padding(8.dp),
horizontalAlignment = Alignment.End
) {
Text(
text = record.text,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSecondary
)
}
}
}
@Composable
private fun MessageItemOutput(record: UARTRecord) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start
) {
Text(
text = record.timeToString(),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.height(4.dp))
Column(
modifier = Modifier
.clip(RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp, bottomEnd = 10.dp))
.background(MaterialTheme.colorScheme.primary)
.padding(8.dp)
) {
Text(
text = record.text,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onPrimary
)
}
}
}
@Composable
private fun Menu(onEvent: (UARTViewEvent) -> Unit) {
Row {
IconButton(onClick = { onEvent(ClearOutputItems) }) {
Icon(
Icons.Default.Delete,
contentDescription = stringResource(id = R.string.uart_clear_items),
)
}
}
}
private val datFormatter = SimpleDateFormat("dd MMMM yyyy, HH:mm:ss", Locale.ENGLISH)
private fun UARTRecord.timeToString(): String {
return datFormatter.format(timestamp)
}

View File

@@ -1,21 +1,12 @@
package no.nordicsemi.android.uart.view package no.nordicsemi.android.uart.view
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import no.nordicsemi.android.material.you.TextField
import no.nordicsemi.android.uart.R import no.nordicsemi.android.uart.R
import no.nordicsemi.android.utils.EMPTY import no.nordicsemi.android.utils.EMPTY
@@ -24,47 +15,28 @@ internal fun UARTAddConfigurationDialog(onEvent: (UARTViewEvent) -> Unit, onDism
val name = rememberSaveable { mutableStateOf(String.EMPTY) } val name = rememberSaveable { mutableStateOf(String.EMPTY) }
val isError = rememberSaveable { mutableStateOf(false) } val isError = rememberSaveable { mutableStateOf(false) }
Dialog(onDismissRequest = { onDismiss() }) { AlertDialog(
Surface( onDismissRequest = { onDismiss() },
color = MaterialTheme.colorScheme.background, title = { Text(stringResource(id = R.string.uart_configuration_dialog_title)) },
shape = RoundedCornerShape(10.dp), text = { NameInput(name, isError) },
shadowElevation = 2.dp, confirmButton = {
) { TextButton(onClick = {
Column(verticalArrangement = Arrangement.SpaceBetween) { if (isNameValid(name.value)) {
Text( onDismiss()
text = stringResource(id = R.string.uart_configuration_dialog_title), onEvent(OnAddConfiguration(name.value))
style = MaterialTheme.typography.headlineSmall, } else {
modifier = Modifier isError.value = true
.fillMaxWidth()
.padding(16.dp)
)
NameInput(name, isError)
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = { onDismiss() }) {
Text(stringResource(id = R.string.uart_macro_dialog_dismiss))
}
Spacer(modifier = Modifier.size(16.dp))
TextButton(onClick = {
if (isNameValid(name.value)) {
onDismiss()
onEvent(OnAddConfiguration(name.value))
} else {
isError.value = true
}
}) {
Text(stringResource(id = R.string.uart_macro_dialog_confirm))
}
} }
}) {
Text(stringResource(id = R.string.uart_macro_dialog_confirm))
}
},
dismissButton = {
TextButton(onClick = { onDismiss() }) {
Text(stringResource(id = R.string.uart_macro_dialog_dismiss))
} }
} }
} )
} }
@Composable @Composable
@@ -72,14 +44,17 @@ private fun NameInput(
name: MutableState<String>, name: MutableState<String>,
isError: MutableState<Boolean> isError: MutableState<Boolean>
) { ) {
Column(modifier = Modifier.padding(16.dp)) { Column {
TextField(
text = name.value, OutlinedTextField(
hint = stringResource(id = R.string.uart_configuration_hint) value = name.value,
) { label = { Text(stringResource(id = R.string.uart_configuration_hint)) },
isError.value = false singleLine = true,
name.value = it onValueChange = {
} isError.value = false
name.value = it
}
)
val errorText = if (isError.value) { val errorText = if (isError.value) {
stringResource(id = R.string.uart_name_empty) stringResource(id = R.string.uart_name_empty)

View File

@@ -1,18 +1,12 @@
package no.nordicsemi.android.uart.view package no.nordicsemi.android.uart.view
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.*
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.GridItemSpan import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.*
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -25,11 +19,9 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import no.nordicsemi.android.material.you.RadioButtonGroup import no.nordicsemi.android.material.you.RadioButtonGroup
import no.nordicsemi.android.material.you.RadioButtonItem import no.nordicsemi.android.material.you.RadioButtonItem
import no.nordicsemi.android.material.you.RadioGroupViewEntity import no.nordicsemi.android.material.you.RadioGroupViewEntity
import no.nordicsemi.android.material.you.TextField
import no.nordicsemi.android.uart.R import no.nordicsemi.android.uart.R
import no.nordicsemi.android.uart.data.MacroEol import no.nordicsemi.android.uart.data.MacroEol
import no.nordicsemi.android.uart.data.MacroIcon import no.nordicsemi.android.uart.data.MacroIcon
@@ -38,130 +30,85 @@ import no.nordicsemi.android.utils.EMPTY
private const val GRID_SIZE = 5 private const val GRID_SIZE = 5
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
internal fun UARTAddMacroDialog(macro: UARTMacro?, onEvent: (UARTViewEvent) -> Unit) { internal fun UARTAddMacroDialog(macro: UARTMacro?, onEvent: (UARTViewEvent) -> Unit) {
val newLineChar = rememberSaveable { mutableStateOf(macro?.newLineChar ?: MacroEol.LF) } val newLineChar = rememberSaveable { mutableStateOf(macro?.newLineChar ?: MacroEol.LF) }
val command = rememberSaveable { mutableStateOf(macro?.command ?: String.EMPTY) } val command = rememberSaveable { mutableStateOf(macro?.command ?: String.EMPTY) }
val isError = rememberSaveable { mutableStateOf(false) }
val selectedIcon = rememberSaveable { mutableStateOf(macro?.icon ?: MacroIcon.values()[0]) } val selectedIcon = rememberSaveable { mutableStateOf(macro?.icon ?: MacroIcon.values()[0]) }
Dialog(onDismissRequest = { onEvent(OnEditFinish) }) { AlertDialog(
Surface( onDismissRequest = { onEvent(OnEditFinish) },
color = MaterialTheme.colorScheme.background, dismissButton = {
shape = RoundedCornerShape(10.dp), TextButton(onClick = { onEvent(OnDeleteMacro) }) {
shadowElevation = 0.dp, Text(stringResource(id = R.string.uart_macro_dialog_delete))
) { }
Column { },
Text( confirmButton = {
text = stringResource(id = R.string.uart_macro_dialog_title), TextButton(onClick = {
style = MaterialTheme.typography.headlineSmall, onEvent(OnCreateMacro(UARTMacro(selectedIcon.value, command.value, newLineChar.value)))
modifier = Modifier }) {
.fillMaxWidth() Text(stringResource(id = R.string.uart_macro_dialog_confirm))
.padding(16.dp) }
) },
title = {
Text(
text = stringResource(id = R.string.uart_macro_dialog_title),
style = MaterialTheme.typography.headlineSmall
)
},
text = {
LazyVerticalGrid(
columns = GridCells.Fixed(GRID_SIZE),
modifier = Modifier.wrapContentHeight()
) {
item(span = { GridItemSpan(GRID_SIZE) }) {
Column {
NewLineCharSection(newLineChar.value) { newLineChar.value = it }
LazyVerticalGrid( Spacer(modifier = Modifier.size(16.dp))
cells = GridCells.Fixed(GRID_SIZE), }
modifier = Modifier }
.padding(horizontal = 16.dp)
.wrapContentHeight()
) {
item(span = { GridItemSpan(GRID_SIZE) }) {
Column {
NewLineCharSection(newLineChar.value) { newLineChar.value = it }
Spacer(modifier = Modifier.size(16.dp)) item(span = { GridItemSpan(GRID_SIZE) }) {
} CommandInput(command)
}
items(20) { item ->
val icon = MacroIcon.create(item)
val background = if (selectedIcon.value == icon) {
MaterialTheme.colorScheme.primaryContainer
} else {
Color.Transparent
} }
item(span = { GridItemSpan(GRID_SIZE) }) { Image(
CommandInput(command, isError) painter = painterResource(id = icon.toResId()),
} contentDescription = stringResource(id = R.string.uart_macro_icon),
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer),
items(20) { item -> modifier = Modifier
val icon = MacroIcon.create(item) .size(40.dp)
val background = if (selectedIcon.value == icon) { .clip(RoundedCornerShape(10.dp))
MaterialTheme.colorScheme.primaryContainer .clickable { selectedIcon.value = icon }
} else { .background(background)
Color.Transparent )
}
Image(
painter = painterResource(id = icon.toResId()),
contentDescription = stringResource(id = R.string.uart_macro_icon),
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer),
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(10.dp))
.clickable { selectedIcon.value = icon }
.background(background)
)
}
item(span = { GridItemSpan(GRID_SIZE) }) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = { onEvent(OnEditFinish) }) {
Text(stringResource(id = R.string.uart_macro_dialog_dismiss))
}
Spacer(modifier = Modifier.size(16.dp))
TextButton(onClick = { onEvent(OnDeleteMacro) }) {
Text(stringResource(id = R.string.uart_macro_dialog_delete))
}
Spacer(modifier = Modifier.size(16.dp))
TextButton(onClick = {
if (isCommandValid(command.value)) {
onEvent(
OnCreateMacro(
UARTMacro(
selectedIcon.value,
command.value,
newLineChar.value
)
)
)
} else {
isError.value = true
}
}) {
Text(stringResource(id = R.string.uart_macro_dialog_confirm))
}
}
}
} }
} }
} }
} )
} }
@Composable @Composable
private fun CommandInput( private fun CommandInput(command: MutableState<String>) {
command: MutableState<String>,
isError: MutableState<Boolean>
) {
Column { Column {
TextField( OutlinedTextField(
text = command.value, modifier = Modifier
hint = stringResource(id = R.string.uart_macro_dialog_command) .fillMaxWidth(),
) { value = command.value,
isError.value = false label = { Text(stringResource(id = R.string.uart_macro_dialog_command)) },
command.value = it onValueChange = {
} command.value = it
}
if (isError.value) { )
Text(
text = stringResource(id = R.string.uart_macro_error),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.error
)
}
Spacer(modifier = Modifier.size(16.dp)) Spacer(modifier = Modifier.size(16.dp))
} }
@@ -186,7 +133,3 @@ private fun NewLineCharSection(checkedItem: MacroEol, onItemClick: (MacroEol) ->
} }
} }
} }
private fun isCommandValid(command: String): Boolean {
return command.isNotBlank()
}

View File

@@ -1,265 +1,40 @@
package no.nordicsemi.android.uart.view package no.nordicsemi.android.uart.view
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import no.nordicsemi.android.material.you.* import no.nordicsemi.android.material.you.Card
import no.nordicsemi.android.theme.view.SectionTitle
import no.nordicsemi.android.uart.R
import no.nordicsemi.android.uart.data.MacroEol
import no.nordicsemi.android.uart.data.UARTData import no.nordicsemi.android.uart.data.UARTData
import no.nordicsemi.android.uart.data.UARTOutputRecord
import no.nordicsemi.android.utils.EMPTY
import java.text.SimpleDateFormat
import java.util.*
@Composable @Composable
internal fun UARTContentView( internal fun UARTContentView(
state: UARTData, state: UARTData,
viewState: UARTViewState,
onEvent: (UARTViewEvent) -> Unit onEvent: (UARTViewEvent) -> Unit
) { ) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(16.dp) modifier = Modifier
.padding(16.dp)
.fillMaxSize()
) { ) {
InputSection(onEvent = onEvent)
Spacer(modifier = Modifier.size(16.dp)) Card(
modifier = Modifier
MacroSection(viewState, onEvent) .weight(1f)
Spacer(modifier = Modifier.size(16.dp))
OutputSection(state.displayMessages, onEvent)
Spacer(modifier = Modifier.size(16.dp))
Button(
onClick = { onEvent(DisconnectEvent) }
) { ) {
Text(text = stringResource(id = R.string.disconnect)) Column(
} modifier = Modifier
} .fillMaxWidth()
} .padding(start = 16.dp, top = 16.dp, end = 16.dp)
@Composable
private fun InputSection(onEvent: (UARTViewEvent) -> Unit) {
val text = rememberSaveable { mutableStateOf(String.EMPTY) }
val hint = stringResource(id = R.string.uart_input_hint)
val checkedItem = rememberSaveable { mutableStateOf(MacroEol.values()[0]) }
val items = MacroEol.values().map {
RadioButtonItem(it.toDisplayString(), it == checkedItem.value)
}
val viewEntity = RadioGroupViewEntity(items)
ScreenSection {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
SectionTitle(resId = R.drawable.ic_input, title = stringResource(R.string.uart_input))
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = stringResource(id = R.string.uart_macro_dialog_eol),
style = MaterialTheme.typography.labelLarge
)
RadioButtonGroup(viewEntity) {
val i = items.indexOf(it)
checkedItem.value = MacroEol.values()[i]
}
}
Spacer(modifier = Modifier.size(16.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Box(modifier = Modifier.weight(1f)) {
TextField(text = text.value, hint = hint) {
text.value = it
}
}
Spacer(modifier = Modifier.size(16.dp))
Button(
onClick = { onEvent(OnRunInput(text.value, checkedItem.value)) },
modifier = Modifier.padding(top = 6.dp)
) {
Text(text = stringResource(id = R.string.uart_send))
}
}
}
}
}
@Composable
private fun MacroSection(viewState: UARTViewState, onEvent: (UARTViewEvent) -> Unit) {
val showAddDialog = rememberSaveable { mutableStateOf(false) }
val showDeleteDialog = rememberSaveable { mutableStateOf(false) }
if (showAddDialog.value) {
UARTAddConfigurationDialog(onEvent) { showAddDialog.value = false }
}
if (showDeleteDialog.value) {
DeleteConfigurationDialog(onEvent) { showDeleteDialog.value = false }
}
ScreenSection {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
SectionTitle(resId = R.drawable.ic_input, title = stringResource(R.string.uart_macros))
Spacer(modifier = Modifier.height(16.dp))
Row {
Box(modifier = Modifier.weight(1f)) {
UARTConfigurationPicker(viewState, onEvent)
}
IconButton(onClick = { showAddDialog.value = true }) {
Icon(Icons.Default.Add, stringResource(id = R.string.uart_configuration_add))
}
viewState.selectedConfiguration?.let {
if (!viewState.isConfigurationEdited) {
IconButton(onClick = { onEvent(OnEditConfiguration) }) {
Icon(
Icons.Default.Edit,
stringResource(id = R.string.uart_configuration_edit)
)
}
} else {
IconButton(onClick = { onEvent(OnEditConfiguration) }) {
Icon(
painterResource(id = R.drawable.ic_pencil_off),
stringResource(id = R.string.uart_configuration_edit)
)
}
}
IconButton(onClick = { showDeleteDialog.value = true }) {
Icon(
Icons.Default.Delete,
stringResource(id = R.string.uart_configuration_delete)
)
}
}
}
viewState.selectedConfiguration?.let {
Spacer(modifier = Modifier.height(16.dp))
UARTMacroView(it, viewState.isConfigurationEdited, onEvent)
}
}
}
}
@Composable
private fun DeleteConfigurationDialog(onEvent: (UARTViewEvent) -> Unit, onDismiss: () -> Unit) {
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
text = stringResource(id = R.string.uart_delete_dialog_title),
style = MaterialTheme.typography.headlineSmall
)
},
text = {
Text(text = stringResource(id = R.string.uart_delete_dialog_info))
},
confirmButton = {
Button(onClick = {
onDismiss()
onEvent(OnDeleteConfiguration)
}) {
Text(text = stringResource(id = R.string.uart_delete_dialog_confirm))
}
},
dismissButton = {
Button(onClick = onDismiss) {
Text(text = stringResource(id = R.string.uart_delete_dialog_cancel))
}
}
)
}
@Composable
private fun OutputSection(records: List<UARTOutputRecord>, onEvent: (UARTViewEvent) -> Unit) {
ScreenSection {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) { ) {
SectionTitle( OutputSection(state.displayMessages, onEvent)
resId = R.drawable.ic_output,
title = stringResource(R.string.uart_output),
modifier = Modifier
)
IconButton(onClick = { onEvent(ClearOutputItems) }) {
Icon(
Icons.Default.Delete,
contentDescription = "Clear items.",
)
}
}
Spacer(modifier = Modifier.size(16.dp))
Column(modifier = Modifier.fillMaxWidth()) {
if (records.isEmpty()) {
Text(text = stringResource(id = R.string.uart_output_placeholder))
} else {
records.forEach {
MessageItem(record = it)
Spacer(modifier = Modifier.height(16.dp))
}
}
} }
} }
Spacer(modifier = Modifier.size(16.dp))
InputSection(onEvent = onEvent)
} }
} }
@Composable
private fun MessageItem(record: UARTOutputRecord) {
Column {
Text(
text = record.timeToString(),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.outline
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = record.text,
style = MaterialTheme.typography.bodyMedium
)
}
}
private val datFormatter = SimpleDateFormat("dd MMMM yyyy, HH:mm:ss", Locale.ENGLISH)
private fun UARTOutputRecord.timeToString(): String {
return datFormatter.format(timestamp)
}

View File

@@ -13,13 +13,13 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import no.nordicsemi.android.uart.R import no.nordicsemi.android.uart.R
import no.nordicsemi.android.uart.data.UARTConfiguration import no.nordicsemi.android.uart.data.UARTConfiguration
import no.nordicsemi.android.uart.data.UARTMacro import no.nordicsemi.android.uart.data.UARTMacro
private val divider = 4.dp private val divider = 4.dp
private val buttonSize = 80.dp
@Composable @Composable
internal fun UARTMacroView( internal fun UARTMacroView(
@@ -27,34 +27,42 @@ internal fun UARTMacroView(
isEdited: Boolean, isEdited: Boolean,
onEvent: (UARTViewEvent) -> Unit onEvent: (UARTViewEvent) -> Unit
) { ) {
Column(modifier = Modifier.padding(horizontal = 16.dp)) { BoxWithConstraints {
val buttonSize = if (maxWidth < 260.dp) {
Row { 48.dp //Minimum touch area
Item(configuration, isEdited, 0, onEvent) } else {
Spacer(modifier = Modifier.size(divider)) 80.dp
Item(configuration, isEdited, 1, onEvent)
Spacer(modifier = Modifier.size(divider))
Item(configuration, isEdited, 2, onEvent)
} }
Spacer(modifier = Modifier.size(divider)) Column(modifier = Modifier.padding(horizontal = 16.dp)) {
Row { Row {
Item(configuration, isEdited, 3, onEvent) Item(configuration, isEdited, 0, buttonSize, onEvent)
Spacer(modifier = Modifier.size(divider)) Spacer(modifier = Modifier.size(divider))
Item(configuration, isEdited, 4, onEvent) Item(configuration, isEdited, 1, buttonSize, onEvent)
Spacer(modifier = Modifier.size(divider)) Spacer(modifier = Modifier.size(divider))
Item(configuration, isEdited, 5, onEvent) Item(configuration, isEdited, 2, buttonSize, onEvent)
} }
Spacer(modifier = Modifier.size(divider)) Spacer(modifier = Modifier.size(divider))
Row {
Item(configuration, isEdited, 3, buttonSize, onEvent)
Spacer(modifier = Modifier.size(divider))
Item(configuration, isEdited, 4, buttonSize, onEvent)
Spacer(modifier = Modifier.size(divider))
Item(configuration, isEdited, 5, buttonSize, onEvent)
}
Row {
Item(configuration, isEdited, 6, onEvent)
Spacer(modifier = Modifier.size(divider)) Spacer(modifier = Modifier.size(divider))
Item(configuration, isEdited, 7, onEvent)
Spacer(modifier = Modifier.size(divider)) Row {
Item(configuration, isEdited, 8, onEvent) Item(configuration, isEdited, 6, buttonSize, onEvent)
Spacer(modifier = Modifier.size(divider))
Item(configuration, isEdited, 7, buttonSize, onEvent)
Spacer(modifier = Modifier.size(divider))
Item(configuration, isEdited, 8, buttonSize, onEvent)
}
} }
} }
} }
@@ -64,14 +72,15 @@ private fun Item(
configuration: UARTConfiguration, configuration: UARTConfiguration,
isEdited: Boolean, isEdited: Boolean,
position: Int, position: Int,
buttonSize: Dp,
onEvent: (UARTViewEvent) -> Unit onEvent: (UARTViewEvent) -> Unit
) { ) {
val macro = configuration.macros.getOrNull(position) val macro = configuration.macros.getOrNull(position)
if (macro == null) { if (macro == null) {
EmptyButton(isEdited, position, onEvent) EmptyButton(isEdited, position, buttonSize, onEvent)
} else { } else {
MacroButton(macro, position, isEdited, onEvent) MacroButton(macro, position, isEdited, buttonSize, onEvent)
} }
} }
@@ -80,6 +89,7 @@ private fun MacroButton(
macro: UARTMacro, macro: UARTMacro,
position: Int, position: Int,
isEdited: Boolean, isEdited: Boolean,
buttonSize: Dp,
onEvent: (UARTViewEvent) -> Unit onEvent: (UARTViewEvent) -> Unit
) { ) {
Image( Image(
@@ -104,6 +114,7 @@ private fun MacroButton(
private fun EmptyButton( private fun EmptyButton(
isEdited: Boolean, isEdited: Boolean,
position: Int, position: Int,
buttonSize: Dp,
onEvent: (UARTViewEvent) -> Unit onEvent: (UARTViewEvent) -> Unit
) { ) {
Box( Box(

View File

@@ -8,15 +8,19 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
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.material.you.PagerView
import no.nordicsemi.android.material.you.PagerViewEntity
import no.nordicsemi.android.material.you.PagerViewItem
import no.nordicsemi.android.service.* import no.nordicsemi.android.service.*
import no.nordicsemi.android.theme.view.BackIconAppBar import no.nordicsemi.android.theme.view.BackIconAppBar
import no.nordicsemi.android.theme.view.LoggerIconAppBar import no.nordicsemi.android.theme.view.LoggerIconAppBar
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.android.uart.R import no.nordicsemi.android.uart.R
import no.nordicsemi.android.uart.data.UARTData
import no.nordicsemi.android.uart.viewmodel.UARTViewModel import no.nordicsemi.android.uart.viewmodel.UARTViewModel
import no.nordicsemi.android.utils.exhaustive import no.nordicsemi.android.utils.exhaustive
import no.nordicsemi.ui.scanner.ui.DeviceConnectingView
import no.nordicsemi.ui.scanner.ui.DeviceDisconnectedView import no.nordicsemi.ui.scanner.ui.DeviceDisconnectedView
import no.nordicsemi.ui.scanner.ui.NoDeviceView
import no.nordicsemi.ui.scanner.ui.Reason import no.nordicsemi.ui.scanner.ui.Reason
@Composable @Composable
@@ -24,29 +28,59 @@ fun UARTScreen() {
val viewModel: UARTViewModel = hiltViewModel() val viewModel: UARTViewModel = hiltViewModel()
val state = viewModel.state.collectAsState().value val state = viewModel.state.collectAsState().value
if (state.showEditDialog) {
UARTAddMacroDialog(state.selectedMacro) { viewModel.onEvent(it) }
}
Column { Column {
val navigateUp = { viewModel.onEvent(NavigateUp) } val navigateUp = { viewModel.onEvent(NavigateUp) }
LoggerIconAppBar(stringResource(id = R.string.uart_title), navigateUp) { AppBar(state = state, navigateUp = navigateUp) { viewModel.onEvent(it) }
viewModel.onEvent(OpenLogger)
}
Column(modifier = Modifier.verticalScroll(rememberScrollState())) { when (state.uartManagerState) {
when (state.uartManagerState) { NoDeviceState -> NoDeviceView()
NoDeviceState -> NoDeviceView() is WorkingState -> when (state.uartManagerState.result) {
is WorkingState -> when (state.uartManagerState.result) { is IdleResult,
is ConnectingResult -> DeviceConnectingView { viewModel.onEvent(DisconnectEvent) } is ConnectingResult -> Scroll { DeviceConnectingView { viewModel.onEvent(DisconnectEvent) } }
is DisconnectedResult -> DeviceDisconnectedView(Reason.USER, navigateUp) is DisconnectedResult -> Scroll { DeviceDisconnectedView(Reason.USER, navigateUp) }
is LinkLossResult -> DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp) is LinkLossResult -> Scroll { DeviceDisconnectedView(Reason.LINK_LOSS, navigateUp) }
is MissingServiceResult -> DeviceDisconnectedView(Reason.MISSING_SERVICE, navigateUp) is MissingServiceResult -> Scroll { DeviceDisconnectedView(Reason.MISSING_SERVICE, navigateUp) }
is UnknownErrorResult -> DeviceDisconnectedView(Reason.UNKNOWN, navigateUp) is UnknownErrorResult -> Scroll { DeviceDisconnectedView(Reason.UNKNOWN, navigateUp) }
is SuccessResult -> UARTContentView(state.uartManagerState.result.data, state) { viewModel.onEvent(it) } is SuccessResult -> SuccessScreen(state.uartManagerState.result.data, state, viewModel)
} }
}.exhaustive }.exhaustive
}
}
@Composable
private fun AppBar(state: UARTViewState, navigateUp: () -> Unit, onEvent: (UARTViewEvent) -> Unit) {
val toolbarName = (state.uartManagerState as? WorkingState)?.let {
(it.result as? SuccessResult<UARTData>)?.deviceName()
}
if (toolbarName == null) {
BackIconAppBar(stringResource(id = R.string.uart_title), navigateUp)
} else {
LoggerIconAppBar(toolbarName, navigateUp, { onEvent(DisconnectEvent) }) {
onEvent(OpenLogger)
} }
} }
} }
@Composable
private fun SuccessScreen(data: UARTData, state: UARTViewState, viewModel: UARTViewModel) {
val viewEntity = PagerViewEntity(
listOf(
PagerViewItem(stringResource(id = R.string.uart_input)) {
UARTContentView(data) { viewModel.onEvent(it) }
},
PagerViewItem(stringResource(id = R.string.uart_macros)) {
MacroSection(state) { viewModel.onEvent(it) }
}
)
)
PagerView(viewEntity)
}
@Composable
fun Scroll(content: @Composable () -> Unit) {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
content()
}
}

View File

@@ -10,7 +10,8 @@ internal data class UARTViewState(
val selectedConfigurationName: String? = null, val selectedConfigurationName: String? = null,
val isConfigurationEdited: Boolean = false, val isConfigurationEdited: Boolean = false,
val configurations: List<UARTConfiguration> = emptyList(), val configurations: List<UARTConfiguration> = emptyList(),
val uartManagerState: HTSManagerState = NoDeviceState val uartManagerState: HTSManagerState = NoDeviceState,
val isInputVisible: Boolean = true
) { ) {
val showEditDialog: Boolean = editedPosition != null val showEditDialog: Boolean = editedPosition != null
@@ -25,6 +26,8 @@ internal data class UARTViewState(
internal sealed class HTSManagerState internal sealed class HTSManagerState
internal data class WorkingState(val result: BleManagerResult<UARTData>) : HTSManagerState() internal data class WorkingState(
val result: BleManagerResult<UARTData>
) : HTSManagerState()
internal object NoDeviceState : HTSManagerState() internal object NoDeviceState : HTSManagerState()

View File

@@ -23,3 +23,5 @@ internal object DisconnectEvent : UARTViewEvent()
internal object NavigateUp : UARTViewEvent() internal object NavigateUp : UARTViewEvent()
internal object OpenLogger : UARTViewEvent() internal object OpenLogger : UARTViewEvent()
internal object MacroInputSwitchClick : UARTViewEvent()

View File

@@ -7,6 +7,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import no.nordicsemi.android.navigation.* import no.nordicsemi.android.navigation.*
import no.nordicsemi.android.service.IdleResult
import no.nordicsemi.android.uart.data.UARTConfiguration import no.nordicsemi.android.uart.data.UARTConfiguration
import no.nordicsemi.android.uart.data.UARTMacro import no.nordicsemi.android.uart.data.UARTMacro
import no.nordicsemi.android.uart.data.UARTPersistentDataSource import no.nordicsemi.android.uart.data.UARTPersistentDataSource
@@ -36,6 +37,9 @@ internal class UARTViewModel @Inject constructor(
} }
repository.data.onEach { repository.data.onEach {
if (it is IdleResult) {
return@onEach
}
_state.value = _state.value.copy(uartManagerState = WorkingState(it)) _state.value = _state.value.copy(uartManagerState = WorkingState(it))
}.launchIn(viewModelScope) }.launchIn(viewModelScope)
@@ -63,7 +67,7 @@ internal class UARTViewModel @Inject constructor(
private fun handleArgs(args: DestinationResult) { private fun handleArgs(args: DestinationResult) {
when (args) { when (args) {
is CancelDestinationResult -> navigationManager.navigateUp() is CancelDestinationResult -> navigationManager.navigateUp()
is SuccessDestinationResult -> repository.launch(args.getDevice().device) is SuccessDestinationResult -> repository.launch(args.getDevice())
}.exhaustive }.exhaustive
} }
@@ -83,9 +87,14 @@ internal class UARTViewModel @Inject constructor(
ClearOutputItems -> repository.clearItems() ClearOutputItems -> repository.clearItems()
OpenLogger -> repository.openLogger() OpenLogger -> repository.openLogger()
is OnRunInput -> repository.sendText(event.text, event.newLineChar) is OnRunInput -> repository.sendText(event.text, event.newLineChar)
MacroInputSwitchClick -> onMacroInputSwitch()
}.exhaustive }.exhaustive
} }
private fun onMacroInputSwitch() {
_state.value = _state.value.copy(isInputVisible = !state.value.isInputVisible)
}
private fun onEditConfiguration() { private fun onEditConfiguration() {
val isEdited = _state.value.isConfigurationEdited val isEdited = _state.value.isConfigurationEdited
_state.value = _state.value.copy(isConfigurationEdited = !isEdited) _state.value = _state.value.copy(isConfigurationEdited = !isEdited)

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M4,8h4L8,4L4,4v4zM10,20h4v-4h-4v4zM4,20h4v-4L4,16v4zM4,14h4v-4L4,10v4zM10,14h4v-4h-4v4zM16,4v4h4L20,4h-4zM10,8h4L14,4h-4v4zM16,14h4v-4h-4v4zM16,20h4v-4h-4v4z" />
</vector>

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3.8,12.18c-0.2,-0.86 -0.3,-1.76 -0.3,-2.68c0,-2.84 0.99,-5.45 2.63,-7.5L7.2,3.07C5.82,4.85 5,7.08 5,9.5c0,0.88 0.11,1.74 0.32,2.56l1.62,-1.62L8,11.5L4.5,15L1,11.5l1.06,-1.06L3.8,12.18zM13.85,11.62l-2.68,-5.37c-0.37,-0.74 -1.27,-1.04 -2.01,-0.67C8.41,5.96 8.11,6.86 8.48,7.6l4.81,9.6L10.05,18c-0.33,0.09 -0.59,0.33 -0.7,0.66L9,19.78l6.19,2.25c0.5,0.17 1.28,0.02 1.75,-0.22l5.51,-2.75c0.89,-0.45 1.32,-1.48 1,-2.42l-1.43,-4.27c-0.27,-0.82 -1.04,-1.37 -1.9,-1.37h-4.56c-0.31,0 -0.62,0.07 -0.89,0.21L13.85,11.62" />
</vector>

View File

@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3.8,12.18c-0.2,-0.86 -0.3,-1.76 -0.3,-2.68c0,-2.84 0.99,-5.45 2.63,-7.5L7.2,3.07C5.82,4.85 5,7.08 5,9.5c0,0.88 0.11,1.74 0.32,2.56l1.62,-1.62L8,11.5L4.5,15L1,11.5l1.06,-1.06L3.8,12.18zM13.85,11.62l-2.68,-5.37c-0.37,-0.74 -1.27,-1.04 -2.01,-0.67C8.41,5.96 8.11,6.86 8.48,7.6l4.81,9.6L10.05,18c-0.33,0.09 -0.59,0.33 -0.7,0.66L9,19.78l6.19,2.25c0.5,0.17 1.28,0.02 1.75,-0.22l5.51,-2.75c0.89,-0.45 1.32,-1.48 1,-2.42l-1.43,-4.27c-0.27,-0.82 -1.04,-1.37 -1.9,-1.37h-4.56c-0.31,0 -0.62,0.07 -0.89,0.21L13.85,11.62" />
<path
android:fillColor="@android:color/white"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
</vector>

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="uart_title">UART</string> <string name="uart_title">UART</string>
<string name="uart_no_macros_info">Please define a macro to send command to the device.</string> <string name="uart_no_macros_info">Please define a macro to send command to the device.</string>
<string name="uart_configuration_add">Add selected configuration.</string> <string name="uart_configuration_add">Add</string>
<string name="uart_configuration_delete">Delete selected configuration.</string> <string name="uart_configuration_delete">Delete selected configuration.</string>
<string name="uart_configuration_edit">Edit selected configuration.</string> <string name="uart_configuration_edit">Edit selected configuration.</string>
@@ -52,4 +52,13 @@
<string name="uart_delete_dialog_info">Are you sure that you want to delete this configuration? Your data will be irretrievably lost.</string> <string name="uart_delete_dialog_info">Are you sure that you want to delete this configuration? Your data will be irretrievably lost.</string>
<string name="uart_delete_dialog_confirm">Confirm</string> <string name="uart_delete_dialog_confirm">Confirm</string>
<string name="uart_delete_dialog_cancel">Cancel</string> <string name="uart_delete_dialog_cancel">Cancel</string>
<string name="uart_input_macro">Click to switch between text input and macro input.</string>
<string name="uart_clear_items">Clear items.</string>
<string name="uart_scroll_down">Click to constantly scroll view to the latest available log.</string>
<string name="uart_input_log">--&gt; %s</string>
<string name="uart_output_log" tools:ignore="TypographyDashes">&lt;-- %s</string>
<string name="uart_settings">Settings</string>
<string name="uart_settings_button">Go to settings screen.</string>
</resources> </resources>

View File

@@ -9,8 +9,8 @@ dependencyResolutionManagement {
versionCatalogs { versionCatalogs {
libs { libs {
library('nordic-ble-common', 'no.nordicsemi.android:ble-common:2.4.0') library('nordic-ble-common', 'no.nordicsemi.android:ble-common:2.4.1')
library('nordic-ble-ktx', 'no.nordicsemi.android:ble-ktx:2.4.0') library('nordic-ble-ktx', 'no.nordicsemi.android:ble-ktx:2.4.1')
library('nordic-scanner', 'no.nordicsemi.android.support.v18:scanner:1.6.0') library('nordic-scanner', 'no.nordicsemi.android.support.v18:scanner:1.6.0')
library('nordic-log', 'no.nordicsemi.android:log:2.3.0') library('nordic-log', 'no.nordicsemi.android:log:2.3.0')
@@ -21,7 +21,7 @@ dependencyResolutionManagement {
library('nordic-theme', 'no.nordicsemi.android.common', 'theme').versionRef('commonlibraries') library('nordic-theme', 'no.nordicsemi.android.common', 'theme').versionRef('commonlibraries')
library('localbroadcastmanager', 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0') library('localbroadcastmanager', 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0')
library('material', 'com.google.android.material:material:1.6.0-alpha02') library('material', 'com.google.android.material:material:1.6.0-rc01')
version('lifecycle', '2.4.1') version('lifecycle', '2.4.1')
library('lifecycle-activity', 'androidx.lifecycle', 'lifecycle-runtime-ktx').versionRef('lifecycle') library('lifecycle-activity', 'androidx.lifecycle', 'lifecycle-runtime-ktx').versionRef('lifecycle')
@@ -37,9 +37,9 @@ dependencyResolutionManagement {
library('datastore-protobuf', 'com.google.protobuf:protobuf-javalite:3.18.0') library('datastore-protobuf', 'com.google.protobuf:protobuf-javalite:3.18.0')
bundle('datastore', ['datastore-core', 'datastore-prefs', 'datastore-protobuf']) bundle('datastore', ['datastore-core', 'datastore-prefs', 'datastore-protobuf'])
version('compose', '1.1.0') version('compose', '1.2.0-alpha07')
library('compose-ui', 'androidx.compose.ui', 'ui').versionRef('compose') library('compose-ui', 'androidx.compose.ui', 'ui').versionRef('compose')
library('compose-material', 'androidx.compose.material3:material3:1.0.0-alpha05') library('compose-material', 'androidx.compose.material3:material3:1.0.0-alpha09')
library('compose-tooling-preview', 'androidx.compose.ui', 'ui-tooling-preview').versionRef('compose') library('compose-tooling-preview', 'androidx.compose.ui', 'ui-tooling-preview').versionRef('compose')
library('compose-navigation', 'androidx.navigation:navigation-compose:2.4.1') library('compose-navigation', 'androidx.navigation:navigation-compose:2.4.1')
bundle('compose', ['compose-ui', 'compose-material', 'compose-tooling-preview', 'compose-navigation']) bundle('compose', ['compose-ui', 'compose-material', 'compose-tooling-preview', 'compose-navigation'])