diff --git a/apps/android/app/src/main/AndroidManifest.xml b/apps/android/app/src/main/AndroidManifest.xml
index 591627eca..436873c2b 100644
--- a/apps/android/app/src/main/AndroidManifest.xml
+++ b/apps/android/app/src/main/AndroidManifest.xml
@@ -11,7 +11,6 @@
android:usesPermissionFlags="neverForLocation" />
-
diff --git a/apps/android/app/src/main/java/ai/openclaw/app/LocationMode.kt b/apps/android/app/src/main/java/ai/openclaw/app/LocationMode.kt
index b673ff270..f06268b4d 100644
--- a/apps/android/app/src/main/java/ai/openclaw/app/LocationMode.kt
+++ b/apps/android/app/src/main/java/ai/openclaw/app/LocationMode.kt
@@ -3,12 +3,12 @@ package ai.openclaw.app
enum class LocationMode(val rawValue: String) {
Off("off"),
WhileUsing("whileUsing"),
- Always("always"),
;
companion object {
fun fromRawValue(raw: String?): LocationMode {
val normalized = raw?.trim()?.lowercase()
+ if (normalized == "always") return WhileUsing
return entries.firstOrNull { it.rawValue.lowercase() == normalized } ?: Off
}
}
diff --git a/apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt b/apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt
index fc821f9fa..bd9f21c8e 100644
--- a/apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt
+++ b/apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt
@@ -82,7 +82,6 @@ class NodeRuntime(context: Context) {
location = location,
json = json,
isForeground = { _isForeground.value },
- locationMode = { locationMode.value },
locationPreciseEnabled = { locationPreciseEnabled.value },
)
diff --git a/apps/android/app/src/main/java/ai/openclaw/app/node/DeviceHandler.kt b/apps/android/app/src/main/java/ai/openclaw/app/node/DeviceHandler.kt
index a19890285..71c23102c 100644
--- a/apps/android/app/src/main/java/ai/openclaw/app/node/DeviceHandler.kt
+++ b/apps/android/app/src/main/java/ai/openclaw/app/node/DeviceHandler.kt
@@ -170,13 +170,6 @@ class DeviceHandler(
promptableWhenDenied = true,
),
)
- put(
- "backgroundLocation",
- permissionStateJson(
- granted = hasPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION),
- promptableWhenDenied = true,
- ),
- )
put(
"sms",
permissionStateJson(
diff --git a/apps/android/app/src/main/java/ai/openclaw/app/node/LocationHandler.kt b/apps/android/app/src/main/java/ai/openclaw/app/node/LocationHandler.kt
index d925fd7eb..014eead66 100644
--- a/apps/android/app/src/main/java/ai/openclaw/app/node/LocationHandler.kt
+++ b/apps/android/app/src/main/java/ai/openclaw/app/node/LocationHandler.kt
@@ -5,7 +5,6 @@ import android.content.Context
import android.content.pm.PackageManager
import android.location.LocationManager
import androidx.core.content.ContextCompat
-import ai.openclaw.app.LocationMode
import ai.openclaw.app.gateway.GatewaySession
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.serialization.json.Json
@@ -17,7 +16,6 @@ class LocationHandler(
private val location: LocationCaptureManager,
private val json: Json,
private val isForeground: () -> Boolean,
- private val locationMode: () -> LocationMode,
private val locationPreciseEnabled: () -> Boolean,
) {
fun hasFineLocationPermission(): Boolean {
@@ -34,19 +32,11 @@ class LocationHandler(
)
}
- fun hasBackgroundLocationPermission(): Boolean {
- return (
- ContextCompat.checkSelfPermission(appContext, Manifest.permission.ACCESS_BACKGROUND_LOCATION) ==
- PackageManager.PERMISSION_GRANTED
- )
- }
-
suspend fun handleLocationGet(paramsJson: String?): GatewaySession.InvokeResult {
- val mode = locationMode()
- if (!isForeground() && mode != LocationMode.Always) {
+ if (!isForeground()) {
return GatewaySession.InvokeResult.error(
code = "LOCATION_BACKGROUND_UNAVAILABLE",
- message = "LOCATION_BACKGROUND_UNAVAILABLE: background location requires Always",
+ message = "LOCATION_BACKGROUND_UNAVAILABLE: location requires OpenClaw to stay open",
)
}
if (!hasFineLocationPermission() && !hasCoarseLocationPermission()) {
@@ -55,12 +45,6 @@ class LocationHandler(
message = "LOCATION_PERMISSION_REQUIRED: grant Location permission",
)
}
- if (!isForeground() && mode == LocationMode.Always && !hasBackgroundLocationPermission()) {
- return GatewaySession.InvokeResult.error(
- code = "LOCATION_PERMISSION_REQUIRED",
- message = "LOCATION_PERMISSION_REQUIRED: enable Always in system Settings",
- )
- }
val (maxAgeMs, timeoutMs, desiredAccuracy) = parseLocationParams(paramsJson)
val preciseEnabled = locationPreciseEnabled()
val accuracy =
diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt
index 5db2a5e6d..738ab4a49 100644
--- a/apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt
+++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt
@@ -1376,7 +1376,7 @@ private fun PermissionsStep(
InlineDivider()
PermissionToggleRow(
title = "Location",
- subtitle = "location.get (while app is open unless set to Always later)",
+ subtitle = "location.get (while app is open)",
checked = enableLocation,
granted = locationGranted,
onCheckedChange = onLocationChange,
diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt
index a58d66f85..8b50f2101 100644
--- a/apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt
+++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt
@@ -114,7 +114,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
viewModel.setCameraEnabled(cameraOk)
}
- var pendingLocationMode by remember { mutableStateOf(null) }
+ var pendingLocationRequest by remember { mutableStateOf(false) }
var pendingPreciseToggle by remember { mutableStateOf(false) }
val locationPermissionLauncher =
@@ -122,8 +122,6 @@ fun SettingsSheet(viewModel: MainViewModel) {
val fineOk = perms[Manifest.permission.ACCESS_FINE_LOCATION] == true
val coarseOk = perms[Manifest.permission.ACCESS_COARSE_LOCATION] == true
val granted = fineOk || coarseOk
- val requestedMode = pendingLocationMode
- pendingLocationMode = null
if (pendingPreciseToggle) {
pendingPreciseToggle = false
@@ -131,21 +129,9 @@ fun SettingsSheet(viewModel: MainViewModel) {
return@rememberLauncherForActivityResult
}
- if (!granted) {
- viewModel.setLocationMode(LocationMode.Off)
- return@rememberLauncherForActivityResult
- }
-
- if (requestedMode != null) {
- viewModel.setLocationMode(requestedMode)
- if (requestedMode == LocationMode.Always) {
- val backgroundOk =
- ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_BACKGROUND_LOCATION) ==
- PackageManager.PERMISSION_GRANTED
- if (!backgroundOk) {
- openAppSettings(context)
- }
- }
+ if (pendingLocationRequest) {
+ pendingLocationRequest = false
+ viewModel.setLocationMode(if (granted) LocationMode.WhileUsing else LocationMode.Off)
}
}
@@ -309,7 +295,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
}
}
- fun requestLocationPermissions(targetMode: LocationMode) {
+ fun requestLocationPermissions() {
val fineOk =
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
@@ -317,17 +303,9 @@ fun SettingsSheet(viewModel: MainViewModel) {
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
if (fineOk || coarseOk) {
- viewModel.setLocationMode(targetMode)
- if (targetMode == LocationMode.Always) {
- val backgroundOk =
- ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_BACKGROUND_LOCATION) ==
- PackageManager.PERMISSION_GRANTED
- if (!backgroundOk) {
- openAppSettings(context)
- }
- }
+ viewModel.setLocationMode(LocationMode.WhileUsing)
} else {
- pendingLocationMode = targetMode
+ pendingLocationRequest = true
locationPermissionLauncher.launch(
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION),
)
@@ -783,20 +761,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
trailingContent = {
RadioButton(
selected = locationMode == LocationMode.WhileUsing,
- onClick = { requestLocationPermissions(LocationMode.WhileUsing) },
- )
- },
- )
- HorizontalDivider(color = mobileBorder)
- ListItem(
- modifier = Modifier.fillMaxWidth(),
- colors = listItemColors,
- headlineContent = { Text("Always", style = mobileHeadline) },
- supportingContent = { Text("Allow background location (requires system permission).", style = mobileCallout) },
- trailingContent = {
- RadioButton(
- selected = locationMode == LocationMode.Always,
- onClick = { requestLocationPermissions(LocationMode.Always) },
+ onClick = { requestLocationPermissions() },
)
},
)
@@ -816,13 +781,6 @@ fun SettingsSheet(viewModel: MainViewModel) {
)
}
}
- item {
- Text(
- "Always may require Android Settings to allow background location.",
- style = mobileCallout,
- color = mobileTextSecondary,
- )
- }
item { HorizontalDivider(color = mobileBorder) }
// Screen
diff --git a/apps/android/app/src/test/java/ai/openclaw/app/node/DeviceHandlerTest.kt b/apps/android/app/src/test/java/ai/openclaw/app/node/DeviceHandlerTest.kt
index 5574baf6e..ab92fb5f3 100644
--- a/apps/android/app/src/test/java/ai/openclaw/app/node/DeviceHandlerTest.kt
+++ b/apps/android/app/src/test/java/ai/openclaw/app/node/DeviceHandlerTest.kt
@@ -87,7 +87,6 @@ class DeviceHandlerTest {
"camera",
"microphone",
"location",
- "backgroundLocation",
"sms",
"notificationListener",
"notifications",
diff --git a/docs/nodes/location-command.md b/docs/nodes/location-command.md
index 6ba3f61ec..ddaf05c35 100644
--- a/docs/nodes/location-command.md
+++ b/docs/nodes/location-command.md
@@ -1,8 +1,8 @@
---
-summary: "Location command for nodes (location.get), permission modes, and background behavior"
+summary: "Location command for nodes (location.get), permission modes, and Android foreground behavior"
read_when:
- Adding location node support or permissions UI
- - Designing background location + push flows
+ - Designing Android location permissions or foreground behavior
title: "Location Command"
---
@@ -12,15 +12,15 @@ title: "Location Command"
- `location.get` is a node command (via `node.invoke`).
- Off by default.
-- Settings use a selector: Off / While Using / Always.
+- Android app settings use a selector: Off / While Using.
- Separate toggle: Precise Location.
## Why a selector (not just a switch)
OS permissions are multi-level. We can expose a selector in-app, but the OS still decides the actual grant.
-- iOS/macOS: user can choose **While Using** or **Always** in system prompts/Settings. App can request upgrade, but OS may require Settings.
-- Android: background location is a separate permission; on Android 10+ it often requires a Settings flow.
+- iOS/macOS may expose **While Using** or **Always** in system prompts/Settings.
+- Android app currently supports foreground location only.
- Precise location is a separate grant (iOS 14+ “Precise”, Android “fine” vs “coarse”).
Selector in UI drives our requested mode; actual grant lives in OS settings.
@@ -29,13 +29,12 @@ Selector in UI drives our requested mode; actual grant lives in OS settings.
Per node device:
-- `location.enabledMode`: `off | whileUsing | always`
+- `location.enabledMode`: `off | whileUsing`
- `location.preciseEnabled`: bool
UI behavior:
- Selecting `whileUsing` requests foreground permission.
-- Selecting `always` first ensures `whileUsing`, then requests background (or sends user to Settings if required).
- If OS denies requested level, revert to the highest granted level and show status.
## Permissions mapping (node.permissions)
@@ -80,24 +79,11 @@ Errors (stable codes):
- `LOCATION_TIMEOUT`: no fix in time.
- `LOCATION_UNAVAILABLE`: system failure / no providers.
-## Background behavior (future)
+## Background behavior
-Goal: model can request location even when node is backgrounded, but only when:
-
-- User selected **Always**.
-- OS grants background location.
-- App is allowed to run in background for location (iOS background mode / Android foreground service or special allowance).
-
-Push-triggered flow (future):
-
-1. Gateway sends a push to the node (silent push or FCM data).
-2. Node wakes briefly and requests location from the device.
-3. Node forwards payload to Gateway.
-
-Notes:
-
-- iOS: Always permission + background location mode required. Silent push may be throttled; expect intermittent failures.
-- Android: background location may require a foreground service; otherwise, expect denial.
+- Android app denies `location.get` while backgrounded.
+- Keep OpenClaw open when requesting location on Android.
+- Other node platforms may differ.
## Model/tooling integration
@@ -109,5 +95,4 @@ Notes:
- Off: “Location sharing is disabled.”
- While Using: “Only when OpenClaw is open.”
-- Always: “Allow background location. Requires system permission.”
- Precise: “Use precise GPS location. Toggle off to share approximate location.”