From 6941b6f9df11a927f2c8c58cb1186f6962def8b2 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 19 May 2025 07:03:01 -0500 Subject: [PATCH] fix: M3 bug squashing (#1872) --- .../main/java/com/geeksville/mesh/ui/Main.kt | 4 +- .../java/com/geeksville/mesh/ui/Settings.kt | 447 +++++++++--------- .../mesh/ui/message/components/MessageItem.kt | 13 +- .../com/geeksville/mesh/ui/theme/Theme.kt | 11 - 4 files changed, 224 insertions(+), 251 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/ui/Main.kt b/app/src/main/java/com/geeksville/mesh/ui/Main.kt index 6bb33674f..9f906b994 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Main.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Main.kt @@ -325,11 +325,11 @@ private fun BottomNavigation( visible = topLevelDestination != null, enter = slideInVertically( initialOffsetY = { it / 2 }, - animationSpec = tween(durationMillis = 200), + animationSpec = tween(durationMillis = 50), ), exit = slideOutVertically( targetOffsetY = { it / 2 }, - animationSpec = tween(durationMillis = 200), + animationSpec = tween(durationMillis = 50), ), ) { NavigationBar { diff --git a/app/src/main/java/com/geeksville/mesh/ui/Settings.kt b/app/src/main/java/com/geeksville/mesh/ui/Settings.kt index ec872511e..9ac233dc0 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/Settings.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/Settings.kt @@ -25,7 +25,6 @@ import android.os.Build import android.util.Patterns import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -37,6 +36,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -65,12 +65,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.hilt.navigation.compose.hiltViewModel +import com.geeksville.mesh.BuildConfig import com.geeksville.mesh.ConfigProtos import com.geeksville.mesh.R import com.geeksville.mesh.android.BuildUtils.debug @@ -118,10 +120,7 @@ fun SettingsScreen( val receivingLocationUpdates by uiViewModel.receivingLocationUpdates.collectAsState(false) val context = LocalContext.current val app = (context.applicationContext as GeeksvilleApplication) - val isGooglePlayAvailable = context.isGooglePlayAvailable() val info by uiViewModel.myNodeInfo.collectAsState() - - val isAnalyticsAllowed = app.isAnalyticsAllowed val selectedDevice = scanModel.selectedNotNull val bluetoothEnabled by bluetoothViewModel.enabled.observeAsState() @@ -218,234 +217,166 @@ fun SettingsScreen( } } } - - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp) - .verticalScroll(scrollState) - ) { - // Scan Status Text - Text( - text = scanStatusText.orEmpty(), - fontSize = 14.sp, - textAlign = TextAlign.Start, - modifier = Modifier.fillMaxWidth() - ) - - Spacer(modifier = Modifier.height(8.dp)) - - // Set Region Button - val isConnected = connectionState == MeshService.ConnectionState.CONNECTED - if (isConnected && regionUnset) { - Button( - modifier = Modifier.fillMaxWidth(), - onClick = { - onSetRegion() - } - ) { - Text(stringResource(R.string.set_region)) - } - Spacer(modifier = Modifier.height(16.dp)) - } - - // Device List and Manual Input - Text( - text = stringResource(R.string.device), - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.padding(vertical = 8.dp) - ) - - // Progress bar while scanning - if (scanning) { - LinearProgressIndicator( + Box(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + .verticalScroll(scrollState) + ) { + // Scan Status Text + Text( + text = scanStatusText.orEmpty(), + fontSize = 14.sp, + textAlign = TextAlign.Start, modifier = Modifier.fillMaxWidth() ) - } - Column(modifier = Modifier.selectableGroup()) { - devices.values.forEach { device -> + Spacer(modifier = Modifier.height(8.dp)) + + // Set Region Button + val isConnected = connectionState == MeshService.ConnectionState.CONNECTED + if (isConnected && regionUnset) { + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { + onSetRegion() + } + ) { + Text(stringResource(R.string.set_region)) + } + Spacer(modifier = Modifier.height(16.dp)) + } + + // Device List and Manual Input + Text( + text = stringResource(R.string.device), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(vertical = 8.dp) + ) + + // Progress bar while scanning + if (scanning) { + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth() + ) + } + Column(modifier = Modifier.selectableGroup()) { + devices.values.forEach { device -> + Row( + modifier = Modifier + .fillMaxWidth() + .selectable( + selected = (device.fullAddress == selectedDevice) || + device.fullAddress == "n", + onClick = { + if (!device.bonded) { + uiViewModel.showSnackbar(context.getString(R.string.starting_pairing)) + } + scanModel.onSelected(device) + }, + role = Role.RadioButton, + ) + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = (device.fullAddress == selectedDevice), + onClick = null + ) + Text( + text = device.name, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(start = 16.dp) + ) + } + } + + // Manual IP Address Input Row( modifier = Modifier .fillMaxWidth() .selectable( - selected = (device.fullAddress == selectedDevice), + selected = ("t$manualIpAddress:$manualIpPort" == selectedDevice), onClick = { - if (!device.bonded) { - uiViewModel.showSnackbar(context.getString(R.string.starting_pairing)) + if (manualIpAddress.isIPAddress()) { + scanModel.onSelected( + BTScanModel.DeviceListEntry( + "", + "t$manualIpAddress:$manualIpPort", + true + ) + ) } - scanModel.onSelected(device) - } + }, + enabled = manualIpAddress.isIPAddress(), + role = Role.RadioButton ) - .padding(horizontal = 16.dp), + .padding(8.dp), verticalAlignment = Alignment.CenterVertically ) { RadioButton( - selected = (device.fullAddress == selectedDevice), - onClick = { - if (!device.bonded) { - uiViewModel.showSnackbar(context.getString(R.string.starting_pairing)) - } - scanModel.onSelected(device) - } + selected = ("t$manualIpAddress:$manualIpPort" == selectedDevice), + onClick = null, + enabled = manualIpAddress.isIPAddress() ) - Text( - text = device.name, - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.padding(start = 16.dp) + OutlinedTextField( + value = manualIpAddress, + onValueChange = { manualIpAddress = it }, + label = { Text(stringResource(R.string.ip_address)) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier + .weight(1f) + .padding(start = 16.dp) + ) + OutlinedTextField( + value = manualIpPort, + onValueChange = { + // Only allow numeric input for port + if (it.all { char -> char.isDigit() }) { + manualIpPort = it + } + }, + label = { Text(stringResource(R.string.ip_port)) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier + .weight(weight = 0.3f) + .padding(start = 8.dp) ) } } - // Manual IP Address Input - Row( - modifier = Modifier - .fillMaxWidth() - .selectable( - selected = ("t$manualIpAddress" == selectedDevice), - onClick = { - if (manualIpAddress.isIPAddress()) { - scanModel.onSelected( - BTScanModel.DeviceListEntry( - "", - "t$manualIpAddress:$manualIpPort", - true - ) - ) - } else { - // Optionally show a warning for invalid IP - } - } - ) - .padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = ("t$manualIpAddress:$manualIpPort" == selectedDevice), - onClick = { - if (manualIpAddress.isIPAddress()) { - scanModel.onSelected( - BTScanModel.DeviceListEntry( - "", - "t$manualIpAddress:$manualIpPort", - true - ) - ) - } else { - // Optionally show a warning for invalid IP - } - } - ) - OutlinedTextField( - value = manualIpAddress, - onValueChange = { manualIpAddress = it }, - label = { Text(stringResource(R.string.ip_address)) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier - .weight(weight = 0.7f) - .padding(start = 16.dp) - ) - OutlinedTextField( - value = manualIpPort, - onValueChange = { - // Only allow numeric input for port - if (it.all { char -> char.isDigit() }) { - manualIpPort = it - } - }, - label = { Text(stringResource(R.string.ip_port)) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier - .weight(weight = 0.3f) - .padding(start = 8.dp) - ) - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - // Provide Location Checkbox - Row( - modifier = Modifier - .fillMaxWidth() - .clickable( - enabled = !isGpsDisabled - ) { - val isChecked = !receivingLocationUpdates // Toggle the state - uiViewModel.provideLocation.value = isChecked - if (isChecked && !context.hasLocationPermission()) { - showLocationRationaleDialog = true // Show the Compose dialog - } - if (isChecked) { - uiViewModel.meshService?.startProvideLocation() - } else { - uiViewModel.meshService?.stopProvideLocation() - } - } - .padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Checkbox( - checked = receivingLocationUpdates, - onCheckedChange = { isChecked -> - uiViewModel.provideLocation.value = isChecked - if (isChecked && !context.hasLocationPermission()) { - showLocationRationaleDialog = true - } - if (isChecked) { - uiViewModel.meshService?.startProvideLocation() - } else { - uiViewModel.meshService?.stopProvideLocation() - } - }, - enabled = !isGpsDisabled // Disable if GPS is disabled - ) - Text( - text = stringResource(R.string.provide_location_to_mesh), - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.padding(start = 16.dp) - ) - } - - Spacer(modifier = Modifier.height(16.dp)) - - // Warning Not Paired - val showWarningNotPaired = !devices.any { it.value.bonded } - if (showWarningNotPaired) { - Text( - text = stringResource(R.string.warning_not_paired), - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(horizontal = 16.dp) - ) Spacer(modifier = Modifier.height(16.dp)) - } - if (isAnalyticsAllowed) { - // Analytics Okay Checkbox + // Provide Location Checkbox Row( modifier = Modifier .fillMaxWidth() - .clickable( - enabled = isGooglePlayAvailable, - ) { - val app = (context.applicationContext as GeeksvilleApplication) - app.isAnalyticsAllowed = !app.isAnalyticsAllowed // Toggle the MutableState - } + .toggleable( + value = receivingLocationUpdates, + onValueChange = { checked -> + uiViewModel.provideLocation.value = checked + if (checked && !context.hasLocationPermission()) { + showLocationRationaleDialog = true // Show the Compose dialog + } + if (checked) { + uiViewModel.meshService?.startProvideLocation() + } else { + uiViewModel.meshService?.stopProvideLocation() + } + }, + enabled = !isGpsDisabled + ) .padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically ) { Checkbox( - checked = isAnalyticsAllowed, - onCheckedChange = { isChecked -> - debug("User changed analytics to $isChecked") - (context.applicationContext as GeeksvilleApplication).isAnalyticsAllowed = - isChecked - }, - enabled = isGooglePlayAvailable + checked = receivingLocationUpdates, + onCheckedChange = null, + enabled = !isGpsDisabled // Disable if GPS is disabled ) Text( - text = stringResource(R.string.analytics_okay), + text = stringResource(R.string.provide_location_to_mesh), style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(start = 16.dp) ) @@ -453,42 +384,90 @@ fun SettingsScreen( Spacer(modifier = Modifier.height(16.dp)) - // Report Bug Button - Button( - onClick = { showReportBugDialog = true }, // Set state to show Report Bug dialog - enabled = isAnalyticsAllowed, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - ) { - Text(stringResource(R.string.report_bug)) + // Warning Not Paired + val showWarningNotPaired = !devices.any { it.value.bonded } + if (showWarningNotPaired) { + Text( + text = stringResource(R.string.warning_not_paired), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(horizontal = 16.dp) + ) + Spacer(modifier = Modifier.height(16.dp)) + } + + // Analytics Okay Checkbox + + val isGooglePlayAvailable = app.isGooglePlayAvailable() || BuildConfig.DEBUG + val isAnalyticsAllowed = app.isAnalyticsAllowed && isGooglePlayAvailable + if (isGooglePlayAvailable) { + var loading by remember { mutableStateOf(false) } + LaunchedEffect(isAnalyticsAllowed) { + loading = false + } + Row( + modifier = Modifier + .fillMaxWidth() + .toggleable( + value = isAnalyticsAllowed, + onValueChange = { + debug("User changed analytics to $it") + app.isAnalyticsAllowed = it + loading = true + }, + role = Role.Checkbox, + enabled = isGooglePlayAvailable && !loading + ) + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + enabled = isGooglePlayAvailable, + checked = isAnalyticsAllowed, + onCheckedChange = null + ) + Text( + text = stringResource(R.string.analytics_okay), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(start = 16.dp) + ) + } + Spacer(modifier = Modifier.height(16.dp)) + // Report Bug Button + Button( + onClick = { showReportBugDialog = true }, // Set state to show Report Bug dialog + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + enabled = isAnalyticsAllowed + ) { + Text(stringResource(R.string.report_bug)) + } } } // Floating Action Button (Change Radio) - Box(modifier = Modifier.fillMaxSize()) { - FloatingActionButton( - onClick = { - val bluetoothPermissions = context.getBluetoothPermissions() - if (bluetoothPermissions.isEmpty()) { - // If no permissions needed, trigger the scan directly (or via ViewModel) - scanModel.startScan() + FloatingActionButton( + onClick = { + val bluetoothPermissions = context.getBluetoothPermissions() + if (bluetoothPermissions.isEmpty()) { + // If no permissions needed, trigger the scan directly (or via ViewModel) + scanModel.startScan() + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && + context.findActivity() + .shouldShowRequestPermissionRationale(bluetoothPermissions.first()) + ) { + showBluetoothRationaleDialog = true } else { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && - context.findActivity() - .shouldShowRequestPermissionRationale(bluetoothPermissions.first()) - ) { - showBluetoothRationaleDialog = true - } else { - requestBluetoothPermissionLauncher.launch(bluetoothPermissions) - } + requestBluetoothPermissionLauncher.launch(bluetoothPermissions) } - }, - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(16.dp) - ) { - Icon(Icons.Filled.Add, contentDescription = stringResource(R.string.change_radio)) - } + } + }, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(16.dp) + ) { + Icon(Icons.Filled.Add, contentDescription = stringResource(R.string.change_radio)) } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt b/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt index 9bd76e498..3df353b76 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/message/components/MessageItem.kt @@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.twotone.Cloud import androidx.compose.material.icons.twotone.CloudDone @@ -43,7 +44,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewLightDark @@ -79,7 +79,11 @@ internal fun MessageItem( verticalAlignment = Alignment.CenterVertically, ) { val fromLocal = node.user.id == DataPacket.ID_LOCAL - val messageColor = if (fromLocal) R.color.colorMyMsg else R.color.colorMsg + val messageColor = if (fromLocal) { + MaterialTheme.colorScheme.secondaryContainer + } else { + MaterialTheme.colorScheme.tertiaryContainer + } val (topStart, topEnd) = if (fromLocal) 12.dp to 4.dp else 4.dp to 12.dp val messageModifier = if (fromLocal) { Modifier.padding(start = 48.dp, top = 8.dp, end = 8.dp, bottom = 6.dp) @@ -105,8 +109,9 @@ internal fun MessageItem( ) .then(messageModifier), colors = CardDefaults.cardColors( - containerColor = colorResource(messageColor) - ) + containerColor = messageColor + ), + shape = RoundedCornerShape(topStart, topEnd, bottomStart = 12.dp, bottomEnd = 12.dp) ) { Row( diff --git a/app/src/main/java/com/geeksville/mesh/ui/theme/Theme.kt b/app/src/main/java/com/geeksville/mesh/ui/theme/Theme.kt index 419f70dec..3153ddcf3 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/theme/Theme.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/theme/Theme.kt @@ -19,7 +19,6 @@ package com.geeksville.mesh.ui.theme -import android.app.Activity import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme @@ -29,11 +28,8 @@ import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable -import androidx.compose.runtime.SideEffect import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalView -import androidx.core.view.WindowCompat private val lightScheme = lightColorScheme( primary = primaryLight, @@ -291,13 +287,6 @@ fun AppTheme( darkTheme -> darkScheme else -> lightScheme } - val view = LocalView.current - if (!view.isInEditMode) { - SideEffect { - val window = (view.context as Activity).window - WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme - } - } MaterialTheme( colorScheme = colorScheme,