mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-06-28 07:25:42 -04:00
feat(connections): detect & inform about disabled Bluetooth / Wi-Fi
Mirror the GPS-disabled-vs-permission-denied distinction for the BLE and network transports: - core/ui: add isBluetoothDisabled() (adapter off) + isWifiUnavailable() (no Wi-Fi/Ethernet), plus rememberOpenBluetoothSettings()/rememberOpenWifiSettings() — alongside the existing isGpsDisabled()/rememberOpenLocationSettings() - extract a reusable RecoveryCard from PermissionRecoveryCard (errorContainer message box + one recovery action); PermissionRecoveryCard now delegates to it - ConnectionsScreen: when a transport's permission is granted but its adapter is off, show an inline recovery banner. The BLE scan toggle routes to Bluetooth settings when the radio is off (scanning can't work); the network banner is informational (manual TCP can still work off-Wi-Fi) - detection refreshes on ON_RESUME, so the banner clears after the user returns from the adapter settings screen Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
5
.skills/compose-ui/strings-index.txt
generated
5
.skills/compose-ui/strings-index.txt
generated
@@ -110,6 +110,7 @@ blue
|
|||||||
bluetooth
|
bluetooth
|
||||||
bluetooth_available_devices
|
bluetooth_available_devices
|
||||||
bluetooth_config
|
bluetooth_config
|
||||||
|
bluetooth_disabled
|
||||||
bluetooth_enabled
|
bluetooth_enabled
|
||||||
bluetooth_feature_config
|
bluetooth_feature_config
|
||||||
bluetooth_feature_config_description
|
bluetooth_feature_config_description
|
||||||
@@ -1015,10 +1016,13 @@ one_month
|
|||||||
one_week
|
one_week
|
||||||
one_wire_temperature
|
one_wire_temperature
|
||||||
only_favorites
|
only_favorites
|
||||||
|
### OPEN ###
|
||||||
|
open_bluetooth_settings
|
||||||
open_compass
|
open_compass
|
||||||
open_settings
|
open_settings
|
||||||
open_source_description
|
open_source_description
|
||||||
open_source_libraries
|
open_source_libraries
|
||||||
|
open_wifi_settings
|
||||||
options
|
options
|
||||||
orient_north
|
orient_north
|
||||||
### OUTPUT ###
|
### OUTPUT ###
|
||||||
@@ -1537,6 +1541,7 @@ wifi_provisioning
|
|||||||
wifi_qr_code_error
|
wifi_qr_code_error
|
||||||
wifi_qr_code_scan
|
wifi_qr_code_scan
|
||||||
wifi_rssi_threshold_defaults_to_80
|
wifi_rssi_threshold_defaults_to_80
|
||||||
|
wifi_unavailable
|
||||||
### WIND ###
|
### WIND ###
|
||||||
wind
|
wind
|
||||||
wind_direction
|
wind_direction
|
||||||
|
|||||||
@@ -128,6 +128,7 @@
|
|||||||
<string name="bluetooth">Bluetooth</string>
|
<string name="bluetooth">Bluetooth</string>
|
||||||
<string name="bluetooth_available_devices">Available Bluetooth Devices</string>
|
<string name="bluetooth_available_devices">Available Bluetooth Devices</string>
|
||||||
<string name="bluetooth_config">Bluetooth Config</string>
|
<string name="bluetooth_config">Bluetooth Config</string>
|
||||||
|
<string name="bluetooth_disabled">Bluetooth is off. Turn it on to scan for nearby devices.</string>
|
||||||
<string name="bluetooth_enabled">Bluetooth enabled</string>
|
<string name="bluetooth_enabled">Bluetooth enabled</string>
|
||||||
<string name="bluetooth_feature_config">Configuration</string>
|
<string name="bluetooth_feature_config">Configuration</string>
|
||||||
<string name="bluetooth_feature_config_description">Wirelessly manage your device settings and channels.</string>
|
<string name="bluetooth_feature_config_description">Wirelessly manage your device settings and channels.</string>
|
||||||
@@ -1045,10 +1046,13 @@
|
|||||||
<string name="one_week">1W</string>
|
<string name="one_week">1W</string>
|
||||||
<string name="one_wire_temperature">1-Wire Temp</string>
|
<string name="one_wire_temperature">1-Wire Temp</string>
|
||||||
<string name="only_favorites">Only Favorites</string>
|
<string name="only_favorites">Only Favorites</string>
|
||||||
|
<!-- OPEN -->
|
||||||
|
<string name="open_bluetooth_settings">Open Bluetooth settings</string>
|
||||||
<string name="open_compass">Open Compass</string>
|
<string name="open_compass">Open Compass</string>
|
||||||
<string name="open_settings">Open settings</string>
|
<string name="open_settings">Open settings</string>
|
||||||
<string name="open_source_description">Meshtastic is built with the following open source libraries. Tap any library to view its license.</string>
|
<string name="open_source_description">Meshtastic is built with the following open source libraries. Tap any library to view its license.</string>
|
||||||
<string name="open_source_libraries">Open Source Libraries</string>
|
<string name="open_source_libraries">Open Source Libraries</string>
|
||||||
|
<string name="open_wifi_settings">Open Wi-Fi settings</string>
|
||||||
<string name="options">Options</string>
|
<string name="options">Options</string>
|
||||||
<string name="orient_north">Orient north</string>
|
<string name="orient_north">Orient north</string>
|
||||||
<!-- OUTPUT -->
|
<!-- OUTPUT -->
|
||||||
@@ -1582,6 +1586,7 @@
|
|||||||
<string name="wifi_qr_code_error">Invalid WiFi Credential QR code format</string>
|
<string name="wifi_qr_code_error">Invalid WiFi Credential QR code format</string>
|
||||||
<string name="wifi_qr_code_scan">Scan WiFi QR code</string>
|
<string name="wifi_qr_code_scan">Scan WiFi QR code</string>
|
||||||
<string name="wifi_rssi_threshold_defaults_to_80">WiFi RSSI threshold (defaults to -80)</string>
|
<string name="wifi_rssi_threshold_defaults_to_80">WiFi RSSI threshold (defaults to -80)</string>
|
||||||
|
<string name="wifi_unavailable">Not connected to Wi-Fi. Network scan may not find nearby devices.</string>
|
||||||
<!-- WIND -->
|
<!-- WIND -->
|
||||||
<string name="wind">Wind</string>
|
<string name="wind">Wind</string>
|
||||||
<string name="wind_direction">Wind Dir</string>
|
<string name="wind_direction">Wind Dir</string>
|
||||||
|
|||||||
@@ -18,9 +18,13 @@
|
|||||||
|
|
||||||
package org.meshtastic.core.ui.util
|
package org.meshtastic.core.ui.util
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothManager
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import androidx.activity.compose.LocalActivity
|
import androidx.activity.compose.LocalActivity
|
||||||
@@ -215,6 +219,62 @@ actual fun isGpsDisabled(): Boolean {
|
|||||||
return rememberOnResumeState { context.gpsDisabled() }
|
return rememberOnResumeState { context.gpsDisabled() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun rememberOpenBluetoothSettings(): () -> Unit {
|
||||||
|
val context = LocalContext.current
|
||||||
|
return remember(context) {
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
context.startActivity(
|
||||||
|
Intent(Settings.ACTION_BLUETOOTH_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||||
|
)
|
||||||
|
} catch (ex: ActivityNotFoundException) {
|
||||||
|
Logger.w(ex) { "No Bluetooth settings activity available" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun rememberOpenWifiSettings(): () -> Unit {
|
||||||
|
val context = LocalContext.current
|
||||||
|
return remember(context) {
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
context.startActivity(Intent(Settings.ACTION_WIFI_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||||
|
} catch (ex: ActivityNotFoundException) {
|
||||||
|
Logger.w(ex) { "No Wi-Fi settings activity available" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun isBluetoothDisabled(): Boolean {
|
||||||
|
val context = LocalContext.current
|
||||||
|
return rememberOnResumeState {
|
||||||
|
// adapter == null means the device has no Bluetooth at all — not "disabled", so don't nag.
|
||||||
|
val adapter = (context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter
|
||||||
|
adapter != null && !adapter.isEnabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
actual fun isWifiUnavailable(): Boolean {
|
||||||
|
val context = LocalContext.current
|
||||||
|
return rememberOnResumeState {
|
||||||
|
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
|
||||||
|
val capabilities = cm?.activeNetwork?.let { cm.getNetworkCapabilities(it) }
|
||||||
|
val onLocalNetwork =
|
||||||
|
capabilities != null &&
|
||||||
|
(
|
||||||
|
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
|
||||||
|
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
|
||||||
|
)
|
||||||
|
!onLocalNetwork
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
actual fun rememberOpenAppSettings(): () -> Unit {
|
actual fun rememberOpenAppSettings(): () -> Unit {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
import org.meshtastic.core.resources.Res
|
import org.meshtastic.core.resources.Res
|
||||||
@@ -43,26 +44,23 @@ import org.meshtastic.core.ui.util.PermissionStatus
|
|||||||
import org.meshtastic.core.ui.util.PermissionUiState
|
import org.meshtastic.core.ui.util.PermissionUiState
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A reusable error-state card for a missing runtime permission. Generalizes the compass warning/recovery pattern so
|
* A reusable error-state card: an `errorContainer` message box plus one full-width recovery action. Generalizes the
|
||||||
* every feature presents context plus a single, context-correct recovery action:
|
* compass warning/recovery pattern so any feature can present context plus a single corrective action (request a
|
||||||
* - [PermissionStatus.NOT_REQUESTED] / [PermissionStatus.DENIED_CAN_RETRY] — shows a "Grant permission" button that
|
* permission, open Bluetooth/Wi-Fi/app settings, etc.).
|
||||||
* re-launches the in-context request.
|
|
||||||
* - [PermissionStatus.PERMANENTLY_DENIED] — shows an "Open settings" button (user-initiated recovery) because the
|
|
||||||
* system will no longer show the dialog.
|
|
||||||
* - [PermissionStatus.GRANTED] — renders nothing.
|
|
||||||
*
|
*
|
||||||
* @param rationale a feature-specific explanation of why the permission is needed.
|
* @param message the user-facing explanation of what is wrong.
|
||||||
|
* @param actionLabel the recovery button label.
|
||||||
|
* @param onAction invoked when the recovery button is tapped.
|
||||||
|
* @param actionIcon optional leading icon for the recovery button.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
internal fun PermissionRecoveryCard(
|
fun RecoveryCard(
|
||||||
status: PermissionStatus,
|
message: String,
|
||||||
rationale: String,
|
actionLabel: String,
|
||||||
onRequest: () -> Unit,
|
onAction: () -> Unit,
|
||||||
onOpenSettings: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
actionIcon: ImageVector? = null,
|
||||||
) {
|
) {
|
||||||
if (status == PermissionStatus.GRANTED) return
|
|
||||||
|
|
||||||
Column(modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
@@ -81,27 +79,61 @@ internal fun PermissionRecoveryCard(
|
|||||||
tint = MaterialTheme.colorScheme.onErrorContainer,
|
tint = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = rationale,
|
text = message,
|
||||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status == PermissionStatus.PERMANENTLY_DENIED) {
|
Button(onClick = onAction, modifier = Modifier.fillMaxWidth()) {
|
||||||
Button(onClick = onOpenSettings, modifier = Modifier.fillMaxWidth()) {
|
if (actionIcon != null) {
|
||||||
Icon(imageVector = MeshtasticIcons.AppSettingsAlt, contentDescription = null)
|
Icon(imageVector = actionIcon, contentDescription = null)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(text = stringResource(Res.string.open_settings))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Button(onClick = onRequest, modifier = Modifier.fillMaxWidth()) {
|
|
||||||
Text(text = stringResource(Res.string.grant_permission))
|
|
||||||
}
|
}
|
||||||
|
Text(text = actionLabel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [RecoveryCard] specialized for a missing runtime permission, presenting a context-correct recovery action:
|
||||||
|
* - [PermissionStatus.NOT_REQUESTED] / [PermissionStatus.DENIED_CAN_RETRY] — shows a "Grant permission" button that
|
||||||
|
* re-launches the in-context request.
|
||||||
|
* - [PermissionStatus.PERMANENTLY_DENIED] — shows an "Open settings" button (user-initiated recovery) because the
|
||||||
|
* system will no longer show the dialog.
|
||||||
|
* - [PermissionStatus.GRANTED] — renders nothing.
|
||||||
|
*
|
||||||
|
* @param rationale a feature-specific explanation of why the permission is needed.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
internal fun PermissionRecoveryCard(
|
||||||
|
status: PermissionStatus,
|
||||||
|
rationale: String,
|
||||||
|
onRequest: () -> Unit,
|
||||||
|
onOpenSettings: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
if (status == PermissionStatus.GRANTED) return
|
||||||
|
|
||||||
|
if (status == PermissionStatus.PERMANENTLY_DENIED) {
|
||||||
|
RecoveryCard(
|
||||||
|
message = rationale,
|
||||||
|
actionLabel = stringResource(Res.string.open_settings),
|
||||||
|
onAction = onOpenSettings,
|
||||||
|
modifier = modifier,
|
||||||
|
actionIcon = MeshtasticIcons.AppSettingsAlt,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
RecoveryCard(
|
||||||
|
message = rationale,
|
||||||
|
actionLabel = stringResource(Res.string.grant_permission),
|
||||||
|
onAction = onRequest,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Convenience overload that reads the status and actions directly from a [PermissionUiState]. */
|
/** Convenience overload that reads the status and actions directly from a [PermissionUiState]. */
|
||||||
@Composable
|
@Composable
|
||||||
fun PermissionRecoveryCard(state: PermissionUiState, rationale: String, modifier: Modifier = Modifier) {
|
fun PermissionRecoveryCard(state: PermissionUiState, rationale: String, modifier: Modifier = Modifier) {
|
||||||
|
|||||||
@@ -58,12 +58,30 @@ expect fun rememberSaveFileLauncher(
|
|||||||
/** Returns a launcher to open the platform's location settings. */
|
/** Returns a launcher to open the platform's location settings. */
|
||||||
@Composable expect fun rememberOpenLocationSettings(): () -> Unit
|
@Composable expect fun rememberOpenLocationSettings(): () -> Unit
|
||||||
|
|
||||||
|
/** Returns a launcher to open the platform's Bluetooth settings. */
|
||||||
|
@Composable expect fun rememberOpenBluetoothSettings(): () -> Unit
|
||||||
|
|
||||||
|
/** Returns a launcher to open the platform's Wi-Fi settings. */
|
||||||
|
@Composable expect fun rememberOpenWifiSettings(): () -> Unit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether GPS/location services are currently disabled at the system level. Always `false` on platforms where
|
* Returns whether GPS/location services are currently disabled at the system level. Always `false` on platforms where
|
||||||
* this concept doesn't apply.
|
* this concept doesn't apply.
|
||||||
*/
|
*/
|
||||||
@Composable expect fun isGpsDisabled(): Boolean
|
@Composable expect fun isGpsDisabled(): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether Bluetooth is currently turned off at the system level (the adapter exists but is disabled). Always
|
||||||
|
* `false` on devices without Bluetooth and on platforms where the concept doesn't apply.
|
||||||
|
*/
|
||||||
|
@Composable expect fun isBluetoothDisabled(): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the device currently lacks a local-network-capable connection (no active Wi-Fi or Ethernet). NSD/mDNS
|
||||||
|
* discovery needs a LAN, so this surfaces the "connect to Wi-Fi" hint. Always `false` where the concept doesn't apply.
|
||||||
|
*/
|
||||||
|
@Composable expect fun isWifiUnavailable(): Boolean
|
||||||
|
|
||||||
/** Returns a function that opens this app's system settings page (where the user can change any permission). */
|
/** Returns a function that opens this app's system settings page (where the user can change any permission). */
|
||||||
@Composable expect fun rememberOpenAppSettings(): () -> Unit
|
@Composable expect fun rememberOpenAppSettings(): () -> Unit
|
||||||
|
|
||||||
|
|||||||
@@ -52,8 +52,16 @@ actual fun rememberOpenFileLauncher(onUriReceived: (CommonUri?) -> Unit): (mimeT
|
|||||||
|
|
||||||
@Composable actual fun rememberOpenLocationSettings(): () -> Unit = {}
|
@Composable actual fun rememberOpenLocationSettings(): () -> Unit = {}
|
||||||
|
|
||||||
|
@Composable actual fun rememberOpenBluetoothSettings(): () -> Unit = {}
|
||||||
|
|
||||||
|
@Composable actual fun rememberOpenWifiSettings(): () -> Unit = {}
|
||||||
|
|
||||||
@Composable actual fun isGpsDisabled(): Boolean = false
|
@Composable actual fun isGpsDisabled(): Boolean = false
|
||||||
|
|
||||||
|
@Composable actual fun isBluetoothDisabled(): Boolean = false
|
||||||
|
|
||||||
|
@Composable actual fun isWifiUnavailable(): Boolean = false
|
||||||
|
|
||||||
@Composable actual fun SetScreenBrightness(brightness: Float) {}
|
@Composable actual fun SetScreenBrightness(brightness: Float) {}
|
||||||
|
|
||||||
@Composable actual fun rememberOpenAppSettings(): () -> Unit = {}
|
@Composable actual fun rememberOpenAppSettings(): () -> Unit = {}
|
||||||
|
|||||||
@@ -118,9 +118,25 @@ actual fun KeepScreenOn(enabled: Boolean) {
|
|||||||
@Composable
|
@Composable
|
||||||
actual fun rememberOpenLocationSettings(): () -> Unit = { Logger.w { "Location settings not implemented on Desktop" } }
|
actual fun rememberOpenLocationSettings(): () -> Unit = { Logger.w { "Location settings not implemented on Desktop" } }
|
||||||
|
|
||||||
|
/** JVM stub — Bluetooth settings are not available on Desktop. */
|
||||||
|
@Composable
|
||||||
|
actual fun rememberOpenBluetoothSettings(): () -> Unit = {
|
||||||
|
Logger.w { "Bluetooth settings not available on JVM/Desktop" }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** JVM stub — Wi-Fi settings are not available on Desktop. */
|
||||||
|
@Composable
|
||||||
|
actual fun rememberOpenWifiSettings(): () -> Unit = { Logger.w { "Wi-Fi settings not available on JVM/Desktop" } }
|
||||||
|
|
||||||
/** JVM — GPS is never disabled on Desktop (concept doesn't apply). */
|
/** JVM — GPS is never disabled on Desktop (concept doesn't apply). */
|
||||||
@Composable actual fun isGpsDisabled(): Boolean = false
|
@Composable actual fun isGpsDisabled(): Boolean = false
|
||||||
|
|
||||||
|
/** JVM — Bluetooth adapter state is not surfaced on Desktop. */
|
||||||
|
@Composable actual fun isBluetoothDisabled(): Boolean = false
|
||||||
|
|
||||||
|
/** JVM — local-network availability is not gated on Desktop. */
|
||||||
|
@Composable actual fun isWifiUnavailable(): Boolean = false
|
||||||
|
|
||||||
/** JVM stub — app settings are not available on Desktop. */
|
/** JVM stub — app settings are not available on Desktop. */
|
||||||
@Composable
|
@Composable
|
||||||
actual fun rememberOpenAppSettings(): () -> Unit = { Logger.w { "App settings not available on JVM/Desktop" } }
|
actual fun rememberOpenAppSettings(): () -> Unit = { Logger.w { "App settings not available on JVM/Desktop" } }
|
||||||
|
|||||||
@@ -54,19 +54,29 @@ import org.meshtastic.core.model.ConnectionState
|
|||||||
import org.meshtastic.core.navigation.Route
|
import org.meshtastic.core.navigation.Route
|
||||||
import org.meshtastic.core.navigation.SettingsRoute
|
import org.meshtastic.core.navigation.SettingsRoute
|
||||||
import org.meshtastic.core.resources.Res
|
import org.meshtastic.core.resources.Res
|
||||||
|
import org.meshtastic.core.resources.bluetooth_disabled
|
||||||
import org.meshtastic.core.resources.connections
|
import org.meshtastic.core.resources.connections
|
||||||
import org.meshtastic.core.resources.no_device_selected
|
import org.meshtastic.core.resources.no_device_selected
|
||||||
|
import org.meshtastic.core.resources.open_bluetooth_settings
|
||||||
|
import org.meshtastic.core.resources.open_wifi_settings
|
||||||
import org.meshtastic.core.resources.set_your_region
|
import org.meshtastic.core.resources.set_your_region
|
||||||
import org.meshtastic.core.resources.unknown_device
|
import org.meshtastic.core.resources.unknown_device
|
||||||
|
import org.meshtastic.core.resources.wifi_unavailable
|
||||||
import org.meshtastic.core.ui.component.AdaptiveTwoPane
|
import org.meshtastic.core.ui.component.AdaptiveTwoPane
|
||||||
import org.meshtastic.core.ui.component.ListItem
|
import org.meshtastic.core.ui.component.ListItem
|
||||||
import org.meshtastic.core.ui.component.MainAppBar
|
import org.meshtastic.core.ui.component.MainAppBar
|
||||||
|
import org.meshtastic.core.ui.component.RecoveryCard
|
||||||
|
import org.meshtastic.core.ui.icon.Bluetooth
|
||||||
import org.meshtastic.core.ui.icon.Language
|
import org.meshtastic.core.ui.icon.Language
|
||||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||||
import org.meshtastic.core.ui.icon.NoDevice
|
import org.meshtastic.core.ui.icon.NoDevice
|
||||||
import org.meshtastic.core.ui.util.PermissionStatus
|
import org.meshtastic.core.ui.util.PermissionStatus
|
||||||
|
import org.meshtastic.core.ui.util.isBluetoothDisabled
|
||||||
|
import org.meshtastic.core.ui.util.isWifiUnavailable
|
||||||
import org.meshtastic.core.ui.util.rememberBluetoothPermissionState
|
import org.meshtastic.core.ui.util.rememberBluetoothPermissionState
|
||||||
import org.meshtastic.core.ui.util.rememberLocalNetworkPermissionState
|
import org.meshtastic.core.ui.util.rememberLocalNetworkPermissionState
|
||||||
|
import org.meshtastic.core.ui.util.rememberOpenBluetoothSettings
|
||||||
|
import org.meshtastic.core.ui.util.rememberOpenWifiSettings
|
||||||
import org.meshtastic.core.ui.viewmodel.ConnectionStatus
|
import org.meshtastic.core.ui.viewmodel.ConnectionStatus
|
||||||
import org.meshtastic.core.ui.viewmodel.ConnectionsViewModel
|
import org.meshtastic.core.ui.viewmodel.ConnectionsViewModel
|
||||||
import org.meshtastic.feature.connections.MOCK_DEVICE_PREFIX
|
import org.meshtastic.feature.connections.MOCK_DEVICE_PREFIX
|
||||||
@@ -131,6 +141,13 @@ fun ConnectionsScreen(
|
|||||||
val localNetworkPermission = rememberLocalNetworkPermissionState()
|
val localNetworkPermission = rememberLocalNetworkPermissionState()
|
||||||
val bluetoothPermission = rememberBluetoothPermissionState()
|
val bluetoothPermission = rememberBluetoothPermissionState()
|
||||||
|
|
||||||
|
// Adapter-state, distinct from permission state: a permission can be granted while Bluetooth is off or the device
|
||||||
|
// is off Wi-Fi. Detected separately so the UI can route to the adapter's settings rather than re-prompting.
|
||||||
|
val bluetoothDisabled = isBluetoothDisabled()
|
||||||
|
val wifiUnavailable = isWifiUnavailable()
|
||||||
|
val openBluetoothSettings = rememberOpenBluetoothSettings()
|
||||||
|
val openWifiSettings = rememberOpenWifiSettings()
|
||||||
|
|
||||||
// Auto-start BLE scan when the screen is visible (lifecycle ≥ STARTED) and the user has previously opted in.
|
// Auto-start BLE scan when the screen is visible (lifecycle ≥ STARTED) and the user has previously opted in.
|
||||||
// LifecycleStartEffect stops scanning on ON_STOP (app backgrounded) and restarts on ON_START — preventing
|
// LifecycleStartEffect stops scanning on ON_STOP (app backgrounded) and restarts on ON_START — preventing
|
||||||
// continuous background BLE radio usage that drains the battery.
|
// continuous background BLE radio usage that drains the battery.
|
||||||
@@ -274,6 +291,24 @@ fun ConnectionsScreen(
|
|||||||
onToggleNetwork = { scanModel.setShowNetworkTransport(!showNetworkTransport) },
|
onToggleNetwork = { scanModel.setShowNetworkTransport(!showNetworkTransport) },
|
||||||
onToggleUsb = { scanModel.setShowUsbTransport(!showUsbTransport) },
|
onToggleUsb = { scanModel.setShowUsbTransport(!showUsbTransport) },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Adapter-off hints: shown only when the relevant permission is granted but the radio/network
|
||||||
|
// is unavailable, so they don't overlap the permission-recovery flow on the scan toggles.
|
||||||
|
if (showBleTransport && bluetoothPermission.isGranted && bluetoothDisabled) {
|
||||||
|
RecoveryCard(
|
||||||
|
message = stringResource(Res.string.bluetooth_disabled),
|
||||||
|
actionLabel = stringResource(Res.string.open_bluetooth_settings),
|
||||||
|
onAction = openBluetoothSettings,
|
||||||
|
actionIcon = MeshtasticIcons.Bluetooth,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (showNetworkTransport && localNetworkPermission.isGranted && wifiUnavailable) {
|
||||||
|
RecoveryCard(
|
||||||
|
message = stringResource(Res.string.wifi_unavailable),
|
||||||
|
actionLabel = stringResource(Res.string.open_wifi_settings),
|
||||||
|
onAction = openWifiSettings,
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
second = {
|
second = {
|
||||||
// ── Unified device list ──
|
// ── Unified device list ──
|
||||||
@@ -293,7 +328,13 @@ fun ConnectionsScreen(
|
|||||||
onSelectDevice = { scanModel.onSelected(it) },
|
onSelectDevice = { scanModel.onSelected(it) },
|
||||||
onToggleBleScan = {
|
onToggleBleScan = {
|
||||||
when {
|
when {
|
||||||
isBleScanning || bluetoothPermission.isGranted -> scanModel.toggleBleScan()
|
// Always allow stopping an in-progress scan.
|
||||||
|
isBleScanning -> scanModel.toggleBleScan()
|
||||||
|
|
||||||
|
// Granted but the radio is off — scanning can't work, so open BT settings.
|
||||||
|
bluetoothPermission.isGranted && bluetoothDisabled -> openBluetoothSettings()
|
||||||
|
|
||||||
|
bluetoothPermission.isGranted -> scanModel.toggleBleScan()
|
||||||
|
|
||||||
// Permanently denied: the system won't prompt again, so send to settings.
|
// Permanently denied: the system won't prompt again, so send to settings.
|
||||||
bluetoothPermission.status == PermissionStatus.PERMANENTLY_DENIED ->
|
bluetoothPermission.status == PermissionStatus.PERMANENTLY_DENIED ->
|
||||||
|
|||||||
Reference in New Issue
Block a user