diff --git a/apps/.i18n/native-source.json b/apps/.i18n/native-source.json
index d56b32573896..1897d78a6056 100644
--- a/apps/.i18n/native-source.json
+++ b/apps/.i18n/native-source.json
@@ -563,7 +563,7 @@
},
{
"kind": "ui-call",
- "line": 218,
+ "line": 216,
"path": "apps/android/app/src/main/java/ai/openclaw/app/MainActivity.kt",
"source": "Too many shares are waiting to be added.",
"surface": "android",
@@ -571,7 +571,7 @@
},
{
"kind": "ui-named-argument",
- "line": 341,
+ "line": 376,
"path": "apps/android/app/src/main/java/ai/openclaw/app/MainActivity.kt",
"source": "OPENCLAW",
"surface": "android",
@@ -579,7 +579,7 @@
},
{
"kind": "ui-state-text",
- "line": 478,
+ "line": 504,
"path": "apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt",
"source": "Searching…",
"surface": "android",
@@ -587,7 +587,7 @@
},
{
"kind": "ui-state-text",
- "line": 614,
+ "line": 640,
"path": "apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt",
"source": "Mic off",
"surface": "android",
@@ -595,7 +595,7 @@
},
{
"kind": "ui-state-text",
- "line": 628,
+ "line": 654,
"path": "apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt",
"source": "Off",
"surface": "android",
@@ -731,7 +731,7 @@
},
{
"kind": "ui-call",
- "line": 184,
+ "line": 186,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Resolution outcome unknown. Actions stay disabled until the Gateway record is verified.",
"surface": "android",
@@ -739,7 +739,7 @@
},
{
"kind": "ui-call",
- "line": 186,
+ "line": 188,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "The Gateway still shows this approval as pending. Review it before trying again.",
"surface": "android",
@@ -747,7 +747,7 @@
},
{
"kind": "ui-call",
- "line": 188,
+ "line": 190,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Could not load approval details. Refresh and try again.",
"surface": "android",
@@ -755,7 +755,7 @@
},
{
"kind": "ui-call",
- "line": 190,
+ "line": 192,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Could not load approvals.",
"surface": "android",
@@ -763,7 +763,7 @@
},
{
"kind": "ui-call",
- "line": 192,
+ "line": 194,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Could not resolve approval. Refresh and try again.",
"surface": "android",
@@ -771,7 +771,7 @@
},
{
"kind": "ui-call",
- "line": 289,
+ "line": 321,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Device approved.",
"surface": "android",
@@ -779,7 +779,7 @@
},
{
"kind": "ui-call",
- "line": 290,
+ "line": 322,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Pairing request rejected.",
"surface": "android",
@@ -787,7 +787,7 @@
},
{
"kind": "ui-call",
- "line": 291,
+ "line": 323,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Paired device removed.",
"surface": "android",
@@ -795,7 +795,7 @@
},
{
"kind": "ui-call",
- "line": 352,
+ "line": 384,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Proposal applied.",
"surface": "android",
@@ -803,7 +803,7 @@
},
{
"kind": "ui-call",
- "line": 352,
+ "line": 384,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "apply",
"surface": "android",
@@ -811,7 +811,7 @@
},
{
"kind": "ui-call",
- "line": 353,
+ "line": 385,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Proposal rejected.",
"surface": "android",
@@ -819,7 +819,7 @@
},
{
"kind": "ui-call",
- "line": 353,
+ "line": 385,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "reject",
"surface": "android",
@@ -827,7 +827,7 @@
},
{
"kind": "ui-call",
- "line": 354,
+ "line": 386,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Proposal quarantined.",
"surface": "android",
@@ -835,7 +835,7 @@
},
{
"kind": "ui-call",
- "line": 354,
+ "line": 386,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "quarantine",
"surface": "android",
@@ -843,7 +843,7 @@
},
{
"kind": "ui-call",
- "line": 361,
+ "line": 393,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "unknown",
"surface": "android",
@@ -851,7 +851,7 @@
},
{
"kind": "ui-call",
- "line": 362,
+ "line": 394,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Gateway returned status '$statusLabel' after ${action.verb}.",
"surface": "android",
@@ -859,7 +859,7 @@
},
{
"kind": "ui-call",
- "line": 370,
+ "line": 402,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Could not ${action.verb} Skill Workshop proposal.",
"surface": "android",
@@ -867,7 +867,7 @@
},
{
"kind": "ui-call",
- "line": 552,
+ "line": 584,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Connected (node offline)",
"surface": "android",
@@ -875,7 +875,7 @@
},
{
"kind": "ui-call",
- "line": 553,
+ "line": 585,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Connected (operator offline)",
"surface": "android",
@@ -883,7 +883,7 @@
},
{
"kind": "ui-call",
- "line": 555,
+ "line": 587,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Reconnecting…",
"surface": "android",
@@ -891,7 +891,7 @@
},
{
"kind": "ui-call",
- "line": 563,
+ "line": 595,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Connected (operator: $operator)",
"surface": "android",
@@ -899,7 +899,7 @@
},
{
"kind": "ui-call",
- "line": 2295,
+ "line": 2386,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "This automation already has a queued run.",
"surface": "android",
@@ -907,7 +907,7 @@
},
{
"kind": "ui-call",
- "line": 2325,
+ "line": 2416,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Automation run queued.",
"surface": "android",
@@ -915,7 +915,7 @@
},
{
"kind": "ui-call",
- "line": 2325,
+ "line": 2416,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Automation started.",
"surface": "android",
@@ -923,7 +923,7 @@
},
{
"kind": "ui-call",
- "line": 2338,
+ "line": 2429,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Gateway rejected the automation run.",
"surface": "android",
@@ -931,7 +931,7 @@
},
{
"kind": "ui-call",
- "line": 2369,
+ "line": 2460,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Automation enabled.",
"surface": "android",
@@ -939,7 +939,7 @@
},
{
"kind": "ui-call",
- "line": 2369,
+ "line": 2460,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Automation paused.",
"surface": "android",
@@ -947,7 +947,7 @@
},
{
"kind": "ui-call",
- "line": 2391,
+ "line": 2482,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "This automation changed on the gateway. Review the latest version before saving again.",
"surface": "android",
@@ -955,7 +955,7 @@
},
{
"kind": "ui-call",
- "line": 2397,
+ "line": 2488,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Automation updated.",
"surface": "android",
@@ -963,7 +963,7 @@
},
{
"kind": "ui-call",
- "line": 2412,
+ "line": 2503,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Automation deleted.",
"surface": "android",
@@ -971,7 +971,7 @@
},
{
"kind": "ui-call",
- "line": 2627,
+ "line": 2718,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Node offline. Reconnect and retry.",
"surface": "android",
@@ -979,7 +979,7 @@
},
{
"kind": "ui-named-argument-concatenated",
- "line": 2638,
+ "line": 2729,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Restore canvas now for session=$sessionKey source=$source. If existing A2UI state exists, replay it immediately. If not, create and render a compact mobile-friendly dashboard in Canvas.",
"surface": "android",
@@ -987,7 +987,7 @@
},
{
"kind": "ui-call",
- "line": 2659,
+ "line": 2750,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Failed to request restore. Tap to retry.",
"surface": "android",
@@ -995,7 +995,7 @@
},
{
"kind": "ui-call",
- "line": 2670,
+ "line": 2761,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "No canvas update yet. Tap to retry.",
"surface": "android",
@@ -1003,7 +1003,7 @@
},
{
"kind": "ui-call",
- "line": 3767,
+ "line": 3868,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Connect to a Gateway to save wake words",
"surface": "android",
@@ -1011,7 +1011,7 @@
},
{
"kind": "ui-call",
- "line": 3795,
+ "line": 3896,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Wake words saved",
"surface": "android",
@@ -1019,7 +1019,7 @@
},
{
"kind": "ui-call",
- "line": 3804,
+ "line": 3905,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Could not save wake words",
"surface": "android",
@@ -1027,7 +1027,7 @@
},
{
"kind": "ui-call",
- "line": 4515,
+ "line": 4618,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Failed: no secure gateway endpoint was detected. Enable gateway TLS or Tailscale Serve, or use a trusted private LAN address with Unencrypted selected.",
"surface": "android",
@@ -1035,7 +1035,7 @@
},
{
"kind": "ui-call",
- "line": 4519,
+ "line": 4622,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Failed: secure endpoint reached, but TLS fingerprint verification timed out. Check Tailscale Serve or gateway TLS and retry.",
"surface": "android",
@@ -1043,7 +1043,7 @@
},
{
"kind": "ui-call",
- "line": 4523,
+ "line": 4626,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Failed: couldn't reach the secure gateway endpoint for this host.",
"surface": "android",
@@ -1051,7 +1051,7 @@
},
{
"kind": "ui-named-argument",
- "line": 4573,
+ "line": 4676,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Connecting…",
"surface": "android",
@@ -1059,7 +1059,7 @@
},
{
"kind": "ui-named-argument",
- "line": 4777,
+ "line": 4880,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Offline",
"surface": "android",
@@ -1067,7 +1067,7 @@
},
{
"kind": "ui-call",
- "line": 5590,
+ "line": 5693,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Could not load provider catalog.",
"surface": "android",
@@ -1075,7 +1075,7 @@
},
{
"kind": "ui-call",
- "line": 5621,
+ "line": 5724,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Update your Gateway to view provider model config.",
"surface": "android",
@@ -1083,7 +1083,7 @@
},
{
"kind": "ui-call",
- "line": 5623,
+ "line": 5726,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Could not load provider model config.",
"surface": "android",
@@ -1091,7 +1091,7 @@
},
{
"kind": "ui-call",
- "line": 5639,
+ "line": 5742,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Provider models loaded, but readiness is unavailable.",
"surface": "android",
@@ -1099,7 +1099,7 @@
},
{
"kind": "ui-call",
- "line": 5726,
+ "line": 5829,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Could not load automations.",
"surface": "android",
@@ -1107,7 +1107,7 @@
},
{
"kind": "ui-call",
- "line": 5812,
+ "line": 5915,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Connect the gateway to inspect automations.",
"surface": "android",
@@ -1115,7 +1115,7 @@
},
{
"kind": "ui-call",
- "line": 5822,
+ "line": 5925,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Gateway returned an invalid automation.",
"surface": "android",
@@ -1123,7 +1123,7 @@
},
{
"kind": "ui-call",
- "line": 5826,
+ "line": 5929,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Could not load automation.",
"surface": "android",
@@ -1131,7 +1131,7 @@
},
{
"kind": "ui-call",
- "line": 5838,
+ "line": 5941,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Connect the gateway to inspect automation run history.",
"surface": "android",
@@ -1139,7 +1139,7 @@
},
{
"kind": "ui-call",
- "line": 5869,
+ "line": 5972,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Could not load automation run history.",
"surface": "android",
@@ -1147,7 +1147,7 @@
},
{
"kind": "ui-call",
- "line": 5886,
+ "line": 5989,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Cron changes require operator.admin access.",
"surface": "android",
@@ -1155,7 +1155,7 @@
},
{
"kind": "ui-call",
- "line": 5895,
+ "line": 5998,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Connect the gateway to manage automations.",
"surface": "android",
@@ -1163,7 +1163,7 @@
},
{
"kind": "ui-call",
- "line": 5907,
+ "line": 6010,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Another cron action is still finishing.",
"surface": "android",
@@ -1171,7 +1171,7 @@
},
{
"kind": "ui-call",
- "line": 5954,
+ "line": 6057,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Cron action failed.",
"surface": "android",
@@ -1179,7 +1179,7 @@
},
{
"kind": "ui-call",
- "line": 6066,
+ "line": 6169,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Could not load usage.",
"surface": "android",
@@ -1187,7 +1187,7 @@
},
{
"kind": "ui-call",
- "line": 6081,
+ "line": 6184,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Could not load skills.",
"surface": "android",
@@ -1195,7 +1195,7 @@
},
{
"kind": "ui-call",
- "line": 6101,
+ "line": 6204,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Connect the gateway to update skills.",
"surface": "android",
@@ -1203,7 +1203,7 @@
},
{
"kind": "ui-call",
- "line": 6105,
+ "line": 6208,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "This gateway connection needs operator.admin to update skills.",
"surface": "android",
@@ -1211,7 +1211,7 @@
},
{
"kind": "conditional-branch",
- "line": 6120,
+ "line": 6223,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Could not disable skill.",
"surface": "android",
@@ -1219,7 +1219,7 @@
},
{
"kind": "conditional-branch",
- "line": 6120,
+ "line": 6223,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Could not enable skill.",
"surface": "android",
@@ -1227,7 +1227,7 @@
},
{
"kind": "ui-call",
- "line": 6138,
+ "line": 6241,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Connect the gateway to search ClawHub skills.",
"surface": "android",
@@ -1235,7 +1235,7 @@
},
{
"kind": "ui-call",
- "line": 6184,
+ "line": 6287,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Could not search ClawHub skills.",
"surface": "android",
@@ -1243,7 +1243,7 @@
},
{
"kind": "ui-call",
- "line": 6197,
+ "line": 6300,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Connect the gateway to inspect ClawHub skills.",
"surface": "android",
@@ -1251,7 +1251,7 @@
},
{
"kind": "conditional-branch",
- "line": 6230,
+ "line": 6333,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "ClawHub did not return an installable version for ${skill.slug}.",
"surface": "android",
@@ -1259,7 +1259,7 @@
},
{
"kind": "ui-call",
- "line": 6246,
+ "line": 6349,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Could not load ClawHub details for ${skill.slug}.",
"surface": "android",
@@ -1267,7 +1267,7 @@
},
{
"kind": "ui-call",
- "line": 6262,
+ "line": 6365,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Connect the gateway to install ClawHub skills.",
"surface": "android",
@@ -1275,7 +1275,7 @@
},
{
"kind": "ui-call",
- "line": 6278,
+ "line": 6381,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "This gateway connection needs operator.admin to install ClawHub skills.",
"surface": "android",
@@ -1283,7 +1283,7 @@
},
{
"kind": "conditional-branch",
- "line": 6368,
+ "line": 6471,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Installed $slug.",
"surface": "android",
@@ -1291,7 +1291,7 @@
},
{
"kind": "ui-call",
- "line": 6375,
+ "line": 6478,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Could not install ${slug} from ClawHub.",
"surface": "android",
@@ -1299,7 +1299,7 @@
},
{
"kind": "ui-call",
- "line": 6415,
+ "line": 6518,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Connect the gateway to load Skill Workshop proposals.",
"surface": "android",
@@ -1307,7 +1307,7 @@
},
{
"kind": "ui-call",
- "line": 6455,
+ "line": 6558,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Could not load Skill Workshop proposals.",
"surface": "android",
@@ -1315,7 +1315,7 @@
},
{
"kind": "ui-call",
- "line": 6475,
+ "line": 6578,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Connect the gateway to inspect Skill Workshop proposals.",
"surface": "android",
@@ -1323,7 +1323,7 @@
},
{
"kind": "ui-call",
- "line": 6527,
+ "line": 6630,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Could not inspect Skill Workshop proposal.",
"surface": "android",
@@ -1331,7 +1331,7 @@
},
{
"kind": "ui-call",
- "line": 6547,
+ "line": 6650,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Skill Workshop proposal actions require operator.admin scope.",
"surface": "android",
@@ -1339,7 +1339,7 @@
},
{
"kind": "ui-call",
- "line": 6552,
+ "line": 6655,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Connect the gateway to update Skill Workshop proposals.",
"surface": "android",
@@ -1347,7 +1347,7 @@
},
{
"kind": "ui-call",
- "line": 6742,
+ "line": 6845,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Could not verify the device pairing change. Refresh and try again.",
"surface": "android",
@@ -1355,7 +1355,7 @@
},
{
"kind": "ui-named-argument",
- "line": 6824,
+ "line": 6927,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Connected",
"surface": "android",
@@ -1363,7 +1363,7 @@
},
{
"kind": "ui-call",
- "line": 6854,
+ "line": 6957,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Could not load nodes and devices.",
"surface": "android",
@@ -1371,7 +1371,7 @@
},
{
"kind": "ui-call",
- "line": 7504,
+ "line": 7607,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Could not load channels.",
"surface": "android",
@@ -1379,7 +1379,7 @@
},
{
"kind": "ui-call",
- "line": 7522,
+ "line": 7625,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Could not load dreaming.",
"surface": "android",
@@ -1387,7 +1387,7 @@
},
{
"kind": "ui-call",
- "line": 7537,
+ "line": 7640,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Could not load gateway logs.",
"surface": "android",
@@ -1395,7 +1395,7 @@
},
{
"kind": "ui-call",
- "line": 8020,
+ "line": 8123,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "One time",
"surface": "android",
@@ -1403,7 +1403,7 @@
},
{
"kind": "ui-call",
- "line": 8029,
+ "line": 8132,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Cron",
"surface": "android",
@@ -1411,7 +1411,7 @@
},
{
"kind": "ui-call",
- "line": 8030,
+ "line": 8133,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Scheduled",
"surface": "android",
@@ -1419,7 +1419,7 @@
},
{
"kind": "ui-call",
- "line": 8038,
+ "line": 8141,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Every ${days}d",
"surface": "android",
@@ -1427,7 +1427,7 @@
},
{
"kind": "ui-call",
- "line": 8039,
+ "line": 8142,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Every ${hours}h",
"surface": "android",
@@ -1435,7 +1435,7 @@
},
{
"kind": "ui-call",
- "line": 8040,
+ "line": 8143,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Every ${minutes}m",
"surface": "android",
@@ -1443,7 +1443,7 @@
},
{
"kind": "ui-call",
- "line": 8041,
+ "line": 8144,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Repeating",
"surface": "android",
@@ -1451,7 +1451,7 @@
},
{
"kind": "ui-call",
- "line": 8057,
+ "line": 8160,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "No prompt",
"surface": "android",
@@ -1459,7 +1459,7 @@
},
{
"kind": "ui-call",
- "line": 8074,
+ "line": 8177,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Gateway",
"surface": "android",
@@ -1467,7 +1467,7 @@
},
{
"kind": "ui-call",
- "line": 8082,
+ "line": 8185,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Connected to $gatewayLabel",
"surface": "android",
@@ -1475,7 +1475,7 @@
},
{
"kind": "ui-call",
- "line": 8083,
+ "line": 8186,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Your agents are ready",
"surface": "android",
@@ -1483,7 +1483,7 @@
},
{
"kind": "ui-call",
- "line": 8085,
+ "line": 8188,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "This phone stays dormant until the gateway needs it, then wakes, syncs, and goes back to sleep.",
"surface": "android",
@@ -1491,7 +1491,7 @@
},
{
"kind": "ui-call",
- "line": 8089,
+ "line": 8192,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Selected on this phone",
"surface": "android",
@@ -1499,7 +1499,7 @@
},
{
"kind": "ui-call",
- "line": 8092,
+ "line": 8195,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "The overview refreshes on reconnect and when this screen opens.",
"surface": "android",
@@ -1507,7 +1507,7 @@
},
{
"kind": "ui-call",
- "line": 8097,
+ "line": 8200,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Reconnecting",
"surface": "android",
@@ -1515,7 +1515,7 @@
},
{
"kind": "ui-call",
- "line": 8098,
+ "line": 8201,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "OpenClaw is syncing back up",
"surface": "android",
@@ -1523,7 +1523,7 @@
},
{
"kind": "ui-call",
- "line": 8100,
+ "line": 8203,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "The gateway session is coming back online. Agent shortcuts should settle automatically in a moment.",
"surface": "android",
@@ -1531,7 +1531,7 @@
},
{
"kind": "ui-call",
- "line": 8104,
+ "line": 8207,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Gateway session in progress",
"surface": "android",
@@ -1539,7 +1539,7 @@
},
{
"kind": "ui-call",
- "line": 8107,
+ "line": 8210,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "If the gateway is reachable, reconnect should complete without intervention.",
"surface": "android",
@@ -1547,7 +1547,7 @@
},
{
"kind": "ui-call",
- "line": 8112,
+ "line": 8215,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Welcome to OpenClaw",
"surface": "android",
@@ -1555,7 +1555,7 @@
},
{
"kind": "ui-call",
- "line": 8113,
+ "line": 8216,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Your phone stays quiet until it is needed",
"surface": "android",
@@ -1563,7 +1563,7 @@
},
{
"kind": "ui-call",
- "line": 8115,
+ "line": 8218,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Pair this device to your gateway to wake it only for real work, keep a live agent overview handy, and avoid battery-draining background loops.",
"surface": "android",
@@ -1571,7 +1571,7 @@
},
{
"kind": "ui-call",
- "line": 8119,
+ "line": 8222,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Connect to load your agents",
"surface": "android",
@@ -1579,7 +1579,7 @@
},
{
"kind": "ui-call",
- "line": 8122,
+ "line": 8225,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "When connected, the gateway can wake the phone with a silent push instead of holding an always-on session.",
"surface": "android",
@@ -1587,7 +1587,7 @@
},
{
"kind": "ui-call",
- "line": 8154,
+ "line": 8257,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Main",
"surface": "android",
@@ -1595,7 +1595,7 @@
},
{
"kind": "ui-call",
- "line": 8169,
+ "line": 8272,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Active on this phone",
"surface": "android",
@@ -1603,7 +1603,7 @@
},
{
"kind": "ui-call",
- "line": 8170,
+ "line": 8273,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Default agent",
"surface": "android",
@@ -1611,7 +1611,7 @@
},
{
"kind": "ui-call",
- "line": 8171,
+ "line": 8274,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Ready",
"surface": "android",
@@ -1619,7 +1619,7 @@
},
{
"kind": "ui-call",
- "line": 8826,
+ "line": 8929,
"path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt",
"source": "Dream",
"surface": "android",
@@ -1627,7 +1627,7 @@
},
{
"kind": "ui-call",
- "line": 199,
+ "line": 368,
"path": "apps/android/app/src/main/java/ai/openclaw/app/PermissionRequester.kt",
"source": "Permission required",
"surface": "android",
@@ -1635,7 +1635,7 @@
},
{
"kind": "ui-call",
- "line": 201,
+ "line": 370,
"path": "apps/android/app/src/main/java/ai/openclaw/app/PermissionRequester.kt",
"source": "Continue",
"surface": "android",
@@ -1643,7 +1643,7 @@
},
{
"kind": "ui-call",
- "line": 202,
+ "line": 371,
"path": "apps/android/app/src/main/java/ai/openclaw/app/PermissionRequester.kt",
"source": "Not now",
"surface": "android",
@@ -1651,7 +1651,7 @@
},
{
"kind": "ui-call",
- "line": 229,
+ "line": 429,
"path": "apps/android/app/src/main/java/ai/openclaw/app/PermissionRequester.kt",
"source": "Enable permission in Settings",
"surface": "android",
@@ -1659,7 +1659,7 @@
},
{
"kind": "ui-call",
- "line": 231,
+ "line": 431,
"path": "apps/android/app/src/main/java/ai/openclaw/app/PermissionRequester.kt",
"source": "Open Settings",
"surface": "android",
@@ -1667,7 +1667,7 @@
},
{
"kind": "ui-call",
- "line": 239,
+ "line": 443,
"path": "apps/android/app/src/main/java/ai/openclaw/app/PermissionRequester.kt",
"source": "Cancel",
"surface": "android",
@@ -1675,7 +1675,7 @@
},
{
"kind": "ui-call",
- "line": 246,
+ "line": 453,
"path": "apps/android/app/src/main/java/ai/openclaw/app/PermissionRequester.kt",
"source": "OpenClaw needs ${labels.joinToString(\", \")} permissions to continue.",
"surface": "android",
@@ -1683,7 +1683,7 @@
},
{
"kind": "ui-call",
- "line": 254,
+ "line": 461,
"path": "apps/android/app/src/main/java/ai/openclaw/app/PermissionRequester.kt",
"source": "Please enable ${labels.joinToString(\", \")} in Android Settings to continue.",
"surface": "android",
@@ -1691,7 +1691,7 @@
},
{
"kind": "ui-call",
- "line": 262,
+ "line": 469,
"path": "apps/android/app/src/main/java/ai/openclaw/app/PermissionRequester.kt",
"source": "Camera",
"surface": "android",
@@ -1699,7 +1699,7 @@
},
{
"kind": "ui-call",
- "line": 263,
+ "line": 470,
"path": "apps/android/app/src/main/java/ai/openclaw/app/PermissionRequester.kt",
"source": "Microphone",
"surface": "android",
@@ -1707,7 +1707,7 @@
},
{
"kind": "ui-call",
- "line": 264,
+ "line": 471,
"path": "apps/android/app/src/main/java/ai/openclaw/app/PermissionRequester.kt",
"source": "Send SMS",
"surface": "android",
@@ -1715,7 +1715,7 @@
},
{
"kind": "ui-call",
- "line": 265,
+ "line": 472,
"path": "apps/android/app/src/main/java/ai/openclaw/app/PermissionRequester.kt",
"source": "Read SMS",
"surface": "android",
@@ -1723,7 +1723,7 @@
},
{
"kind": "ui-call",
- "line": 266,
+ "line": 473,
"path": "apps/android/app/src/main/java/ai/openclaw/app/PermissionRequester.kt",
"source": "Read Contacts",
"surface": "android",
@@ -1731,7 +1731,7 @@
},
{
"kind": "ui-call",
- "line": 267,
+ "line": 474,
"path": "apps/android/app/src/main/java/ai/openclaw/app/PermissionRequester.kt",
"source": "Write Contacts",
"surface": "android",
@@ -1739,7 +1739,7 @@
},
{
"kind": "ui-call",
- "line": 268,
+ "line": 475,
"path": "apps/android/app/src/main/java/ai/openclaw/app/PermissionRequester.kt",
"source": "Read Calendar",
"surface": "android",
@@ -1747,7 +1747,7 @@
},
{
"kind": "ui-call",
- "line": 269,
+ "line": 476,
"path": "apps/android/app/src/main/java/ai/openclaw/app/PermissionRequester.kt",
"source": "Write Calendar",
"surface": "android",
@@ -1755,7 +1755,7 @@
},
{
"kind": "ui-call",
- "line": 270,
+ "line": 477,
"path": "apps/android/app/src/main/java/ai/openclaw/app/PermissionRequester.kt",
"source": "Read Call Log",
"surface": "android",
@@ -1763,7 +1763,7 @@
},
{
"kind": "ui-call",
- "line": 271,
+ "line": 478,
"path": "apps/android/app/src/main/java/ai/openclaw/app/PermissionRequester.kt",
"source": "Motion Activity",
"surface": "android",
@@ -1771,7 +1771,7 @@
},
{
"kind": "ui-call",
- "line": 274,
+ "line": 481,
"path": "apps/android/app/src/main/java/ai/openclaw/app/PermissionRequester.kt",
"source": "Photos",
"surface": "android",
@@ -12955,7 +12955,7 @@
},
{
"kind": "ui-call",
- "line": 409,
+ "line": 418,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Model",
"surface": "android",
@@ -12963,7 +12963,7 @@
},
{
"kind": "ui-call",
- "line": 532,
+ "line": 541,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Some shared images were omitted or could not be added.",
"surface": "android",
@@ -12971,7 +12971,7 @@
},
{
"kind": "ui-call",
- "line": 688,
+ "line": 697,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Chat needs attention",
"surface": "android",
@@ -12979,7 +12979,7 @@
},
{
"kind": "ui-call",
- "line": 1038,
+ "line": 1047,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "All",
"surface": "android",
@@ -12987,7 +12987,7 @@
},
{
"kind": "ui-call",
- "line": 1118,
+ "line": 1127,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Show Sidebar",
"surface": "android",
@@ -12995,7 +12995,7 @@
},
{
"kind": "ui-call",
- "line": 1134,
+ "line": 1143,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Working",
"surface": "android",
@@ -13003,7 +13003,7 @@
},
{
"kind": "ui-call",
- "line": 1135,
+ "line": 1144,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Ready",
"surface": "android",
@@ -13011,7 +13011,7 @@
},
{
"kind": "ui-call",
- "line": 1136,
+ "line": 1145,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Offline",
"surface": "android",
@@ -13019,7 +13019,7 @@
},
{
"kind": "ui-call",
- "line": 1149,
+ "line": 1158,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Chat actions",
"surface": "android",
@@ -13027,7 +13027,7 @@
},
{
"kind": "ui-call",
- "line": 1154,
+ "line": 1163,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Refresh chat",
"surface": "android",
@@ -13035,7 +13035,7 @@
},
{
"kind": "ui-call",
- "line": 1173,
+ "line": 1182,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Dashboard",
"surface": "android",
@@ -13043,7 +13043,7 @@
},
{
"kind": "ui-call",
- "line": 1181,
+ "line": 1190,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Background tasks",
"surface": "android",
@@ -13051,7 +13051,7 @@
},
{
"kind": "ui-call",
- "line": 1202,
+ "line": 1211,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Chat",
"surface": "android",
@@ -13059,7 +13059,7 @@
},
{
"kind": "ui-call",
- "line": 1395,
+ "line": 1407,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Messages to recover",
"surface": "android",
@@ -13067,7 +13067,7 @@
},
{
"kind": "ui-call",
- "line": 1397,
+ "line": 1409,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "${item.count} message(s) need recovery. Re-enter anything you want to keep, then delete these rows.",
"surface": "android",
@@ -13075,7 +13075,7 @@
},
{
"kind": "ui-call",
- "line": 1442,
+ "line": 1454,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Loading thread",
"surface": "android",
@@ -13083,7 +13083,7 @@
},
{
"kind": "ui-call",
- "line": 1475,
+ "line": 1487,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Jump to latest",
"surface": "android",
@@ -13091,7 +13091,7 @@
},
{
"kind": "ui-call",
- "line": 1549,
+ "line": 1561,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Ready when you are",
"surface": "android",
@@ -13099,7 +13099,7 @@
},
{
"kind": "ui-call",
- "line": 1553,
+ "line": 1565,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Start with a prompt, or use voice.",
"surface": "android",
@@ -13107,7 +13107,7 @@
},
{
"kind": "ui-call",
- "line": 1555,
+ "line": 1567,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Use the recovery options below to reconnect.",
"surface": "android",
@@ -13115,7 +13115,7 @@
},
{
"kind": "ui-call",
- "line": 1557,
+ "line": 1569,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Chat is checking Gateway health.",
"surface": "android",
@@ -13123,7 +13123,7 @@
},
{
"kind": "ui-call",
- "line": 1580,
+ "line": 1592,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Fix connection",
"surface": "android",
@@ -13131,7 +13131,7 @@
},
{
"kind": "ui-call",
- "line": 1581,
+ "line": 1593,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Copy diagnostics",
"surface": "android",
@@ -13139,7 +13139,7 @@
},
{
"kind": "ui-call",
- "line": 1640,
+ "line": 1652,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Catch me up",
"surface": "android",
@@ -13147,7 +13147,7 @@
},
{
"kind": "ui-call",
- "line": 1641,
+ "line": 1653,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Summarize recent threads and next steps.",
"surface": "android",
@@ -13155,7 +13155,7 @@
},
{
"kind": "ui-call",
- "line": 1642,
+ "line": 1654,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Catch me up on my recent OpenClaw threads and suggest next steps.",
"surface": "android",
@@ -13163,7 +13163,7 @@
},
{
"kind": "ui-call",
- "line": 1646,
+ "line": 1658,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Plan the work",
"surface": "android",
@@ -13171,7 +13171,7 @@
},
{
"kind": "ui-call",
- "line": 1647,
+ "line": 1659,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Turn a goal into an actionable checklist.",
"surface": "android",
@@ -13179,7 +13179,7 @@
},
{
"kind": "ui-call",
- "line": 1648,
+ "line": 1660,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Help me turn this goal into a practical checklist: ",
"surface": "android",
@@ -13187,7 +13187,7 @@
},
{
"kind": "ui-call",
- "line": 1652,
+ "line": 1664,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Use this phone",
"surface": "android",
@@ -13195,7 +13195,7 @@
},
{
"kind": "ui-call",
- "line": 1653,
+ "line": 1665,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Ask OpenClaw to use Android capabilities.",
"surface": "android",
@@ -13203,7 +13203,7 @@
},
{
"kind": "ui-call",
- "line": 1654,
+ "line": 1666,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "What can you help me do from this phone right now?",
"surface": "android",
@@ -13211,7 +13211,7 @@
},
{
"kind": "ui-call",
- "line": 1738,
+ "line": 1750,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "OpenClaw · Live",
"surface": "android",
@@ -13219,7 +13219,7 @@
},
{
"kind": "ui-call",
- "line": 1739,
+ "line": 1751,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "You",
"surface": "android",
@@ -13227,7 +13227,7 @@
},
{
"kind": "ui-call",
- "line": 1740,
+ "line": 1752,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "System",
"surface": "android",
@@ -13235,7 +13235,7 @@
},
{
"kind": "ui-call",
- "line": 1741,
+ "line": 1753,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "OpenClaw",
"surface": "android",
@@ -13243,7 +13243,7 @@
},
{
"kind": "ui-call",
- "line": 1780,
+ "line": 1792,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Image",
"surface": "android",
@@ -13251,7 +13251,7 @@
},
{
"kind": "ui-call",
- "line": 1796,
+ "line": 1808,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Additional images hidden: ${omittedImageCount}",
"surface": "android",
@@ -13259,7 +13259,7 @@
},
{
"kind": "ui-call",
- "line": 1851,
+ "line": 1863,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Preparing audio…",
"surface": "android",
@@ -13267,7 +13267,7 @@
},
{
"kind": "ui-call",
- "line": 1851,
+ "line": 1863,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Speaking…",
"surface": "android",
@@ -13275,7 +13275,7 @@
},
{
"kind": "ui-call",
- "line": 1882,
+ "line": 1894,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Close",
"surface": "android",
@@ -13283,7 +13283,7 @@
},
{
"kind": "ui-call",
- "line": 1882,
+ "line": 1894,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "View all",
"surface": "android",
@@ -13291,7 +13291,7 @@
},
{
"kind": "ui-call",
- "line": 1912,
+ "line": 1924,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Tools running",
"surface": "android",
@@ -13299,7 +13299,7 @@
},
{
"kind": "ui-call",
- "line": 1914,
+ "line": 1926,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "OpenClaw is working",
"surface": "android",
@@ -13307,7 +13307,7 @@
},
{
"kind": "ui-call",
- "line": 1917,
+ "line": 1929,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "+${toolCalls.size - 4} more",
"surface": "android",
@@ -13315,7 +13315,7 @@
},
{
"kind": "ui-named-argument",
- "line": 1983,
+ "line": 1995,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "$completedCount/${steps.size}",
"surface": "android",
@@ -13323,7 +13323,7 @@
},
{
"kind": "ui-call",
- "line": 1990,
+ "line": 2002,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Collapse plan checklist",
"surface": "android",
@@ -13331,7 +13331,7 @@
},
{
"kind": "ui-call",
- "line": 1990,
+ "line": 2002,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Expand plan checklist",
"surface": "android",
@@ -13339,7 +13339,7 @@
},
{
"kind": "ui-call",
- "line": 2123,
+ "line": 2135,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Dismiss shared-image warning",
"surface": "android",
@@ -13347,7 +13347,7 @@
},
{
"kind": "ui-call",
- "line": 2227,
+ "line": 2239,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Stop",
"surface": "android",
@@ -13355,7 +13355,7 @@
},
{
"kind": "ui-call",
- "line": 2314,
+ "line": 2326,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Switch branch",
"surface": "android",
@@ -13363,7 +13363,7 @@
},
{
"kind": "ui-call",
- "line": 2338,
+ "line": 2350,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Untitled branch",
"surface": "android",
@@ -13371,7 +13371,7 @@
},
{
"kind": "ui-call",
- "line": 2353,
+ "line": 2365,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Current branch",
"surface": "android",
@@ -13379,7 +13379,7 @@
},
{
"kind": "ui-call",
- "line": 2365,
+ "line": 2377,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Messages: $count",
"surface": "android",
@@ -13387,7 +13387,7 @@
},
{
"kind": "ui-call",
- "line": 2373,
+ "line": 2385,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "$count · $updated",
"surface": "android",
@@ -13395,7 +13395,7 @@
},
{
"kind": "ui-call",
- "line": 2402,
+ "line": 2414,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Default",
"surface": "android",
@@ -13403,7 +13403,7 @@
},
{
"kind": "ui-call",
- "line": 2468,
+ "line": 2480,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Pin model",
"surface": "android",
@@ -13411,7 +13411,7 @@
},
{
"kind": "ui-call",
- "line": 2468,
+ "line": 2480,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Unpin model",
"surface": "android",
@@ -13419,7 +13419,7 @@
},
{
"kind": "ui-call",
- "line": 2485,
+ "line": 2497,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "No commands found",
"surface": "android",
@@ -13427,7 +13427,7 @@
},
{
"kind": "ui-call",
- "line": 2527,
+ "line": 2539,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Command",
"surface": "android",
@@ -13435,7 +13435,7 @@
},
{
"kind": "ui-call",
- "line": 2547,
+ "line": 2559,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Gateway offline",
"surface": "android",
@@ -13443,7 +13443,7 @@
},
{
"kind": "ui-call",
- "line": 2593,
+ "line": 2605,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Close thinking level selector",
"surface": "android",
@@ -13451,7 +13451,7 @@
},
{
"kind": "ui-call",
- "line": 2593,
+ "line": 2605,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Open thinking level selector",
"surface": "android",
@@ -13459,7 +13459,7 @@
},
{
"kind": "ui-call",
- "line": 2659,
+ "line": 2671,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Attach image",
"surface": "android",
@@ -13467,7 +13467,7 @@
},
{
"kind": "ui-call",
- "line": 2664,
+ "line": 2676,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Attachment",
"surface": "android",
@@ -13475,7 +13475,7 @@
},
{
"kind": "ui-call",
- "line": 2669,
+ "line": 2681,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Attach video",
"surface": "android",
@@ -13483,7 +13483,7 @@
},
{
"kind": "ui-call",
- "line": 2700,
+ "line": 2712,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Message OpenClaw",
"surface": "android",
@@ -13491,7 +13491,7 @@
},
{
"kind": "ui-call",
- "line": 2729,
+ "line": 2741,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "End Talk",
"surface": "android",
@@ -13499,7 +13499,7 @@
},
{
"kind": "ui-call",
- "line": 2729,
+ "line": 2741,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Start Talk",
"surface": "android",
@@ -13507,7 +13507,7 @@
},
{
"kind": "ui-call",
- "line": 2829,
+ "line": 2841,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Voice note · ${formatVoiceNoteDuration(duration)}",
"surface": "android",
@@ -13515,7 +13515,7 @@
},
{
"kind": "ui-call",
- "line": 2838,
+ "line": 2850,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Remove attachment",
"surface": "android",
@@ -13523,7 +13523,7 @@
},
{
"kind": "ui-call",
- "line": 2850,
+ "line": 2862,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "New chat",
"surface": "android",
@@ -13531,7 +13531,7 @@
},
{
"kind": "ui-call",
- "line": 2859,
+ "line": 2871,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Main",
"surface": "android",
@@ -13539,7 +13539,7 @@
},
{
"kind": "ui-call",
- "line": 2860,
+ "line": 2872,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Current",
"surface": "android",
@@ -13547,7 +13547,7 @@
},
{
"kind": "ui-call",
- "line": 2867,
+ "line": 2879,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "$emoji $name",
"surface": "android",
@@ -13555,7 +13555,7 @@
},
{
"kind": "ui-call",
- "line": 2926,
+ "line": 2938,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Send",
"surface": "android",
@@ -13563,7 +13563,7 @@
},
{
"kind": "ui-call",
- "line": 2937,
+ "line": 2949,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Chat is still checking Gateway health.",
"surface": "android",
@@ -13571,7 +13571,7 @@
},
{
"kind": "ui-call",
- "line": 2938,
+ "line": 2950,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Gateway is offline. Fix the connection below or copy diagnostics.",
"surface": "android",
@@ -13579,7 +13579,7 @@
},
{
"kind": "ui-call",
- "line": 2939,
+ "line": 2951,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Gateway authentication needs attention.",
"surface": "android",
@@ -13587,7 +13587,7 @@
},
{
"kind": "ui-call",
- "line": 2958,
+ "line": 2970,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Context ${(it * 100).roundToInt()}%",
"surface": "android",
@@ -13595,7 +13595,7 @@
},
{
"kind": "ui-call",
- "line": 2959,
+ "line": 2971,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Context --",
"surface": "android",
@@ -13603,7 +13603,7 @@
},
{
"kind": "ui-call",
- "line": 2960,
+ "line": 2972,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "$contextLabel · ${contextMeterThinkingLabel(thinkingLevel)}",
"surface": "android",
@@ -13611,7 +13611,7 @@
},
{
"kind": "ui-call",
- "line": 2999,
+ "line": 3011,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Off",
"surface": "android",
@@ -13619,7 +13619,7 @@
},
{
"kind": "ui-call",
- "line": 3000,
+ "line": 3012,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Minimal",
"surface": "android",
@@ -13627,7 +13627,7 @@
},
{
"kind": "ui-call",
- "line": 3001,
+ "line": 3013,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Low",
"surface": "android",
@@ -13635,7 +13635,7 @@
},
{
"kind": "ui-call",
- "line": 3002,
+ "line": 3014,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Medium",
"surface": "android",
@@ -13643,7 +13643,7 @@
},
{
"kind": "ui-call",
- "line": 3003,
+ "line": 3015,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "High",
"surface": "android",
@@ -13651,7 +13651,7 @@
},
{
"kind": "ui-call",
- "line": 3004,
+ "line": 3016,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Xhigh",
"surface": "android",
@@ -13659,7 +13659,7 @@
},
{
"kind": "ui-call",
- "line": 3005,
+ "line": 3017,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Adaptive",
"surface": "android",
@@ -13667,7 +13667,7 @@
},
{
"kind": "ui-call",
- "line": 3006,
+ "line": 3018,
"path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt",
"source": "Max",
"surface": "android",
diff --git a/apps/android/README.md b/apps/android/README.md
index 371ab7ff7d56..55c30a1b21b0 100644
--- a/apps/android/README.md
+++ b/apps/android/README.md
@@ -367,7 +367,9 @@ What it does:
- Reads `node.describe` command list from the selected Android node.
- Invokes advertised non-interactive commands.
-- Skips `screen.record` in this suite (Android requires interactive per-invocation screen-capture consent).
+- Skips `screen.record` and `talk.ptt.*` in this suite because they require
+ interactive capture. Use `apps/android/scripts/voice-e2e.sh` for microphone
+ and voice-path proof.
- Asserts command contracts (success or expected deterministic error for safe-invalid calls like `sms.send` and `notifications.actions`).
Common failure quick-fixes:
diff --git a/apps/android/app/src/debug/java/ai/openclaw/app/VoiceE2eReceiver.kt b/apps/android/app/src/debug/java/ai/openclaw/app/VoiceE2eReceiver.kt
index 832e79b1c675..eeeb3ff8e8c0 100644
--- a/apps/android/app/src/debug/java/ai/openclaw/app/VoiceE2eReceiver.kt
+++ b/apps/android/app/src/debug/java/ai/openclaw/app/VoiceE2eReceiver.kt
@@ -10,6 +10,7 @@ import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -164,10 +165,22 @@ class VoiceE2eService : Service() {
runtime: NodeRuntime,
timeoutMs: Long,
) {
- withTimeout(timeoutMs) {
- while (!runtime.isConnected.value) {
- delay(100L)
+ try {
+ withTimeout(timeoutMs) {
+ while (!runtime.isConnected.value) {
+ voiceE2eTerminalGatewayFailure(runtime.gatewayConnectionProblem.value)?.let { error(it) }
+ delay(100L)
+ }
}
+ } catch (err: TimeoutCancellationException) {
+ throw IllegalStateException(
+ voiceE2eGatewayTimeoutMessage(
+ timeoutMs = timeoutMs,
+ statusText = runtime.statusText.value,
+ problem = runtime.gatewayConnectionProblem.value,
+ ),
+ err,
+ )
}
}
@@ -198,3 +211,32 @@ private fun Intent.getDecodedStringExtra(name: String): String? {
}
return getStringExtra(name)
}
+
+internal fun voiceE2eTerminalGatewayFailure(problem: GatewayConnectionProblem?): String? =
+ problem
+ ?.takeIf { it.pauseReconnect && !it.canAutoRetry }
+ ?.message
+ ?.trim()
+ ?.takeIf { it.isNotEmpty() }
+
+internal fun voiceE2eGatewayTimeoutMessage(
+ timeoutMs: Long,
+ statusText: String,
+ problem: GatewayConnectionProblem?,
+): String {
+ val detail =
+ problem
+ ?.message
+ ?.trim()
+ ?.takeIf { it.isNotEmpty() }
+ ?: statusText.trim().takeIf { it.isNotEmpty() }
+ return buildString {
+ append("Gateway connection timed out after ")
+ append(timeoutMs)
+ append(" ms")
+ if (detail != null) {
+ append(": ")
+ append(detail)
+ }
+ }
+}
diff --git a/apps/android/app/src/main/AndroidManifest.xml b/apps/android/app/src/main/AndroidManifest.xml
index 7986485f8308..2b6c953e8320 100644
--- a/apps/android/app/src/main/AndroidManifest.xml
+++ b/apps/android/app/src/main/AndroidManifest.xml
@@ -69,6 +69,13 @@
android:host="*"
android:path="/openclaw/wear/v1/realtime/audio" />
+
+
+
+
initializedViewModel?.let { handleLaunchIntent(viewModel = it, intent = routedIntent) }
}
- if (!accepted) {
- Toast.makeText(this, nativeString("Too many shares are waiting to be added."), Toast.LENGTH_SHORT).show()
- }
+ if (!accepted) return
}
override fun onRequestPermissionsResult(
@@ -135,9 +147,8 @@ class MainActivity : AppCompatActivity() {
) {
// AppCompatActivity marks this callback @CallSuper; it preserves Fragment and ActivityResult dispatch.
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
- if (::permissionRequester.isInitialized) {
- permissionRequester.onRequestPermissionsResult(requestCode, permissions, grantResults)
- }
+ permissionRequester.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ initializedViewModel?.refreshNodePermissionSurface()
}
/**
@@ -154,6 +165,7 @@ class MainActivity : AppCompatActivity() {
pendingIntentRouter.activate { initialIntent ->
handleLaunchIntent(viewModel = readyViewModel, intent = initialIntent)
}
+ readyViewModel.reportShareLaunchOverflow(pendingIntentRouter.takeShareOverflowCount())
}
/**
@@ -192,6 +204,22 @@ class MainActivity : AppCompatActivity() {
}
}
}
+
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ readyViewModel.shareLaunchOverflowRevision.collect { revision ->
+ if (revision == 0L) return@collect
+ repeat(readyViewModel.takeShareLaunchOverflowCount()) {
+ Toast
+ .makeText(
+ this@MainActivity,
+ nativeString("Too many shares are waiting to be added."),
+ Toast.LENGTH_SHORT,
+ ).show()
+ }
+ }
+ }
+ }
}
/**
@@ -202,23 +230,7 @@ class MainActivity : AppCompatActivity() {
intent: Intent?,
) {
if (intent?.isShareLaunchIntent() == true) {
- if (!shareLaunchSlots.tryAcquire()) {
- Toast.makeText(this, nativeString("Too many shares are waiting to be added."), Toast.LENGTH_SHORT).show()
- return
- }
- val owner = viewModel.captureChatShareOwner()
- lifecycleScope
- .launch {
- shareLaunchMutex.withLock {
- val request =
- withContext(Dispatchers.IO) {
- parseShareLaunchIntent(intent, contentResolver::getType)
- } ?: return@withLock
- if (!viewModel.handleShareLaunch(request, owner)) {
- Toast.makeText(this@MainActivity, nativeString("Too many shares are waiting to be added."), Toast.LENGTH_SHORT).show()
- }
- }
- }.invokeOnCompletion { shareLaunchSlots.release() }
+ viewModel.handleShareLaunchIntent(intent)
return
}
parseHomeDestinationIntent(intent)?.let { destination ->
@@ -242,6 +254,7 @@ internal class MainActivityPendingIntentRouter {
private var sequence = 0L
private val pendingShareIntents = ArrayDeque()
private var pendingNonShareIntent: PendingLaunchIntent? = null
+ private var shareOverflowCount = 0
fun setInitialIntent(intent: Intent?) {
if (!activated && intent != null) store(intent = intent, initial = true)
@@ -275,6 +288,11 @@ internal class MainActivityPendingIntentRouter {
return true
}
+ fun takeShareOverflowCount(): Int =
+ shareOverflowCount.also {
+ shareOverflowCount = 0
+ }
+
private fun store(
intent: Intent,
initial: Boolean,
@@ -284,7 +302,10 @@ internal class MainActivityPendingIntentRouter {
pendingNonShareIntent = pending
return true
}
- if (pendingShareIntents.size >= MAX_PENDING_CHAT_SHARES) return false
+ if (pendingShareIntents.size >= MAX_PENDING_CHAT_SHARES) {
+ shareOverflowCount += 1
+ return false
+ }
pendingShareIntents.addLast(pending)
return true
}
@@ -305,6 +326,20 @@ internal class MainActivityInitialIntentGate {
internal fun shouldNotifyRuntimeBackgrounded(isChangingConfigurations: Boolean): Boolean = !isChangingConfigurations
+internal fun updateTopResumedPermissionHost(
+ isTopResumedActivity: Boolean,
+ activate: () -> Unit,
+ deactivate: () -> Unit,
+ refreshPermissionSurface: () -> Unit,
+) {
+ if (isTopResumedActivity) {
+ activate()
+ refreshPermissionSurface()
+ } else {
+ deactivate()
+ }
+}
+
/** Preserves one-shot runtime UI startup while allowing screenshot fixtures to skip side effects. */
internal class MainActivityRuntimeUiStarter {
private var completed = false
diff --git a/apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt b/apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt
index 23499aeb11d5..e88dd45e62f3 100644
--- a/apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt
+++ b/apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt
@@ -47,6 +47,8 @@ import ai.openclaw.app.voice.VoiceConversationEntry
import ai.openclaw.app.voice.VoiceWakePreferences
import android.Manifest
import android.app.Application
+import android.content.Intent
+import android.net.Uri
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.SavedStateHandle
@@ -65,7 +67,9 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
import java.util.UUID
import java.util.concurrent.atomic.AtomicLong
@@ -276,17 +280,33 @@ class MainViewModel private constructor(
app: Application,
private val prefs: SecurePrefs,
savedStateHandle: SavedStateHandle,
+ private val resolveShareMimeType: (Uri) -> String?,
+ shareLaunchCapacity: Int,
) : AndroidViewModel(app) {
constructor(
app: Application,
savedStateHandle: SavedStateHandle,
- ) : this(app, (app as NodeApp).prefs, savedStateHandle)
+ ) : this(
+ app = app,
+ prefs = (app as NodeApp).prefs,
+ savedStateHandle = savedStateHandle,
+ resolveShareMimeType = app.contentResolver::getType,
+ shareLaunchCapacity = MAX_PENDING_CHAT_SHARES,
+ )
internal constructor(
app: NodeApp,
prefs: SecurePrefs,
savedStateHandle: SavedStateHandle,
- ) : this(app as Application, prefs, savedStateHandle)
+ resolveShareMimeType: (Uri) -> String? = app.contentResolver::getType,
+ shareLaunchCapacity: Int = MAX_PENDING_CHAT_SHARES,
+ ) : this(
+ app = app as Application,
+ prefs = prefs,
+ savedStateHandle = savedStateHandle,
+ resolveShareMimeType = resolveShareMimeType,
+ shareLaunchCapacity = shareLaunchCapacity,
+ )
private val nodeApp = app as NodeApp
private val runtimeRef = MutableStateFlow(null)
@@ -296,6 +316,10 @@ class MainViewModel private constructor(
// Multiple MainActivity instances can overlap across sender tasks; the process owns one queue.
private val chatShareDraftSeq = nodeApp.chatShareDraftSeq
private val chatShareDraftQueue = nodeApp.chatShareDraftQueue
+ private val shareLaunchMutex = Mutex()
+ private val shareLaunchSlots = Semaphore(shareLaunchCapacity)
+ private val shareLaunchOverflowLock = Any()
+ private var pendingShareLaunchOverflowCount = 0
// One bounded heap-only slot follows the ViewModel across Activity recreation.
// Detail disposal clears it; process death drops it with the ViewModel.
@@ -352,6 +376,8 @@ class MainViewModel private constructor(
val chatShareDraft: StateFlow = chatShareDraftQueue.head
internal val chatShareDrafts: StateFlow> = chatShareDraftQueue.queued
internal val chatShareDraftOwnerRevision: StateFlow = chatShareDraftQueue.ownerRevision
+ private val shareLaunchOverflowRevisionMutable = MutableStateFlow(0L)
+ internal val shareLaunchOverflowRevision: StateFlow = shareLaunchOverflowRevisionMutable.asStateFlow()
private val pendingAssistantAutoSendMutable = MutableStateFlow(null)
internal val pendingAssistantAutoSend: StateFlow = pendingAssistantAutoSendMutable
private val _assistantAutoSendInFlight = MutableStateFlow(false)
@@ -706,6 +732,10 @@ class MainViewModel private constructor(
runtimeRef.value?.setForeground(value)
}
+ fun refreshNodePermissionSurface() {
+ runtimeRef.value?.refreshNodePermissionSurface()
+ }
+
fun setDisplayName(value: String) {
prefs.setDisplayName(value)
}
@@ -964,8 +994,49 @@ class MainViewModel private constructor(
setChatDraft(request.prompt?.let { ChatDraft(text = it, placement = ChatDraftPlacement.Replace, owner = owner) })
}
+ /**
+ * Owns share admission through queue insertion so Activity recreation cannot cancel accepted work.
+ */
+ internal fun handleShareLaunchIntent(intent: Intent): Boolean {
+ if (!shareLaunchSlots.tryAcquire()) {
+ reportShareLaunchOverflow()
+ return false
+ }
+ val retainedIntent = Intent(intent)
+ val owner = captureChatShareOwner()
+ viewModelScope.launch(start = CoroutineStart.UNDISPATCHED) {
+ try {
+ shareLaunchMutex.withLock {
+ val request =
+ withContext(Dispatchers.IO) {
+ parseShareLaunchIntent(retainedIntent, resolveShareMimeType)
+ } ?: return@withLock
+ if (!enqueueShareLaunch(request, owner)) reportShareLaunchOverflow()
+ }
+ } finally {
+ shareLaunchSlots.release()
+ }
+ }
+ return true
+ }
+
+ internal fun reportShareLaunchOverflow(count: Int = 1) {
+ if (count <= 0) return
+ synchronized(shareLaunchOverflowLock) {
+ pendingShareLaunchOverflowCount += count
+ shareLaunchOverflowRevisionMutable.value += 1
+ }
+ }
+
+ internal fun takeShareLaunchOverflowCount(): Int =
+ synchronized(shareLaunchOverflowLock) {
+ pendingShareLaunchOverflowCount.also {
+ pendingShareLaunchOverflowCount = 0
+ }
+ }
+
/** Opens shared content as a fresh composer draft; sending still requires an explicit tap. */
- internal fun handleShareLaunch(
+ private fun enqueueShareLaunch(
request: ShareLaunchRequest,
owner: ChatComposerOwner,
): Boolean {
diff --git a/apps/android/app/src/main/java/ai/openclaw/app/NodeApp.kt b/apps/android/app/src/main/java/ai/openclaw/app/NodeApp.kt
index bb644120230e..c24f2954b88e 100644
--- a/apps/android/app/src/main/java/ai/openclaw/app/NodeApp.kt
+++ b/apps/android/app/src/main/java/ai/openclaw/app/NodeApp.kt
@@ -24,6 +24,7 @@ class NodeApp : Application() {
// System share senders can create overlapping Activity tasks; keep one bounded process queue.
internal val chatShareDraftSeq = AtomicLong()
internal val chatShareDraftQueue = ChatShareDraftQueue()
+ internal val permissionRequester by lazy { PermissionRequester(this) }
private val runtimeScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val runtimeLock = Any()
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 8bf1a3af817d..cd49a6f4b670 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
@@ -96,6 +96,7 @@ import ai.openclaw.app.node.asObjectOrNull
import ai.openclaw.app.node.asStringOrNull
import ai.openclaw.app.node.invokeErrorFromThrowable
import ai.openclaw.app.node.parseHexColorArgb
+import ai.openclaw.app.node.readAndroidPermissionSnapshot
import ai.openclaw.app.protocol.OpenClawCanvasA2UIAction
import ai.openclaw.app.systemagent.SystemAgentChatController
import ai.openclaw.app.systemagent.SystemAgentChatState
@@ -119,6 +120,7 @@ import ai.openclaw.app.wear.WearProxyBridge
import ai.openclaw.app.wear.WearProxyController
import ai.openclaw.app.wear.WearProxyGatewayException
import ai.openclaw.app.wear.WearProxyModel
+import ai.openclaw.app.wear.WearRealtimeAttemptOwner
import ai.openclaw.app.wear.WearRealtimeTalkController
import ai.openclaw.app.wear.wearConnectionFailure
import ai.openclaw.wear.shared.WearMessage
@@ -194,6 +196,36 @@ private fun execApprovalResolveFailureMessage(): String = nativeText("Could not
internal typealias GatewayDataRequestOverride =
suspend (stableId: String, method: String, paramsJson: String?) -> String
+internal suspend fun startWearRealtimeTalkWhileCurrent(
+ owner: WearRealtimeAttemptOwner,
+ isCurrent: suspend (WearRealtimeAttemptOwner) -> Boolean,
+ start: suspend (onSessionActivated: () -> Unit) -> Boolean,
+ stop: suspend (WearRealtimeAttemptOwner) -> Unit,
+): Boolean {
+ if (!isCurrent(owner)) return false
+ var relayStarted = false
+ var committed = false
+ try {
+ val startReturned =
+ start {
+ // The controller invokes this synchronously at activation, before a
+ // canceled caller can lose the successful suspend result.
+ relayStarted = true
+ }
+ if (!startReturned || !isCurrent(owner)) return false
+ committed = true
+ return true
+ } finally {
+ // Relay creation suspends outside the channel registry. Never leave a late
+ // session alive when replacement or cancellation wins before commit.
+ if (relayStarted && !committed) {
+ withContext(NonCancellable) {
+ stop(owner)
+ }
+ }
+ }
+}
+
private class ExecApprovalWriteOutcomeUnknown : IllegalStateException("approval resolve response was not authoritative")
private class GatewayApprovalRpcUnavailable : IllegalStateException("Gateway approval RPC catalog is inconsistent")
@@ -907,11 +939,23 @@ class NodeRuntime private constructor(
locationPreciseEnabled = { locationPreciseEnabled.value },
)
+ private val permissionSnapshot = {
+ readAndroidPermissionSnapshot(
+ context = appContext,
+ smsEnabled = SensitiveFeatureConfig.smsEnabled,
+ callLogEnabled = SensitiveFeatureConfig.callLogEnabled,
+ photosEnabled = SensitiveFeatureConfig.photosEnabled,
+ backgroundLocationEnabled = SensitiveFeatureConfig.backgroundLocationEnabled,
+ )
+ }
+
private val deviceHandler: DeviceHandler =
- DeviceHandler(
+ DeviceHandler.withPermissionSnapshot(
appContext = appContext,
smsEnabled = SensitiveFeatureConfig.smsEnabled,
callLogEnabled = SensitiveFeatureConfig.callLogEnabled,
+ photosEnabled = SensitiveFeatureConfig.photosEnabled,
+ permissionSnapshot = permissionSnapshot,
)
private val notificationsHandler: NotificationsHandler =
@@ -985,12 +1029,14 @@ class NodeRuntime private constructor(
SensitiveFeatureConfig.accessibilityControlEnabled && mobileUiHandler.isConnected.value
},
inlineWidgetsAvailable = { WebViewFeature.isFeatureSupported(WebViewFeature.MULTI_PROFILE) },
+ permissionSnapshot = permissionSnapshot,
manualTls = { endpoint ->
prefs.gatewayRegistry.entries.value
.firstOrNull { it.stableId == endpoint.stableId }
?.tls ?: manualTls.value
},
)
+ private var lastNodePermissions = connectionManager.buildPermissions()
private var lastVoiceWakeCapabilityEnabled = isVoiceWakeCapabilityEnabled()
private val invokeDispatcher: InvokeDispatcher =
@@ -1510,8 +1556,8 @@ class NodeRuntime private constructor(
},
connectGateway = { refreshGatewayConnection() },
disconnectGateway = { disconnect() },
- startRealtimeTalk = { nodeId, sessionKey, attemptId, language ->
- if (startWearRealtimeTalk(nodeId, sessionKey, attemptId, language)) wearRealtimeTalkSnapshot.value else null
+ startRealtimeTalk = { nodeId, sessionKey, attemptId, language, attemptScopedAudio ->
+ if (startWearRealtimeTalk(nodeId, sessionKey, attemptId, language, attemptScopedAudio)) wearRealtimeTalkSnapshot.value else null
},
stopRealtimeTalk = { nodeId, attemptId ->
if (stopWearRealtimeTalk(nodeId, attemptId)) wearRealtimeTalkSnapshot.value else null
@@ -2042,6 +2088,8 @@ class NodeRuntime private constructor(
val talkModeConversation: StateFlow>
get() = talkMode.conversation
+ private val wearRealtimeLifecycleMutex = Mutex()
+
private val wearRealtimeTalkControllerLazy: Lazy =
lazy {
WearRealtimeTalkController(
@@ -2057,15 +2105,17 @@ class NodeRuntime private constructor(
onError(error.message)
}
},
- sendWatchFrame = { nodeId, type, payload ->
+ sendWatchFrame = { owner, type, payload ->
val app = appContext as? NodeApp ?: error("Wear channel owner is unavailable")
- app.wearRealtimeChannels.send(nodeId, type, payload)
+ app.wearRealtimeChannels.send(owner, type, payload)
},
onSnapshot = { snapshot ->
wearProxyBridge()?.publishTalk(WearRealtimeTalkCodec.encode(snapshot))
},
- onForceCloseWatchChannel = { nodeId ->
- scope.launch { (appContext as? NodeApp)?.wearRealtimeChannels?.close(nodeId) }
+ onForceCloseWatchChannel = { owner ->
+ scope.launch {
+ (appContext as? NodeApp)?.wearRealtimeChannels?.close(owner)
+ }
},
)
}
@@ -2081,27 +2131,68 @@ class NodeRuntime private constructor(
sessionKey: String,
attemptId: String,
language: String?,
+ attemptScopedAudio: Boolean,
): Boolean {
if (talkModeEnabled.value || micEnabled.value || micCooldown.value) return false
+ val app = appContext as? NodeApp ?: return false
+ val claim =
+ app.wearRealtimeChannels.claim(
+ nodeId = nodeId,
+ attemptId = attemptId,
+ attemptScopedAudio = attemptScopedAudio,
+ ) ?: return false
+ val owner = claim.owner
val resolvedLanguage = talkMode.resolveRealtimeLanguageHint(language)
- return wearRealtimeTalkController.start(nodeId, sessionKey, attemptId, resolvedLanguage)
+ var started = false
+ return try {
+ started =
+ wearRealtimeLifecycleMutex.withLock {
+ if (talkModeEnabled.value || micEnabled.value || micCooldown.value) {
+ return@withLock false
+ }
+ startWearRealtimeTalkWhileCurrent(
+ owner = owner,
+ isCurrent = app.wearRealtimeChannels::isCurrent,
+ start = { onSessionActivated ->
+ wearRealtimeTalkController.start(
+ owner = owner,
+ sessionKey = sessionKey,
+ language = resolvedLanguage,
+ onSessionActivated = onSessionActivated,
+ )
+ },
+ stop = { staleOwner ->
+ wearRealtimeTalkController.stop(staleOwner)
+ },
+ )
+ }
+ started
+ } finally {
+ if (!started && claim.newlyAcquired) app.wearRealtimeChannels.release(owner)
+ }
}
internal suspend fun stopWearRealtimeTalk(
nodeId: String? = null,
attemptId: String? = null,
- ): Boolean {
- // The watch closes its channel after receiving the stop response. Closing
- // here races the response and makes a normal stop look like link failure.
- return wearRealtimeTalkController.stop(nodeId, attemptId)
- }
+ ): Boolean =
+ wearRealtimeLifecycleMutex.withLock {
+ // The watch closes its channel after receiving the stop response. Closing
+ // here races the response and makes a normal stop look like link failure.
+ wearRealtimeTalkController.stop(nodeId, attemptId)
+ }
+
+ internal suspend fun stopWearRealtimeTalk(owner: WearRealtimeAttemptOwner): Boolean =
+ wearRealtimeLifecycleMutex.withLock {
+ wearRealtimeTalkController.stop(owner)
+ }
internal fun appendWearRealtimeAudio(
- nodeId: String,
+ owner: WearRealtimeAttemptOwner,
payload: ByteArray,
) {
if (wearRealtimeTalkControllerLazy.isInitialized()) {
- wearRealtimeTalkController.appendAudio(nodeId, payload)
+ wearRealtimeTalkController.appendAudio(owner, payload)
}
}
@@ -2991,6 +3082,7 @@ class NodeRuntime private constructor(
voiceLifecycleEpoch.incrementAndGet()
}
if (value) {
+ refreshNodePermissionSurface()
refreshVoiceWakeCapabilitySurfaceIfChanged()
reconnectPreferredGatewayOnForeground()
scope.launch {
@@ -3263,6 +3355,15 @@ class NodeRuntime private constructor(
resolvePreferredGatewayEndpoint()?.let(::connect)
}
+ /**
+ * Reconnect a live node only when Android authority changed since its last connect.
+ */
+ fun refreshNodePermissionSurface() {
+ val permissions = connectionManager.buildPermissions()
+ if (permissions == lastNodePermissions) return
+ refreshNodeSurfaceAfterSettingsChange()
+ }
+
fun setDisplayName(value: String) {
prefs.setDisplayName(value)
}
@@ -4247,12 +4348,14 @@ class NodeRuntime private constructor(
tls,
)
}
+ val nodeConnectOptions = connectionManager.buildNodeConnectOptions()
+ lastNodePermissions = nodeConnectOptions.permissions
nodeSession.connect(
endpoint,
auth.token,
auth.bootstrapToken,
auth.password,
- connectionManager.buildNodeConnectOptions(),
+ nodeConnectOptions,
tls,
)
if (reconnect && operatorAuth != null) {
diff --git a/apps/android/app/src/main/java/ai/openclaw/app/PermissionRequester.kt b/apps/android/app/src/main/java/ai/openclaw/app/PermissionRequester.kt
index e6685593cceb..ed4088cfa7e9 100644
--- a/apps/android/app/src/main/java/ai/openclaw/app/PermissionRequester.kt
+++ b/apps/android/app/src/main/java/ai/openclaw/app/PermissionRequester.kt
@@ -2,6 +2,7 @@ package ai.openclaw.app
import ai.openclaw.app.i18n.nativeString
import android.Manifest
+import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
@@ -15,12 +16,20 @@ import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
+import java.util.IdentityHashMap
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.resume
@@ -28,28 +37,83 @@ import kotlin.coroutines.resume
* Serializes Android runtime-permission prompts behind coroutine-friendly request calls.
*/
class PermissionRequester internal constructor(
- private val activity: ComponentActivity,
- private val permissionRequestLauncher: (Array, Int) -> Unit,
+ context: Context,
private val requestCodeAllocator: PermissionRequestCodeAllocator = PermissionRequestCodeAllocator(),
) {
+ private data class ActivityHost(
+ val activity: ComponentActivity,
+ val permissionRequestLauncher: (Array, Int) -> Unit,
+ )
+
+ private data class ActiveActivityHost(
+ val host: ActivityHost,
+ val activation: Long,
+ )
+
private data class PendingPermissionRequest(
val requestCode: Int,
val permissions: List,
val deferred: CompletableDeferred