@@ -13,6 +13,7 @@ import ai.openclaw.app.ui.design.ClawListItem
1313import ai.openclaw.app.ui.design.ClawPanel
1414import ai.openclaw.app.ui.design.ClawPrimaryButton
1515import ai.openclaw.app.ui.design.ClawScaffold
16+ import ai.openclaw.app.ui.design.ClawSecondaryButton
1617import ai.openclaw.app.ui.design.ClawStatus
1718import ai.openclaw.app.ui.design.ClawStatusPill
1819import ai.openclaw.app.ui.design.ClawTextField
@@ -25,6 +26,7 @@ import android.content.Intent
2526import android.content.pm.PackageManager
2627import android.hardware.Sensor
2728import android.hardware.SensorManager
29+ import android.net.Uri
2830import android.os.Build
2931import android.os.SystemClock
3032import android.provider.Settings
@@ -79,6 +81,7 @@ import androidx.compose.material.icons.filled.QrCode2
7981import androidx.compose.material.icons.filled.Refresh
8082import androidx.compose.material.icons.filled.Security
8183import androidx.compose.material.icons.filled.Sensors
84+ import androidx.compose.material.icons.filled.Settings
8285import androidx.compose.material.icons.filled.WifiTethering
8386import androidx.compose.material3.AlertDialog
8487import androidx.compose.material3.Icon
@@ -181,6 +184,14 @@ fun OnboardingFlow(
181184 val qrScanner = remember(context, qrScannerOptions) { GmsBarcodeScanning .getClient(context, qrScannerOptions) }
182185
183186 val permissionState = rememberPermissionState(context = context, viewModel = viewModel)
187+ val cameraPermissionLauncher =
188+ rememberLauncherForActivityResult(ActivityResultContracts .RequestPermission ()) { granted ->
189+ setupError =
190+ handleGatewayScannerPermissionResult(
191+ granted = granted,
192+ updateCameraPermission = permissionState.updateCameraPermission,
193+ )
194+ }
184195
185196 LaunchedEffect (startAtGatewaySetup) {
186197 if (startAtGatewaySetup) {
@@ -254,6 +265,7 @@ fun OnboardingFlow(
254265 discoveryStatusText = discoveryStatusText,
255266 discoveryStarted = runtimeInitialized,
256267 error = setupError,
268+ showCameraSettingsAction = gatewaySetupErrorShowsCameraSettings(setupError),
257269 onBack = { step = OnboardingStep .Welcome },
258270 onScan = {
259271 setupError = null
@@ -270,7 +282,16 @@ fun OnboardingFlow(
270282 return @addOnSuccessListener
271283 }
272284 setupCode = scanned.setupCode
273- }.addOnFailureListener { setupError = " Could not open the scanner." }
285+ }.addOnFailureListener {
286+ val cameraPermissionGranted = hasPermission(context, Manifest .permission.CAMERA )
287+ setupError = gatewayScannerFailureMessage(cameraPermissionGranted)
288+ if (! cameraPermissionGranted) {
289+ cameraPermissionLauncher.launch(Manifest .permission.CAMERA )
290+ }
291+ }
292+ },
293+ onOpenCameraSettings = {
294+ openAppSettings(context)
274295 },
275296 onSetupCodeChange = {
276297 setupCode = it
@@ -493,8 +514,10 @@ private fun GatewaySetupScreen(
493514 discoveryStatusText : String ,
494515 discoveryStarted : Boolean ,
495516 error : String? ,
517+ showCameraSettingsAction : Boolean ,
496518 onBack : () -> Unit ,
497519 onScan : () -> Unit ,
520+ onOpenCameraSettings : () -> Unit ,
498521 onSetupCodeChange : (String ) -> Unit ,
499522 onManualHostChange : (String ) -> Unit ,
500523 onManualPortChange : (String ) -> Unit ,
@@ -566,6 +589,18 @@ private fun GatewaySetupScreen(
566589 ClawErrorState (
567590 title = " Setup code issue" ,
568591 body = message,
592+ action =
593+ if (showCameraSettingsAction) {
594+ {
595+ ClawSecondaryButton (
596+ text = " Open app settings" ,
597+ icon = Icons .Default .Settings ,
598+ onClick = onOpenCameraSettings,
599+ )
600+ }
601+ } else {
602+ null
603+ },
569604 )
570605 }
571606 }
@@ -1295,6 +1330,7 @@ private data class PermissionRowModel(
12951330private class PermissionState (
12961331 val rows : List <PermissionRowModel >,
12971332 val applyToViewModel : () -> Unit ,
1333+ val updateCameraPermission : (Boolean ) -> Unit ,
12981334)
12991335
13001336/* * Onboarding finishes only after the gateway resolves node capability approval. */
@@ -1316,6 +1352,34 @@ internal fun canFinishOnboarding(
13161352 -> true
13171353 }
13181354
1355+ internal fun gatewayScannerFailureMessage (cameraPermissionGranted : Boolean ): String =
1356+ if (cameraPermissionGranted) {
1357+ " Could not open the scanner. You can paste the setup code instead."
1358+ } else {
1359+ " Camera permission is required to scan setup codes. Grant Camera permission, then try again."
1360+ }
1361+
1362+ internal fun gatewayScannerFailureShowsAppSettings (cameraPermissionGranted : Boolean ): Boolean = ! cameraPermissionGranted
1363+
1364+ internal fun gatewayScannerPermissionResultMessage (granted : Boolean ): String? =
1365+ if (granted) {
1366+ null
1367+ } else {
1368+ " Camera permission is required to scan setup codes. Open Android app settings to grant Camera permission, then try again."
1369+ }
1370+
1371+ internal fun handleGatewayScannerPermissionResult (
1372+ granted : Boolean ,
1373+ updateCameraPermission : (Boolean ) -> Unit ,
1374+ ): String? {
1375+ updateCameraPermission(granted)
1376+ return gatewayScannerPermissionResultMessage(granted)
1377+ }
1378+
1379+ internal fun gatewaySetupErrorShowsCameraSettings (error : String? ): Boolean =
1380+ error == gatewayScannerFailureMessage(cameraPermissionGranted = false ) ||
1381+ error == gatewayScannerPermissionResultMessage(granted = false )
1382+
13191383/* * Builds permission rows and applies granted feature toggles after onboarding. */
13201384@Composable
13211385private fun rememberPermissionState (
@@ -1361,6 +1425,7 @@ private fun rememberPermissionState(
13611425 LifecycleEventObserver { _, event ->
13621426 if (event == Lifecycle .Event .ON_RESUME ) {
13631427 notificationListenerGranted = DeviceNotificationListenerService .isAccessEnabled(context)
1428+ cameraGranted = hasPermission(context, Manifest .permission.CAMERA )
13641429 }
13651430 }
13661431 lifecycleOwner.lifecycle.addObserver(observer)
@@ -1455,6 +1520,9 @@ private fun rememberPermissionState(
14551520 viewModel.setLocationMode(if (locationGranted) LocationMode .WhileUsing else LocationMode .Off )
14561521 viewModel.setNotificationForwardingEnabled(notificationListenerGranted)
14571522 },
1523+ updateCameraPermission = { granted ->
1524+ cameraGranted = granted
1525+ },
14581526 )
14591527}
14601528
@@ -1463,6 +1531,16 @@ private fun hasPermission(
14631531 permission : String ,
14641532): Boolean = ContextCompat .checkSelfPermission(context, permission) == PackageManager .PERMISSION_GRANTED
14651533
1534+ /* * Opens this app's Android settings page for permissions that require system UI. */
1535+ private fun openAppSettings (context : Context ) {
1536+ val intent =
1537+ Intent (
1538+ Settings .ACTION_APPLICATION_DETAILS_SETTINGS ,
1539+ Uri .fromParts(" package" , context.packageName, null ),
1540+ )
1541+ context.startActivity(intent)
1542+ }
1543+
14661544/* * Returns true when Android exposes any motion sensor that can back node motion commands. */
14671545private fun hasMotionCapabilities (context : Context ): Boolean {
14681546 val sensorManager = context.getSystemService(SensorManager ::class .java) ? : return false
0 commit comments