diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/BleManagerStatus.kt b/lib_service/src/main/java/no/nordicsemi/android/service/BleManagerStatus.kt
new file mode 100644
index 00000000..1ce97078
--- /dev/null
+++ b/lib_service/src/main/java/no/nordicsemi/android/service/BleManagerStatus.kt
@@ -0,0 +1,5 @@
+package no.nordicsemi.android.service
+
+enum class BleManagerStatus {
+ CONNECTING, OK, DISCONNECTED
+}
diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/BleProfileService.kt b/lib_service/src/main/java/no/nordicsemi/android/service/BleProfileService.kt
index bda5a2a9..dfc280a8 100644
--- a/lib_service/src/main/java/no/nordicsemi/android/service/BleProfileService.kt
+++ b/lib_service/src/main/java/no/nordicsemi/android/service/BleProfileService.kt
@@ -21,22 +21,34 @@
*/
package no.nordicsemi.android.service
+import android.app.Service
import android.bluetooth.BluetoothDevice
import android.content.Intent
import android.os.Handler
+import android.os.IBinder
+import android.util.Log
import android.widget.Toast
import androidx.lifecycle.LifecycleService
import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
import no.nordicsemi.android.ble.BleManager
import no.nordicsemi.android.log.ILogSession
import no.nordicsemi.android.log.Logger
import javax.inject.Inject
@AndroidEntryPoint
-abstract class BleProfileService : LifecycleService() {
+abstract class BleProfileService : Service() {
+
+ protected val scope = CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
protected abstract val manager: BleManager
+ private val _status = MutableStateFlow(BleManagerStatus.CONNECTING)
+ val status = _status.asStateFlow()
+
@Inject
lateinit var bluetoothDeviceHolder: SelectedBluetoothDeviceHolder
@@ -71,6 +83,23 @@ abstract class BleProfileService : LifecycleService() {
override fun onCreate() {
super.onCreate()
handler = Handler()
+
+ manager.setConnectionObserver(object : ConnectionObserverAdapter() {
+ override fun onDeviceConnected(device: BluetoothDevice) {
+ super.onDeviceConnected(device)
+ _status.value = BleManagerStatus.OK
+ }
+
+ override fun onDeviceDisconnected(device: BluetoothDevice, reason: Int) {
+ super.onDeviceDisconnected(device, reason)
+ _status.value = BleManagerStatus.DISCONNECTED
+ scope.close()
+ }
+ })
+ }
+
+ override fun onBind(intent: Intent?): IBinder? {
+ return null
}
/**
@@ -89,6 +118,7 @@ abstract class BleProfileService : LifecycleService() {
.useAutoConnect(shouldAutoConnect())
.retry(3, 100)
.enqueue()
+
return START_REDELIVER_INTENT
}
diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/CloseableCoroutineScope.kt b/lib_service/src/main/java/no/nordicsemi/android/service/CloseableCoroutineScope.kt
new file mode 100644
index 00000000..d2c1b22f
--- /dev/null
+++ b/lib_service/src/main/java/no/nordicsemi/android/service/CloseableCoroutineScope.kt
@@ -0,0 +1,14 @@
+package no.nordicsemi.android.service
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import java.io.Closeable
+import kotlin.coroutines.CoroutineContext
+
+class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
+ override val coroutineContext: CoroutineContext = context
+
+ override fun close() {
+ coroutineContext.cancel()
+ }
+}
diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/ConnectionObserverAdapter.kt b/lib_service/src/main/java/no/nordicsemi/android/service/ConnectionObserverAdapter.kt
new file mode 100644
index 00000000..cb3f90ac
--- /dev/null
+++ b/lib_service/src/main/java/no/nordicsemi/android/service/ConnectionObserverAdapter.kt
@@ -0,0 +1,19 @@
+package no.nordicsemi.android.service
+
+import android.bluetooth.BluetoothDevice
+import no.nordicsemi.android.ble.observer.ConnectionObserver
+
+abstract class ConnectionObserverAdapter : ConnectionObserver {
+
+ override fun onDeviceConnecting(device: BluetoothDevice) { }
+
+ override fun onDeviceConnected(device: BluetoothDevice) { }
+
+ override fun onDeviceFailedToConnect(device: BluetoothDevice, reason: Int) { }
+
+ override fun onDeviceReady(device: BluetoothDevice) { }
+
+ override fun onDeviceDisconnecting(device: BluetoothDevice) { }
+
+ override fun onDeviceDisconnected(device: BluetoothDevice, reason: Int) { }
+}
diff --git a/lib_theme/build.gradle b/lib_theme/build.gradle
index 273b27ee..5f8dbf44 100644
--- a/lib_theme/build.gradle
+++ b/lib_theme/build.gradle
@@ -6,6 +6,7 @@ dependencies {
implementation libs.nordic.theme
implementation libs.bundles.compose
+ implementation libs.bundles.icons
implementation libs.compose.lifecycle
implementation libs.compose.activity
}
diff --git a/lib_theme/src/main/java/no/nordicsemi/android/theme/view/DeviceConnectingView.kt b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/DeviceConnectingView.kt
new file mode 100644
index 00000000..cb498fa7
--- /dev/null
+++ b/lib_theme/src/main/java/no/nordicsemi/android/theme/view/DeviceConnectingView.kt
@@ -0,0 +1,73 @@
+package no.nordicsemi.android.theme.view
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.HourglassTop
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import no.nordicsemi.android.theme.R
+
+@Composable
+fun DeviceConnectingView() {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp)
+ ) {
+ ScreenSection {
+ Icon(
+ imageVector = Icons.Default.HourglassTop,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onSecondary,
+ modifier = Modifier
+ .background(
+ color = MaterialTheme.colorScheme.secondary,
+ shape = CircleShape
+ )
+ .padding(8.dp)
+ )
+
+ Spacer(modifier = Modifier.size(16.dp))
+
+ Text(
+ text = stringResource(id = R.string.device_connecting),
+ style = MaterialTheme.typography.titleMedium
+ )
+
+ Spacer(modifier = Modifier.size(16.dp))
+
+ Text(
+ text = stringResource(id = R.string.device_explanation),
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.bodyMedium
+ )
+
+ Spacer(modifier = Modifier.size(16.dp))
+
+ Text(
+ text = stringResource(id = R.string.device_please_wait),
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.titleLarge
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+fun DeviceConnectingView_Preview() {
+ DeviceConnectingView()
+}
diff --git a/lib_theme/src/main/res/values/strings.xml b/lib_theme/src/main/res/values/strings.xml
index b5bbd16c..570f2f80 100644
--- a/lib_theme/src/main/res/values/strings.xml
+++ b/lib_theme/src/main/res/values/strings.xml
@@ -10,4 +10,8 @@
DISCONNECT
Battery
+
+ Connecting
+ The mobile is trying to connect to peripheral device.
+ Please wait.
\ No newline at end of file
diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMRepository.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMRepository.kt
index 7aa81900..19749ca9 100644
--- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMRepository.kt
+++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMRepository.kt
@@ -2,6 +2,7 @@ package no.nordicsemi.android.cgms.data
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.*
+import no.nordicsemi.android.service.BleManagerStatus
import javax.inject.Inject
import javax.inject.Singleton
@@ -11,9 +12,12 @@ internal class CGMRepository @Inject constructor() {
private val _data = MutableStateFlow(CGMData())
val data: StateFlow = _data.asStateFlow()
- private val _command = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST)
+ private val _command = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST)
val command = _command.asSharedFlow()
+ private val _status = MutableStateFlow(BleManagerStatus.CONNECTING)
+ val status = _status.asStateFlow()
+
fun emitNewBatteryLevel(batterLevel: Int) {
_data.tryEmit(_data.value.copy(batteryLevel = batterLevel))
}
@@ -26,10 +30,14 @@ internal class CGMRepository @Inject constructor() {
_data.tryEmit(_data.value.copy(requestStatus = requestStatus))
}
- fun requestNewWorkingMode(workingMode: WorkingMode) {
+ fun sendNewServiceCommand(workingMode: CGMServiceCommand) {
_command.tryEmit(workingMode)
}
+ fun setNewStatus(status: BleManagerStatus) {
+ _status.value = status
+ }
+
fun clear() {
_data.tryEmit(CGMData())
}
diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMServiceCommand.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMServiceCommand.kt
new file mode 100644
index 00000000..486b1da2
--- /dev/null
+++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMServiceCommand.kt
@@ -0,0 +1,8 @@
+package no.nordicsemi.android.cgms.data
+
+internal enum class CGMServiceCommand {
+ REQUEST_ALL_RECORDS,
+ REQUEST_LAST_RECORD,
+ REQUEST_FIRST_RECORD,
+ DISCONNECT
+}
diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/WorkingMode.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/WorkingMode.kt
deleted file mode 100644
index d2f9bc52..00000000
--- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/WorkingMode.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package no.nordicsemi.android.cgms.data
-
-internal enum class WorkingMode {
- ALL,
- LAST,
- FIRST
-}
diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMService.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMService.kt
index 7b7e15e9..5ce8f8da 100644
--- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMService.kt
+++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMService.kt
@@ -1,11 +1,10 @@
package no.nordicsemi.android.cgms.repository
-import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.cgms.data.CGMRepository
-import no.nordicsemi.android.cgms.data.WorkingMode
+import no.nordicsemi.android.cgms.data.CGMServiceCommand
import no.nordicsemi.android.service.ForegroundBleService
import no.nordicsemi.android.utils.exhaustive
import javax.inject.Inject
@@ -14,19 +13,24 @@ import javax.inject.Inject
internal class CGMService : ForegroundBleService() {
@Inject
- lateinit var dataHolder: CGMRepository
+ lateinit var repository: CGMRepository
- override val manager: CGMManager by lazy { CGMManager(this, dataHolder) }
+ override val manager: CGMManager by lazy { CGMManager(this, repository) }
override fun onCreate() {
super.onCreate()
- dataHolder.command.onEach {
+ status.onEach {
+ repository.setNewStatus(it)
+ }.launchIn(scope)
+
+ repository.command.onEach {
when (it) {
- WorkingMode.ALL -> manager.requestAllRecords()
- WorkingMode.LAST -> manager.requestLastRecord()
- WorkingMode.FIRST -> manager.requestFirstRecord()
+ CGMServiceCommand.REQUEST_ALL_RECORDS -> manager.requestAllRecords()
+ CGMServiceCommand.REQUEST_LAST_RECORD -> manager.requestLastRecord()
+ CGMServiceCommand.REQUEST_FIRST_RECORD -> manager.requestFirstRecord()
+ CGMServiceCommand.DISCONNECT -> stopSelf()
}.exhaustive
- }.launchIn(lifecycleScope)
+ }.launchIn(scope)
}
}
diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMContentView.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMContentView.kt
index 21011594..f1f12c30 100644
--- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMContentView.kt
+++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMContentView.kt
@@ -18,7 +18,7 @@ import no.nordicsemi.android.cgms.R
import no.nordicsemi.android.cgms.data.CGMData
import no.nordicsemi.android.cgms.data.CGMRecord
import no.nordicsemi.android.cgms.data.RequestStatus
-import no.nordicsemi.android.cgms.data.WorkingMode
+import no.nordicsemi.android.cgms.data.CGMServiceCommand
import no.nordicsemi.android.material.you.CircularProgressIndicator
import no.nordicsemi.android.theme.view.BatteryLevelView
import no.nordicsemi.android.theme.view.ScreenSection
@@ -29,12 +29,10 @@ internal fun CGMContentView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
- .padding(horizontal = 16.dp)
+ .padding(16.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
- Spacer(modifier = Modifier.height(16.dp))
-
SettingsView(state, onEvent)
Spacer(modifier = Modifier.height(16.dp))
@@ -71,10 +69,14 @@ private fun SettingsView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) {
if (state.requestStatus == RequestStatus.PENDING) {
CircularProgressIndicator()
} else {
- WorkingMode.values().forEach {
- Button(onClick = { onEvent(OnWorkingModeSelected(it)) }) {
- Text(it.toDisplayString())
- }
+ Button(onClick = { onEvent(OnWorkingModeSelected(CGMServiceCommand.REQUEST_ALL_RECORDS)) }) {
+ Text(stringResource(id = R.string.cgms__working_mode__all))
+ }
+ Button(onClick = { onEvent(OnWorkingModeSelected(CGMServiceCommand.REQUEST_LAST_RECORD)) }) {
+ Text(stringResource(id = R.string.cgms__working_mode__last))
+ }
+ Button(onClick = { onEvent(OnWorkingModeSelected(CGMServiceCommand.REQUEST_FIRST_RECORD)) }) {
+ Text(stringResource(id = R.string.cgms__working_mode__first))
}
}
}
diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMMapper.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMMapper.kt
index 42bbf071..3fb7871b 100644
--- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMMapper.kt
+++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMMapper.kt
@@ -4,19 +4,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import no.nordicsemi.android.cgms.R
import no.nordicsemi.android.cgms.data.CGMRecord
-import no.nordicsemi.android.cgms.data.WorkingMode
+import no.nordicsemi.android.cgms.data.CGMServiceCommand
import java.text.SimpleDateFormat
import java.util.*
-@Composable
-internal fun WorkingMode.toDisplayString(): String {
- return when (this) {
- WorkingMode.ALL -> stringResource(id = R.string.cgms__working_mode__all)
- WorkingMode.LAST -> stringResource(id = R.string.cgms__working_mode__last)
- WorkingMode.FIRST -> stringResource(id = R.string.cgms__working_mode__first)
- }
-}
-
internal fun CGMRecord.formattedTime(): String {
val timeFormat = SimpleDateFormat("dd.MM.yyyy HH:mm", Locale.US)
return timeFormat.format(Date(timestamp))
diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMScreen.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMScreen.kt
index 33be257e..32a53631 100644
--- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMScreen.kt
+++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMScreen.kt
@@ -9,49 +9,43 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.cgms.R
-import no.nordicsemi.android.cgms.data.CGMData
import no.nordicsemi.android.cgms.repository.CGMService
import no.nordicsemi.android.cgms.viewmodel.CGMScreenViewModel
import no.nordicsemi.android.theme.view.BackIconAppBar
-import no.nordicsemi.android.utils.isServiceRunning
+import no.nordicsemi.android.theme.view.DeviceConnectingView
+import no.nordicsemi.android.utils.exhaustive
@Composable
fun CGMScreen(finishAction: () -> Unit) {
val viewModel: CGMScreenViewModel = hiltViewModel()
val state = viewModel.state.collectAsState().value
- val isScreenActive = viewModel.isActive.collectAsState().value
val context = LocalContext.current
- LaunchedEffect(isScreenActive) {
- if (!isScreenActive) {
- finishAction()
- }
- if (context.isServiceRunning(CGMService::class.java.name)) {
- val intent = Intent(context, CGMService::class.java)
- context.stopService(intent)
- }
- }
-
- LaunchedEffect("start-service") {
- if (!context.isServiceRunning(CGMService::class.java.name)) {
+ LaunchedEffect(state.isActive) {
+ if (state.isActive) {
val intent = Intent(context, CGMService::class.java)
context.startService(intent)
+ } else if (!state.isActive) {
+ finishAction()
}
}
- CGMView(state) {
+ CGMView(state.viewState) {
viewModel.onEvent(it)
}
}
@Composable
-private fun CGMView(state: CGMData, onEvent: (CGMViewEvent) -> Unit) {
+private fun CGMView(state: CGMViewState, onEvent: (CGMViewEvent) -> Unit) {
Column {
BackIconAppBar(stringResource(id = R.string.cgms_title)) {
onEvent(DisconnectEvent)
}
- CGMContentView(state, onEvent)
+ when (state) {
+ is DisplayDataState -> CGMContentView(state.data, onEvent)
+ LoadingState -> DeviceConnectingView()
+ }.exhaustive
}
}
diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMViewEvent.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMViewEvent.kt
index 7599efab..865116da 100644
--- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMViewEvent.kt
+++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMViewEvent.kt
@@ -1,9 +1,9 @@
package no.nordicsemi.android.cgms.view
-import no.nordicsemi.android.cgms.data.WorkingMode
+import no.nordicsemi.android.cgms.data.CGMServiceCommand
internal sealed class CGMViewEvent
-internal data class OnWorkingModeSelected(val workingMode: WorkingMode) : CGMViewEvent()
+internal data class OnWorkingModeSelected(val workingMode: CGMServiceCommand) : CGMViewEvent()
internal object DisconnectEvent : CGMViewEvent()
diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMViewState.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMViewState.kt
new file mode 100644
index 00000000..b2f1116b
--- /dev/null
+++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMViewState.kt
@@ -0,0 +1,14 @@
+package no.nordicsemi.android.cgms.view
+
+import no.nordicsemi.android.cgms.data.CGMData
+
+internal data class CGMState(
+ val viewState: CGMViewState,
+ val isActive: Boolean = true
+)
+
+internal sealed class CGMViewState
+
+internal object LoadingState : CGMViewState()
+
+internal data class DisplayDataState(val data: CGMData) : CGMViewState()
diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/viewmodel/CGMScreenViewModel.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/viewmodel/CGMScreenViewModel.kt
index 17c99d4f..997fccab 100644
--- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/viewmodel/CGMScreenViewModel.kt
+++ b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/viewmodel/CGMScreenViewModel.kt
@@ -1,30 +1,45 @@
package no.nordicsemi.android.cgms.viewmodel
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
import no.nordicsemi.android.cgms.data.CGMRepository
+import no.nordicsemi.android.cgms.data.CGMServiceCommand
+import no.nordicsemi.android.cgms.view.CGMState
import no.nordicsemi.android.cgms.view.CGMViewEvent
import no.nordicsemi.android.cgms.view.DisconnectEvent
+import no.nordicsemi.android.cgms.view.DisplayDataState
+import no.nordicsemi.android.cgms.view.LoadingState
import no.nordicsemi.android.cgms.view.OnWorkingModeSelected
-import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
+import no.nordicsemi.android.service.BleManagerStatus
import no.nordicsemi.android.utils.exhaustive
import javax.inject.Inject
@HiltViewModel
internal class CGMScreenViewModel @Inject constructor(
- private val dataHolder: CGMRepository
-) : CloseableViewModel() {
+ private val repository: CGMRepository
+) : ViewModel() {
- val state = dataHolder.data
+ val state = repository.data.combine(repository.status) { data, status ->
+ when (status) {
+ BleManagerStatus.CONNECTING -> CGMState(LoadingState)
+ BleManagerStatus.OK -> CGMState(DisplayDataState(data))
+ BleManagerStatus.DISCONNECTED -> CGMState(DisplayDataState(data), false)
+ }
+ }.stateIn(viewModelScope, SharingStarted.Lazily, CGMState(LoadingState))
fun onEvent(event: CGMViewEvent) {
when (event) {
DisconnectEvent -> disconnect()
- is OnWorkingModeSelected -> dataHolder.requestNewWorkingMode(event.workingMode)
+ is OnWorkingModeSelected -> repository.sendNewServiceCommand(event.workingMode)
}.exhaustive
}
private fun disconnect() {
- finish()
- dataHolder.clear()
+ repository.clear()
+ repository.sendNewServiceCommand(CGMServiceCommand.DISCONNECT)
}
}
diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCContentView.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCContentView.kt
index 07e27442..08d98610 100644
--- a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCContentView.kt
+++ b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCContentView.kt
@@ -4,6 +4,8 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Button
@@ -26,24 +28,24 @@ internal fun CSCContentView(state: CSCData, onEvent: (CSCViewEvent) -> Unit) {
SelectWheelSizeDialog { onEvent(it) }
}
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- modifier = Modifier.padding(horizontal = 16.dp)
- ) {
- Spacer(modifier = Modifier.height(16.dp))
-
- SettingsSection(state, onEvent)
-
- Spacer(modifier = Modifier.height(16.dp))
-
- SensorsReadingView(state = state)
-
- Spacer(modifier = Modifier.height(16.dp))
-
- Button(
- onClick = { onEvent(OnDisconnectButtonClick) }
+ Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.padding(16.dp)
) {
- Text(text = stringResource(id = R.string.disconnect))
+ SettingsSection(state, onEvent)
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ SensorsReadingView(state = state)
+
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Button(
+ onClick = { onEvent(OnDisconnectButtonClick) }
+ ) {
+ Text(text = stringResource(id = R.string.disconnect))
+ }
}
}
}
diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/service/PRXService.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/service/PRXService.kt
index 675981e0..2d71f6ef 100644
--- a/profile_prx/src/main/java/no/nordicsemi/android/prx/service/PRXService.kt
+++ b/profile_prx/src/main/java/no/nordicsemi/android/prx/service/PRXService.kt
@@ -39,7 +39,7 @@ internal class PRXService : ForegroundBleService() {
DisableAlarm -> manager.writeImmediateAlert(false)
EnableAlarm -> manager.writeImmediateAlert(true)
}.exhaustive
- }.launchIn(lifecycleScope)
+ }.launchIn(scope)
dataHolder.data.onEach {
if (it.localAlarmLevel != AlarmLevel.NONE) {
@@ -47,7 +47,7 @@ internal class PRXService : ForegroundBleService() {
} else {
alarmHandler.pauseAlarm()
}
- }.launchIn(lifecycleScope)
+ }.launchIn(scope)
}
override fun onDestroy() {
diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCMeasurementParser.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCMeasurementParser.kt
deleted file mode 100644
index d0246d2c..00000000
--- a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCMeasurementParser.kt
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (c) 2015, Nordic Semiconductor
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
- *
- * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- *
- * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this
- * software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
- * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package no.nordicsemi.android.rscs.service
-
-import no.nordicsemi.android.ble.data.Data
-import java.util.*
-
-internal object RSCMeasurementParser {
-
- private const val INSTANTANEOUS_STRIDE_LENGTH_PRESENT: Byte = 0x01 // 1 bit
- private const val TOTAL_DISTANCE_PRESENT: Byte = 0x02 // 1 bit
- private const val WALKING_OR_RUNNING_STATUS_BITS: Byte = 0x04 // 1 bit
-
- fun parse(data: Data): String {
- var offset = 0
- val flags = data.value!![offset].toInt() // 1 byte
- offset += 1
- val islmPresent = flags and INSTANTANEOUS_STRIDE_LENGTH_PRESENT.toInt() > 0
- val tdPreset = flags and TOTAL_DISTANCE_PRESENT.toInt() > 0
- val running = flags and WALKING_OR_RUNNING_STATUS_BITS.toInt() > 0
- val walking = !running
- val instantaneousSpeed =
- data.getIntValue(Data.FORMAT_UINT16, offset) as Float / 256.0f // 1/256 m/s
- offset += 2
- val instantaneousCadence = data.getIntValue(Data.FORMAT_UINT8, offset)!!
- offset += 1
- var instantaneousStrideLength = 0f
- if (islmPresent) {
- instantaneousStrideLength =
- data.getIntValue(Data.FORMAT_UINT16, offset) as Float / 100.0f // 1/100 m
- offset += 2
- }
- var totalDistance = 0f
- if (tdPreset) {
- totalDistance = data.getIntValue(Data.FORMAT_UINT32, offset) as Float / 10.0f
- // offset += 4;
- }
- val builder = StringBuilder()
- builder.append(
- String.format(
- Locale.US,
- "Speed: %.2f m/s, Cadence: %d RPM,\n",
- instantaneousSpeed,
- instantaneousCadence
- )
- )
- if (islmPresent) builder.append(
- String.format(
- Locale.US,
- "Instantaneous Stride Length: %.2f m,\n",
- instantaneousStrideLength
- )
- )
- if (tdPreset) builder.append(
- String.format(
- Locale.US,
- "Total Distance: %.1f m,\n",
- totalDistance
- )
- )
- if (walking) builder.append("Status: WALKING") else builder.append("Status: RUNNING")
- return builder.toString()
- }
-}
diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCSManager.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCSManager.kt
index 99982c00..a84847db 100644
--- a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCSManager.kt
+++ b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/service/RSCSManager.kt
@@ -26,8 +26,6 @@ import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.content.Context
import no.nordicsemi.android.ble.common.callback.rsc.RunningSpeedAndCadenceMeasurementDataCallback
-import no.nordicsemi.android.ble.data.Data
-import no.nordicsemi.android.log.LogContract
import no.nordicsemi.android.rscs.data.RSCSRepository
import no.nordicsemi.android.service.BatteryManager
import java.util.*
@@ -46,13 +44,6 @@ internal class RSCSManager internal constructor(
private var rscMeasurementCharacteristic: BluetoothGattCharacteristic? = null
private val callback = object : RunningSpeedAndCadenceMeasurementDataCallback() {
- override fun onDataReceived(device: BluetoothDevice, data: Data) {
- log(
- LogContract.Log.Level.APPLICATION,
- "\"" + RSCMeasurementParser.parse(data).toString() + "\" received"
- )
- super.onDataReceived(device, data)
- }
override fun onRSCMeasurementReceived(
device: BluetoothDevice,
diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTRepository.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTRepository.kt
index 5fbf9934..82c0faaf 100644
--- a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTRepository.kt
+++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTRepository.kt
@@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
+import no.nordicsemi.android.service.BleManagerStatus
import javax.inject.Inject
import javax.inject.Singleton
@@ -17,6 +18,9 @@ internal class UARTRepository @Inject constructor() {
private val _command = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST)
val command = _command.asSharedFlow()
+ private val _status = MutableStateFlow(BleManagerStatus.CONNECTING)
+ val status = _status.asStateFlow()
+
fun addNewMacro(macro: UARTMacro) {
_data.tryEmit(_data.value.copy(macros = _data.value.macros + macro))
}
@@ -39,4 +43,8 @@ internal class UARTRepository @Inject constructor() {
fun sendNewCommand(command: UARTServiceCommand) {
_command.tryEmit(command)
}
+
+ fun setNewStatus(status: BleManagerStatus) {
+ _status.value = status
+ }
}
diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTServiceCommand.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTServiceCommand.kt
index eb6fb906..d7235ede 100644
--- a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTServiceCommand.kt
+++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTServiceCommand.kt
@@ -1,3 +1,7 @@
package no.nordicsemi.android.uart.data
-data class UARTServiceCommand(val command: String)
+internal sealed class UARTServiceCommand
+
+internal data class SendTextCommand(val command: String) : UARTServiceCommand()
+
+internal object DisconnectCommand : UARTServiceCommand()
diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTService.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTService.kt
index 282145c8..19301c10 100644
--- a/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTService.kt
+++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTService.kt
@@ -1,26 +1,35 @@
package no.nordicsemi.android.uart.repository
-import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import no.nordicsemi.android.service.ForegroundBleService
+import no.nordicsemi.android.uart.data.DisconnectCommand
+import no.nordicsemi.android.uart.data.SendTextCommand
import no.nordicsemi.android.uart.data.UARTRepository
+import no.nordicsemi.android.utils.exhaustive
import javax.inject.Inject
@AndroidEntryPoint
internal class UARTService : ForegroundBleService() {
@Inject
- lateinit var dataHolder: UARTRepository
+ lateinit var repository: UARTRepository
- override val manager: UARTManager by lazy { UARTManager(this, dataHolder) }
+ override val manager: UARTManager by lazy { UARTManager(this, repository) }
override fun onCreate() {
super.onCreate()
- dataHolder.command.onEach {
- manager.send(it.command)
- }.launchIn(lifecycleScope)
+ status.onEach {
+ repository.setNewStatus(it)
+ }.launchIn(scope)
+
+ repository.command.onEach {
+ when (it) {
+ DisconnectCommand -> stopSelf()
+ is SendTextCommand -> manager.send(it.command)
+ }.exhaustive
+ }.launchIn(scope)
}
}
diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTScreen.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTScreen.kt
index ddbddeff..97ae8ed9 100644
--- a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTScreen.kt
+++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTScreen.kt
@@ -9,46 +9,40 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import no.nordicsemi.android.theme.view.BackIconAppBar
+import no.nordicsemi.android.theme.view.DeviceConnectingView
import no.nordicsemi.android.uart.R
-import no.nordicsemi.android.uart.data.UARTData
import no.nordicsemi.android.uart.repository.UARTService
import no.nordicsemi.android.uart.viewmodel.UARTViewModel
-import no.nordicsemi.android.utils.isServiceRunning
+import no.nordicsemi.android.utils.exhaustive
@Composable
fun UARTScreen(finishAction: () -> Unit) {
val viewModel: UARTViewModel = hiltViewModel()
val state = viewModel.state.collectAsState().value
- val isScreenActive = viewModel.isActive.collectAsState().value
val context = LocalContext.current
- LaunchedEffect(isScreenActive) {
- if (!isScreenActive) {
- finishAction()
- }
- if (context.isServiceRunning(UARTService::class.java.name)) {
- val intent = Intent(context, UARTService::class.java)
- context.stopService(intent)
- }
- }
-
- LaunchedEffect("start-service") {
- if (!context.isServiceRunning(UARTService::class.java.name)) {
+ LaunchedEffect(state.isActive) {
+ if (state.isActive) {
val intent = Intent(context, UARTService::class.java)
context.startService(intent)
+ } else if (!state.isActive) {
+ finishAction()
}
}
- UARTView(state) { viewModel.onEvent(it) }
+ UARTView(state.viewState) { viewModel.onEvent(it) }
}
@Composable
-private fun UARTView(state: UARTData, onEvent: (UARTViewEvent) -> Unit) {
+private fun UARTView(state: UARTViewState, onEvent: (UARTViewEvent) -> Unit) {
Column {
BackIconAppBar(stringResource(id = R.string.uart_title)) {
onEvent(OnDisconnectButtonClick)
}
- UARTContentView(state) { onEvent(it) }
+ when (state) {
+ is DisplayDataState -> UARTContentView(state.data) { onEvent(it) }
+ LoadingState -> DeviceConnectingView()
+ }.exhaustive
}
}
diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTState.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTState.kt
new file mode 100644
index 00000000..823a336b
--- /dev/null
+++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTState.kt
@@ -0,0 +1,14 @@
+package no.nordicsemi.android.uart.view
+
+import no.nordicsemi.android.uart.data.UARTData
+
+internal data class UARTState(
+ val viewState: UARTViewState,
+ val isActive: Boolean = true
+)
+
+internal sealed class UARTViewState
+
+internal object LoadingState : UARTViewState()
+
+internal data class DisplayDataState(val data: UARTData) : UARTViewState()
diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/viewmodel/UARTViewModel.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/viewmodel/UARTViewModel.kt
index 322ce94d..137a3894 100644
--- a/profile_uart/src/main/java/no/nordicsemi/android/uart/viewmodel/UARTViewModel.kt
+++ b/profile_uart/src/main/java/no/nordicsemi/android/uart/viewmodel/UARTViewModel.kt
@@ -1,26 +1,46 @@
package no.nordicsemi.android.uart.viewmodel
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
+import no.nordicsemi.android.service.BleManagerStatus
import no.nordicsemi.android.theme.viewmodel.CloseableViewModel
+import no.nordicsemi.android.uart.data.DisconnectCommand
+import no.nordicsemi.android.uart.data.SendTextCommand
import no.nordicsemi.android.uart.data.UARTRepository
-import no.nordicsemi.android.uart.data.UARTServiceCommand
-import no.nordicsemi.android.uart.view.*
+import no.nordicsemi.android.uart.view.DisplayDataState
+import no.nordicsemi.android.uart.view.LoadingState
+import no.nordicsemi.android.uart.view.OnCreateMacro
+import no.nordicsemi.android.uart.view.OnDeleteMacro
+import no.nordicsemi.android.uart.view.OnDisconnectButtonClick
+import no.nordicsemi.android.uart.view.OnRunMacro
+import no.nordicsemi.android.uart.view.UARTState
+import no.nordicsemi.android.uart.view.UARTViewEvent
import no.nordicsemi.android.utils.exhaustive
import javax.inject.Inject
@HiltViewModel
internal class UARTViewModel @Inject constructor(
- private val dataHolder: UARTRepository
-) : CloseableViewModel() {
+ private val repository: UARTRepository
+) : ViewModel() {
- val state = dataHolder.data
+ val state = repository.data.combine(repository.status) { data, status ->
+ when (status) {
+ BleManagerStatus.CONNECTING -> UARTState(LoadingState)
+ BleManagerStatus.OK -> UARTState(DisplayDataState(data))
+ BleManagerStatus.DISCONNECTED -> UARTState(DisplayDataState(data), false)
+ }
+ }.stateIn(viewModelScope, SharingStarted.Lazily, UARTState(LoadingState))
fun onEvent(event: UARTViewEvent) {
when (event) {
- is OnCreateMacro -> dataHolder.addNewMacro(event.macro)
- is OnDeleteMacro -> dataHolder.deleteMacro(event.macro)
- OnDisconnectButtonClick -> finish()
- is OnRunMacro -> dataHolder.sendNewCommand(UARTServiceCommand(event.macro.command))
+ is OnCreateMacro -> repository.addNewMacro(event.macro)
+ is OnDeleteMacro -> repository.deleteMacro(event.macro)
+ OnDisconnectButtonClick -> repository.sendNewCommand(DisconnectCommand)
+ is OnRunMacro -> repository.sendNewCommand(SendTextCommand(event.macro.command))
}.exhaustive
}
}
diff --git a/settings.gradle b/settings.gradle
index beda6bd0..be26bef9 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -37,6 +37,10 @@ dependencyResolutionManagement {
alias('compose-navigation').to('androidx.navigation:navigation-compose:2.4.0-alpha09')
bundle('compose', ['compose-livedata', 'compose-ui', 'compose-material', 'compose-tooling-preview', 'compose-navigation'])
+ alias('material-icons').to('androidx.compose.material', 'material-icons-core').versionRef('compose')
+ alias('material-icons-extended').to('androidx.compose.material', 'material-icons-extended').versionRef('compose')
+ bundle('icons', ['material-icons', 'material-icons-extended'])
+
version('hilt', '2.38.1')
alias('hilt-android').to('com.google.dagger', 'hilt-android').versionRef('hilt')
alias('hilt-compiler').to('com.google.dagger', 'hilt-compiler').versionRef('hilt')