Skip to content

Commit

Permalink
#200: Major fixes to bluetooth pairing and app behavior:
Browse files Browse the repository at this point in the history
- Completely rebuilt the bluetooth core
- Implemented new pairing behavior
- Added error message capabilities for each observation
- Added the capability to open the bluetooth device connection view, when a device is not connected, directly from the task view or the error list view
- Added a new button to the schedule header which shows the amount of errors and opens a new page, which shows a list of all errors
- Fixed Threading issues
- General improvements
- Version Upgrades
  • Loading branch information
janoliver20 committed May 23, 2024
1 parent 86237f3 commit 244471c
Show file tree
Hide file tree
Showing 58 changed files with 930 additions and 207 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ enum class NavigationScreen(
NavType.StringType, ""
)
), stringResource = R.string.nav_limesurvey
);
),
OBSERVATION_ERRORS("observation-errors", stringResource = R.string.nav_observation_errors);

private var cachedNavArguments: List<NamedNavArgument>? = null
private var cachedRoute: String? = null
Expand Down Expand Up @@ -172,6 +173,8 @@ enum class NavigationScreen(

fun byRoute(route: String) = entries.firstOrNull { it.route == route }

fun allDeepLinks(deepLinkHost: String) = entries.flatMap { it.createDeepLinkRoute(deepLinkHost).mapNotNull { it.uriPattern } }.toSet()
fun allDeepLinks(deepLinkHost: String) =
entries.flatMap { it.createDeepLinkRoute(deepLinkHost).mapNotNull { it.uriPattern } }
.toSet()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.redlink.more.app.android.R
import io.redlink.more.app.android.extensions.getStringResource
import io.redlink.more.app.android.services.sensorsListener.BluetoothStateListener
import io.redlink.more.app.android.services.sensorsListener.GPSStateListener
import io.redlink.more.app.android.shared_composables.BasicText
import io.redlink.more.app.android.shared_composables.EmptyListView
Expand All @@ -53,6 +54,7 @@ import io.redlink.more.app.android.ui.theme.MoreColors
class BLEConnectionActivity : ComponentActivity() {
val viewModel = BluetoothViewModel()
private var gpsListenerSelfActivated = false
private var bluetoothListenerSelfActivated = false

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -65,6 +67,10 @@ class BLEConnectionActivity : ComponentActivity() {
override fun onResume() {
super.onResume()
viewModel.viewDidAppear()
if (!BluetoothStateListener.listenerActive) {
bluetoothListenerSelfActivated = true
BluetoothStateListener.startListening(this)
}
if (!GPSStateListener.listenerActive) {
gpsListenerSelfActivated = true
GPSStateListener.startListening(this)
Expand All @@ -78,6 +84,10 @@ class BLEConnectionActivity : ComponentActivity() {
gpsListenerSelfActivated = false
GPSStateListener.stopListening(this)
}
if (bluetoothListenerSelfActivated) {
bluetoothListenerSelfActivated = false
BluetoothStateListener.stopListening(this)
}
}

companion object {
Expand Down Expand Up @@ -198,30 +208,32 @@ fun LoginBLESetupView(viewModel: BluetoothViewModel, showDescrPart2: Boolean) {
} else {
itemsIndexed(viewModel.discoveredDevices) { _, device ->
Column(
horizontalAlignment = Alignment.Start,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxWidth()
.height(55.dp)
.height(60.dp)
.clickable {
viewModel.connectToDevice(device)
}
) {
MoreDivider()
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxSize()
) {
SmallTitle(
text = device.deviceName
?: getStringResource(id = R.string.more_ble_unknown_device)
)
if (device.address in viewModel.connectingDevices) {
CircularProgressIndicator(
strokeWidth = 1.dp,
strokeWidth = 2.dp,
color = MoreColors.Primary,
modifier = Modifier
.width(20.dp)
.height(20.dp)
.width(progressSize)
.height(progressSize)
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import io.redlink.more.app.android.MoreApplication
import io.redlink.more.app.android.services.sensorsListener.BluetoothStateListener
import io.redlink.more.app.android.services.sensorsListener.GPSStateListener
import io.redlink.more.more_app_mutliplatform.AlertController
import io.redlink.more.more_app_mutliplatform.models.AlertDialogModel
import io.redlink.more.more_app_mutliplatform.services.bluetooth.BluetoothDevice
import io.redlink.more.more_app_mutliplatform.services.bluetooth.BluetoothDeviceManager
import io.redlink.more.more_app_mutliplatform.services.bluetooth.BluetoothState
import io.redlink.more.more_app_mutliplatform.viewModels.ViewManager
import io.redlink.more.more_app_mutliplatform.viewModels.startupConnection.CoreBluetoothViewModel
import kotlinx.coroutines.Dispatchers
Expand All @@ -36,7 +36,7 @@ class BluetoothViewModel : ViewModel() {
val connectedDevices = mutableStateListOf<BluetoothDevice>()
val connectingDevices = mutableStateListOf<String>()
val isScanning = mutableStateOf(false)
val bluetoothPowerState = mutableStateOf(false)
val bluetoothPowerState = mutableStateOf(BluetoothStateListener.bluetoothEnabled.value)

val neededDevices = mutableStateListOf<String>()

Expand All @@ -60,40 +60,52 @@ class BluetoothViewModel : ViewModel() {
}
viewModelScope.launch(Dispatchers.IO) {
BluetoothDeviceManager.discoveredDevices.collect {
discoveredDevices.clear()
discoveredDevices.addAll(it)
withContext(Dispatchers.Main) {
discoveredDevices.clear()
discoveredDevices.addAll(it)
}
}
}

viewModelScope.launch {
viewModelScope.launch(Dispatchers.IO) {
BluetoothDeviceManager.connectedDevices.collect {
connectedDevices.clear()
connectedDevices.addAll(it)
withContext(Dispatchers.Main) {
connectedDevices.clear()
connectedDevices.addAll(it)
}
}
}

viewModelScope.launch {
viewModelScope.launch(Dispatchers.IO) {
coreBluetoothViewModel.coreBluetooth.isScanning.collect {
isScanning.value = it
withContext(Dispatchers.Main) {
isScanning.value = it
}
}
}

viewModelScope.launch {
viewModelScope.launch(Dispatchers.IO) {
coreBluetoothViewModel.devicesNeededToConnectTo.collect {
neededDevices.clear()
neededDevices.addAll(MoreApplication.shared!!.observationFactory.bleDevicesNeeded())
withContext(Dispatchers.Main) {
neededDevices.clear()
neededDevices.addAll(MoreApplication.shared!!.observationFactory.bleDevicesNeeded())
}
}
}
viewModelScope.launch {
coreBluetoothViewModel.coreBluetooth.bluetoothPower.collect {
bluetoothPowerState.value = it == BluetoothState.ON
viewModelScope.launch(Dispatchers.IO) {
BluetoothStateListener.bluetoothEnabled.collect {
withContext(Dispatchers.Main) {
bluetoothPowerState.value = it
}
}
}

viewModelScope.launch {
viewModelScope.launch(Dispatchers.IO) {
BluetoothDeviceManager.devicesCurrentlyConnecting.collect {
connectingDevices.clear()
connectingDevices.addAll(it.mapNotNull { it.address })
withContext(Dispatchers.Main) {
connectingDevices.clear()
connectingDevices.addAll(it.mapNotNull { it.address })
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import io.redlink.more.app.android.extensions.jvmLocalDate
import io.redlink.more.app.android.observations.HR.PolarHeartRateObservation
import io.redlink.more.more_app_mutliplatform.models.ScheduleListType
import io.redlink.more.more_app_mutliplatform.models.ScheduleModel
import io.redlink.more.more_app_mutliplatform.observations.Observation
import io.redlink.more.more_app_mutliplatform.viewModels.dashboard.CoreDashboardFilterViewModel
import io.redlink.more.more_app_mutliplatform.viewModels.schedules.CoreScheduleViewModel
import kotlinx.coroutines.Dispatchers
Expand All @@ -44,6 +45,7 @@ class ScheduleViewModel(

val schedulesByDate = mutableStateMapOf<LocalDate, List<ScheduleModel>>()
val observationErrors = mutableStateMapOf<String, Set<String>>()
val observationErrorActions = mutableStateMapOf<String, Set<String>>()

val filterModel = DashboardFilterViewModel(coreDashboardFilterViewModel)

Expand All @@ -52,9 +54,17 @@ class ScheduleViewModel(
init {
viewModelScope.launch(Dispatchers.IO) {
MoreApplication.shared!!.observationFactory.observationErrors.collect {
val actions = it.mapValues { entry ->
entry.value.filter { it == Observation.ERROR_DEVICE_NOT_CONNECTED }.toSet()
}
val errors = it.mapValues { entry ->
entry.value.filter { it != Observation.ERROR_DEVICE_NOT_CONNECTED }.toSet()
}
withContext(Dispatchers.Main) {
observationErrors.clear()
observationErrors.putAll(it)
observationErrors.putAll(errors)
observationErrorActions.clear()
observationErrorActions.putAll(actions)
}
}
}
Expand Down Expand Up @@ -104,6 +114,9 @@ class ScheduleViewModel(
coreViewModel.stop(scheduleId)
}

fun numberOfObservationErrors(): Int = observationErrors.values.flatten().toSet().count()
.let { if (it > 0) it else observationErrorActions.values.flatten().toSet().count() }


private fun mergeSchedules(
first: Set<ScheduleModel>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
import androidx.compose.material.icons.filled.Warning
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
Expand All @@ -49,8 +48,6 @@ fun ScheduleListItem(
viewModel: ScheduleViewModel,
showButton: Boolean
) {
val observationErrors =
remember { viewModel.observationErrors[scheduleModel.observationType] ?: emptySet() }
Column(
verticalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier
Expand Down Expand Up @@ -80,8 +77,10 @@ fun ScheduleListItem(
) {
BasicText(text = scheduleModel.observationType, color = MoreColors.Secondary)
Row(horizontalArrangement = Arrangement.End) {
if (observationErrors.isNotEmpty()) {
BasicText(text = "${observationErrors.count()}")
if ((viewModel.observationErrors[scheduleModel.observationType]?.count()
?: 0) > 0
) {
BasicText(text = "${viewModel.observationErrors[scheduleModel.observationType]?.count() ?: 0}")
Icon(
Icons.Default.Warning,
contentDescription = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import io.redlink.more.app.android.activities.dashboard.filter.DashboardFilterVi
import io.redlink.more.app.android.activities.info.InfoView
import io.redlink.more.app.android.activities.notification.NotificationView
import io.redlink.more.app.android.activities.notification.filter.NotificationFilterView
import io.redlink.more.app.android.activities.observationErrors.ObservationErrorView
import io.redlink.more.app.android.activities.observations.questionnaire.QuestionnaireResponseView
import io.redlink.more.app.android.activities.observations.questionnaire.QuestionnaireView
import io.redlink.more.app.android.activities.runningSchedules.RunningSchedulesView
Expand Down Expand Up @@ -440,6 +441,19 @@ fun MainView(
LeaveStudyConfirmView(navController, viewModel = viewModel.leaveStudyViewModel)
}
}

NavigationScreen.OBSERVATION_ERRORS.let { screen ->
composable(
screen.routeWithParameters(),
screen.createListOfNavArguments(),
screen.createDeepLinkRoute()
) {
viewModel.navigationBarTitle.value =
screen.stringRes()
viewModel.showBackButton.value = true
ObservationErrorView()
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,6 @@ class MainViewModel(context: Context) : ViewModel() {
}
}

private fun showBLESetup() {
MoreApplication.shared!!.showBLESetupOnFirstStartup()
}

fun getTaskDetailsVM(scheduleId: String) =
taskDetailsViewModel.apply { setSchedule(scheduleId) }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package io.redlink.more.app.android.activities.observationErrors

import android.app.Activity
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.filled.Watch
import androidx.compose.runtime.Composable
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.redlink.more.app.android.R
import io.redlink.more.app.android.activities.NavigationScreen
import io.redlink.more.app.android.activities.bluetooth.BLEConnectionActivity
import io.redlink.more.app.android.extensions.getStringResource
import io.redlink.more.app.android.extensions.getStringResourceByName
import io.redlink.more.app.android.extensions.showNewActivity
import io.redlink.more.app.android.shared_composables.BasicText
import io.redlink.more.app.android.shared_composables.SmallTextIconButton
import io.redlink.more.app.android.ui.theme.MoreColors
import io.redlink.more.more_app_mutliplatform.observations.Observation

@Composable
fun ObservationErrorListView(
errors: SnapshotStateList<String>,
errorActions: SnapshotStateList<String>
) {
val context = LocalContext.current
if (errors.isNotEmpty()) {
LazyColumn(
verticalArrangement = Arrangement.Bottom,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
items(errors.toList()) { error ->
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
) {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = "Error",
tint = MoreColors.Important,
modifier = Modifier.padding(end = 4.dp)
)
BasicText(
text = "${getStringResourceByName(error)}!",
fontSize = 16.sp,
color = MoreColors.Important
)
}
}
if (errorActions.isNotEmpty()) {
item {
if (errorActions.contains(Observation.ERROR_DEVICE_NOT_CONNECTED)) {
SmallTextIconButton(
text = NavigationScreen.BLUETOOTH_CONNECTION.stringRes(),
imageText = getStringResource(id = R.string.more_ble_icon_description),
image = Icons.Default.Watch,
imageTint = MoreColors.White
) {
(context as? Activity)?.let {
showNewActivity(it, BLEConnectionActivity::class.java)
}
}
}
}
}
}
Divider(thickness = 1.dp)
}
}
Loading

0 comments on commit 244471c

Please sign in to comment.