diff --git a/apps/.i18n/native-source.json b/apps/.i18n/native-source.json index 85ed92fc62f7..00cca0eeec77 100644 --- a/apps/.i18n/native-source.json +++ b/apps/.i18n/native-source.json @@ -195,7 +195,7 @@ }, { "kind": "ui-state-text", - "line": 640, + "line": 639, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Offline", "surface": "android", @@ -203,7 +203,7 @@ }, { "kind": "conditional-branch", - "line": 2914, + "line": 2913, "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", @@ -211,7 +211,7 @@ }, { "kind": "conditional-branch", - "line": 2916, + "line": 2915, "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", @@ -219,7 +219,7 @@ }, { "kind": "conditional-branch", - "line": 2918, + "line": 2917, "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", @@ -425,22 +425,6 @@ "surface": "android", "id": "native.android.5b17d331b254e403" }, - { - "kind": "conditional-branch", - "line": 54, - "path": "apps/android/app/src/main/java/ai/openclaw/app/tools/ToolDisplay.kt", - "source": "$emoji $label", - "surface": "android", - "id": "native.android.1623b8cd55455bfc" - }, - { - "kind": "conditional-branch", - "line": 54, - "path": "apps/android/app/src/main/java/ai/openclaw/app/tools/ToolDisplay.kt", - "source": "$emoji $label: $detailLine", - "surface": "android", - "id": "native.android.a393de564f65660f" - }, { "kind": "conditional-branch", "line": 47, @@ -979,7 +963,7 @@ }, { "kind": "conditional-branch", - "line": 86, + "line": 80, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayDiagnostics.kt", "source": "Setup code expired", "surface": "android", @@ -987,7 +971,7 @@ }, { "kind": "conditional-branch", - "line": 87, + "line": 81, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayDiagnostics.kt", "source": "Gateway token needed", "surface": "android", @@ -995,7 +979,7 @@ }, { "kind": "conditional-branch", - "line": 88, + "line": 82, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayDiagnostics.kt", "source": "Gateway token not configured", "surface": "android", @@ -1003,7 +987,7 @@ }, { "kind": "conditional-branch", - "line": 89, + "line": 83, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayDiagnostics.kt", "source": "Gateway password needed", "surface": "android", @@ -1011,7 +995,7 @@ }, { "kind": "conditional-branch", - "line": 90, + "line": 84, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayDiagnostics.kt", "source": "Gateway password invalid", "surface": "android", @@ -1019,7 +1003,7 @@ }, { "kind": "conditional-branch", - "line": 91, + "line": 85, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayDiagnostics.kt", "source": "Gateway password not configured", "surface": "android", @@ -1027,7 +1011,7 @@ }, { "kind": "conditional-branch", - "line": 92, + "line": 86, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayDiagnostics.kt", "source": "Gateway access needs review", "surface": "android", @@ -1035,7 +1019,7 @@ }, { "kind": "conditional-branch", - "line": 93, + "line": 87, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayDiagnostics.kt", "source": "Saved auth invalid", "surface": "android", @@ -1043,7 +1027,7 @@ }, { "kind": "conditional-branch", - "line": 94, + "line": 88, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayDiagnostics.kt", "source": "Device identity required", "surface": "android", @@ -1051,7 +1035,7 @@ }, { "kind": "ui-toast", - "line": 177, + "line": 171, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayDiagnostics.kt", "source": "Copied gateway diagnostics", "surface": "android", @@ -3825,526 +3809,6 @@ "surface": "android", "id": "native.android.f36439e78187c554" }, - { - "kind": "ui-call", - "line": 475, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "DEVICE", - "surface": "android", - "id": "native.android.46ba30c035d8f9b3" - }, - { - "kind": "ui-call", - "line": 486, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Name", - "surface": "android", - "id": "native.android.00d9f5fe4abd05a0" - }, - { - "kind": "ui-call", - "line": 496, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "$deviceModel · $appVersion", - "surface": "android", - "id": "native.android.d35345db50231440" - }, - { - "kind": "ui-call", - "line": 508, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Default Assistant", - "surface": "android", - "id": "native.android.a3959f171202f031" - }, - { - "kind": "ui-call", - "line": 544, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "MEDIA", - "surface": "android", - "id": "native.android.2949f2c21abc7e61" - }, - { - "kind": "ui-call", - "line": 555, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Microphone", - "surface": "android", - "id": "native.android.d8116d29802a3122" - }, - { - "kind": "conditional-branch", - "line": 558, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Granted", - "surface": "android", - "id": "native.android.4485058344287d1f" - }, - { - "kind": "conditional-branch", - "line": 558, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Required for voice transcription.", - "surface": "android", - "id": "native.android.e2a32f3002f5dccf" - }, - { - "kind": "ui-call", - "line": 585, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Camera", - "surface": "android", - "id": "native.android.72cb670153cdb9a2" - }, - { - "kind": "ui-call", - "line": 586, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Photos and video clips (foreground only).", - "surface": "android", - "id": "native.android.29a57979453ac97e" - }, - { - "kind": "ui-call", - "line": 594, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "NOTIFICATIONS", - "surface": "android", - "id": "native.android.893b84d511b75a97" - }, - { - "kind": "ui-call", - "line": 605, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "System Notifications", - "surface": "android", - "id": "native.android.d316b20dd0309255" - }, - { - "kind": "ui-call", - "line": 607, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Alerts and foreground service.", - "surface": "android", - "id": "native.android.ef0ca43d6ab9e92c" - }, - { - "kind": "ui-call", - "line": 632, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Notification Listener Access", - "surface": "android", - "id": "native.android.30bcd3f0ebf33d13" - }, - { - "kind": "ui-call", - "line": 634, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Required for `notifications.list`, `notifications.actions`, and forwarded notification events.", - "surface": "android", - "id": "native.android.f5c666d3707d9a31" - }, - { - "kind": "conditional-branch", - "line": 646, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Enable", - "surface": "android", - "id": "native.android.5ad85205f857a25b" - }, - { - "kind": "ui-call", - "line": 657, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "SMS", - "surface": "android", - "id": "native.android.581a2f434f14c5a2" - }, - { - "kind": "ui-call", - "line": 659, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Grants Android SMS access. The Gateway must separately allow `sms.search` and, only if needed, `sms.send`.", - "surface": "android", - "id": "native.android.cba20518e9758043" - }, - { - "kind": "ui-call", - "line": 696, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Forward Notification Events", - "surface": "android", - "id": "native.android.8c32ca1f11a93a25" - }, - { - "kind": "ui-call", - "line": 735, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Package Filter: Allowlist", - "surface": "android", - "id": "native.android.0ca0ef510d5c07d3" - }, - { - "kind": "ui-call", - "line": 737, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Only listed package IDs are forwarded.", - "surface": "android", - "id": "native.android.57bc87918c65bb9f" - }, - { - "kind": "ui-call", - "line": 753, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Package Filter: Blocklist", - "surface": "android", - "id": "native.android.c20fb2c785da76b8" - }, - { - "kind": "ui-call", - "line": 755, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "All packages except listed IDs are forwarded.", - "surface": "android", - "id": "native.android.7395a775f7d34568" - }, - { - "kind": "conditional-branch", - "line": 778, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Close App Picker", - "surface": "android", - "id": "native.android.1ffc2ba152a64d9b" - }, - { - "kind": "conditional-branch", - "line": 778, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Open App Picker", - "surface": "android", - "id": "native.android.f4ae672967a25eb4" - }, - { - "kind": "ui-call", - "line": 797, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Search apps", - "surface": "android", - "id": "native.android.a06d9e470d1d453f" - }, - { - "kind": "ui-call", - "line": 809, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Show System Apps", - "surface": "android", - "id": "native.android.b035f039d963c268" - }, - { - "kind": "ui-call", - "line": 811, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Include Android/system packages in results.", - "surface": "android", - "id": "native.android.1600a4884e2a41da" - }, - { - "kind": "ui-call", - "line": 850, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Quiet Hours", - "surface": "android", - "id": "native.android.e2ef6b37990a691c" - }, - { - "kind": "ui-call", - "line": 852, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Suppress forwarding during a local time window.", - "surface": "android", - "id": "native.android.a4f770f835357f6c" - }, - { - "kind": "ui-call", - "line": 874, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Quiet Start (HH:mm)", - "surface": "android", - "id": "native.android.e1869f00aa2189c7" - }, - { - "kind": "ui-call", - "line": 882, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Use 24-hour HH:mm format, for example 22:00.", - "surface": "android", - "id": "native.android.d608320273495697" - }, - { - "kind": "ui-call", - "line": 891, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Quiet End (HH:mm)", - "surface": "android", - "id": "native.android.ddcfd9ba56cc1aeb" - }, - { - "kind": "ui-call", - "line": 899, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Use 24-hour HH:mm format, for example 07:00.", - "surface": "android", - "id": "native.android.4921ed94f99d6e63" - }, - { - "kind": "ui-call", - "line": 918, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Save Quiet Hours", - "surface": "android", - "id": "native.android.556d0a3c15366f05" - }, - { - "kind": "ui-call", - "line": 926, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Max Events / Minute", - "surface": "android", - "id": "native.android.63501cb40aae19ea" - }, - { - "kind": "ui-call", - "line": 944, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Save Rate", - "surface": "android", - "id": "native.android.cad16090254466f2" - }, - { - "kind": "ui-call", - "line": 953, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Route Session Key (optional)", - "surface": "android", - "id": "native.android.a08855b4ebbbd10a" - }, - { - "kind": "ui-call", - "line": 960, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Blank keeps notification events on this device's default notification route. Set a key only to pin forwarding into a different session.", - "surface": "android", - "id": "native.android.db352fa71ce1e349" - }, - { - "kind": "ui-call", - "line": 982, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Save Session Route", - "surface": "android", - "id": "native.android.6051f516f0825a19" - }, - { - "kind": "ui-call", - "line": 990, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "DATA ACCESS", - "surface": "android", - "id": "native.android.908f014114bee723" - }, - { - "kind": "ui-call", - "line": 1002, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Photos", - "surface": "android", - "id": "native.android.da5835468d89c5c7" - }, - { - "kind": "ui-call", - "line": 1003, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Access recent photos.", - "surface": "android", - "id": "native.android.37cc59babec722da" - }, - { - "kind": "ui-call", - "line": 1028, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Contacts", - "surface": "android", - "id": "native.android.7a5393132c797c82" - }, - { - "kind": "ui-call", - "line": 1029, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Search and add contacts.", - "surface": "android", - "id": "native.android.2d3e44bab8c02b4e" - }, - { - "kind": "ui-call", - "line": 1055, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Calendar", - "surface": "android", - "id": "native.android.be1a5598d23e8b99" - }, - { - "kind": "ui-call", - "line": 1056, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Read and create events.", - "surface": "android", - "id": "native.android.dbd6257d7ed809a9" - }, - { - "kind": "ui-call", - "line": 1083, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Call Log", - "surface": "android", - "id": "native.android.735879662e992dcd" - }, - { - "kind": "ui-call", - "line": 1084, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Search recent call history.", - "surface": "android", - "id": "native.android.3c842261a945e43c" - }, - { - "kind": "conditional-branch", - "line": 1098, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Grant", - "surface": "android", - "id": "native.android.16306ca8baec0e6e" - }, - { - "kind": "conditional-branch", - "line": 1098, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Manage", - "surface": "android", - "id": "native.android.c5dd189c29c4983f" - }, - { - "kind": "ui-call", - "line": 1110, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Motion", - "surface": "android", - "id": "native.android.475e97f2df757f8b" - }, - { - "kind": "ui-call", - "line": 1111, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Track steps and activity.", - "surface": "android", - "id": "native.android.fa5092a818a770ab" - }, - { - "kind": "ui-call", - "line": 1140, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "LOCATION", - "surface": "android", - "id": "native.android.304217db5e6b3192" - }, - { - "kind": "ui-call", - "line": 1151, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Off", - "surface": "android", - "id": "native.android.474bf9e9dd0bb1ab" - }, - { - "kind": "ui-call", - "line": 1152, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Disable location sharing.", - "surface": "android", - "id": "native.android.7d3e67457d28228a" - }, - { - "kind": "ui-call", - "line": 1164, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "While Using", - "surface": "android", - "id": "native.android.5d60864f0652a0ab" - }, - { - "kind": "ui-call", - "line": 1165, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Only while OpenClaw is open.", - "surface": "android", - "id": "native.android.3b2109ae1192e191" - }, - { - "kind": "ui-call", - "line": 1177, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Precise Location", - "surface": "android", - "id": "native.android.b92944ae7d52f9e3" - }, - { - "kind": "ui-call", - "line": 1178, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Use precise GPS when available.", - "surface": "android", - "id": "native.android.a8bc0f574f3563b0" - }, - { - "kind": "ui-call", - "line": 1192, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "PREFERENCES", - "surface": "android", - "id": "native.android.8c3caf6577f67577" - }, - { - "kind": "ui-call", - "line": 1203, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Prevent Sleep", - "surface": "android", - "id": "native.android.a88b03a8b3e7a353" - }, - { - "kind": "ui-call", - "line": 1204, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Keep screen awake while open.", - "surface": "android", - "id": "native.android.a347983646f16109" - }, - { - "kind": "ui-call", - "line": 1211, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Debug Canvas", - "surface": "android", - "id": "native.android.95b2da6c6a76bb18" - }, - { - "kind": "ui-call", - "line": 1212, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt", - "source": "Show status overlay on canvas.", - "surface": "android", - "id": "native.android.9f346a677f3367e7" - }, { "kind": "ui-named-argument", "line": 437, @@ -5907,7 +5371,7 @@ }, { "kind": "ui-named-argument", - "line": 300, + "line": 322, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Chat needs attention", "surface": "android", @@ -5915,7 +5379,7 @@ }, { "kind": "ui-named-argument", - "line": 482, + "line": 504, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "All", "surface": "android", @@ -5923,7 +5387,7 @@ }, { "kind": "ui-named-argument", - "line": 540, + "line": 562, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "OpenClaw", "surface": "android", @@ -5931,7 +5395,7 @@ }, { "kind": "ui-named-argument", - "line": 561, + "line": 583, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "New chat", "surface": "android", @@ -5939,7 +5403,7 @@ }, { "kind": "ui-named-argument", - "line": 566, + "line": 588, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "More new chat options", "surface": "android", @@ -5947,7 +5411,7 @@ }, { "kind": "ui-named-argument", - "line": 581, + "line": 603, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Refresh chat", "surface": "android", @@ -5955,7 +5419,7 @@ }, { "kind": "ui-named-argument", - "line": 584, + "line": 606, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Chat", "surface": "android", @@ -5963,7 +5427,7 @@ }, { "kind": "ui-named-argument", - "line": 732, + "line": 754, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Loading session", "surface": "android", @@ -5971,7 +5435,7 @@ }, { "kind": "ui-named-argument", - "line": 759, + "line": 781, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Jump to latest", "surface": "android", @@ -5979,7 +5443,7 @@ }, { "kind": "conditional-branch", - "line": 785, + "line": 807, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Ready when you are", "surface": "android", @@ -5987,7 +5451,7 @@ }, { "kind": "ui-named-argument", - "line": 816, + "line": 838, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Fix connection", "surface": "android", @@ -5995,7 +5459,7 @@ }, { "kind": "ui-named-argument", - "line": 817, + "line": 839, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Copy diagnostics", "surface": "android", @@ -6003,7 +5467,7 @@ }, { "kind": "conditional-branch", - "line": 1001, + "line": 1023, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Preparing audio…", "surface": "android", @@ -6011,7 +5475,7 @@ }, { "kind": "conditional-branch", - "line": 1001, + "line": 1023, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Speaking…", "surface": "android", @@ -6019,7 +5483,7 @@ }, { "kind": "ui-named-argument", - "line": 1022, + "line": 1044, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Tools running", "surface": "android", @@ -6027,7 +5491,7 @@ }, { "kind": "ui-named-argument", - "line": 1024, + "line": 1046, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "OpenClaw is working", "surface": "android", @@ -6035,7 +5499,7 @@ }, { "kind": "ui-named-argument", - "line": 1027, + "line": 1049, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "+${toolCalls.size - 4} more", "surface": "android", @@ -6043,7 +5507,7 @@ }, { "kind": "ui-named-argument", - "line": 1037, + "line": 1059, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Thinking", "surface": "android", @@ -6051,7 +5515,7 @@ }, { "kind": "ui-named-argument", - "line": 1038, + "line": 1060, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "OpenClaw is preparing a response.", "surface": "android", @@ -6059,7 +5523,7 @@ }, { "kind": "ui-named-argument", - "line": 1200, + "line": 1222, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Stop", "surface": "android", @@ -6067,7 +5531,7 @@ }, { "kind": "ui-named-argument", - "line": 1266, + "line": 1288, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Default", "surface": "android", @@ -6075,7 +5539,7 @@ }, { "kind": "conditional-branch", - "line": 1332, + "line": 1354, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Pin model", "surface": "android", @@ -6083,7 +5547,7 @@ }, { "kind": "conditional-branch", - "line": 1332, + "line": 1354, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Unpin model", "surface": "android", @@ -6091,7 +5555,7 @@ }, { "kind": "ui-named-argument", - "line": 1349, + "line": 1371, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "No commands found", "surface": "android", @@ -6099,7 +5563,7 @@ }, { "kind": "ui-named-argument", - "line": 1411, + "line": 1433, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Gateway offline", "surface": "android", @@ -6107,7 +5571,7 @@ }, { "kind": "ui-named-argument", - "line": 1511, + "line": 1533, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Attach image", "surface": "android", @@ -6115,7 +5579,7 @@ }, { "kind": "ui-named-argument", - "line": 1546, + "line": 1568, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Message OpenClaw", "surface": "android", @@ -6123,7 +5587,7 @@ }, { "kind": "ui-named-argument", - "line": 1562, + "line": 1584, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Open voice", "surface": "android", @@ -6131,7 +5595,7 @@ }, { "kind": "ui-named-argument", - "line": 1611, + "line": 1633, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Remove attachment", "surface": "android", @@ -6139,7 +5603,7 @@ }, { "kind": "conditional-branch", - "line": 1632, + "line": 1654, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Main", "surface": "android", @@ -6147,7 +5611,7 @@ }, { "kind": "conditional-branch", - "line": 1640, + "line": 1662, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "$emoji $name", "surface": "android", @@ -6155,7 +5619,7 @@ }, { "kind": "ui-named-argument", - "line": 1699, + "line": 1721, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Send", "surface": "android", @@ -6163,20 +5627,12 @@ }, { "kind": "conditional-branch", - "line": 1739, + "line": 1761, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "$contextLabel · ${contextMeterThinkingLabel(thinkingLevel)}", "surface": "android", "id": "native.android.4e6c67df44d588d3" }, - { - "kind": "ui-named-argument", - "line": 297, - "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatSheetContent.kt", - "source": "CHAT ERROR", - "surface": "android", - "id": "native.android.826804c0189e03d0" - }, { "kind": "ui-named-argument", "line": 109, diff --git a/apps/android/app/src/main/java/ai/openclaw/app/AndroidScreenshotFixture.kt b/apps/android/app/src/main/java/ai/openclaw/app/AndroidScreenshotFixture.kt index 0f2815d8e921..7b664d9f67c4 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/AndroidScreenshotFixture.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/AndroidScreenshotFixture.kt @@ -8,7 +8,6 @@ import kotlinx.serialization.json.buildJsonObject internal object AndroidScreenshotFixture { const val mainSessionKey = "agent:main:node-screenshot" const val primarySessionTitle = "Android release planning" - const val chatReadyText = "Ready when you are" val agents = listOf( 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 9881881615c6..8325964a3e52 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 @@ -279,7 +279,6 @@ data class GatewayConnectionProblem( val minimumProbeProtocol: Int? = null, ) { val isPairingRequired: Boolean = code == "PAIRING_REQUIRED" - val isProtocolMismatch: Boolean = code == "PROTOCOL_MISMATCH" val canAutoRetry: Boolean = isPairingRequired && ( diff --git a/apps/android/app/src/main/java/ai/openclaw/app/tools/ToolDisplay.kt b/apps/android/app/src/main/java/ai/openclaw/app/tools/ToolDisplay.kt index e85be6420248..88dadf10c541 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/tools/ToolDisplay.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/tools/ToolDisplay.kt @@ -48,10 +48,6 @@ data class ToolDisplaySummary( if (!detail.isNullOrBlank()) parts.add(detail) return if (parts.isEmpty()) null else parts.joinToString(" · ") } - - /** Single-line fallback for compact tool rows that do not render detail separately. */ - val summaryLine: String - get() = if (detailLine != null) "$emoji $label: $detailLine" else "$emoji $label" } /** Resolves tool-call names and args into user-facing Android display text. */ diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayDiagnostics.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayDiagnostics.kt index 5e93af3ce9dd..5f7eefa8fe9d 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayDiagnostics.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayDiagnostics.kt @@ -24,12 +24,6 @@ internal fun openClawAndroidVersionLabel(): String { /** Normalizes blank gateway status text for display and diagnostics copy. */ internal fun gatewayStatusForDisplay(statusText: String): String = statusText.trim().ifEmpty { "Offline" } -/** Returns true when the status has enough signal to show diagnostics affordances. */ -internal fun gatewayStatusHasDiagnostics(statusText: String): Boolean { - val lower = gatewayStatusForDisplay(statusText).lowercase() - return lower != "offline" && !lower.contains("connecting") -} - /** Resolves the best non-secret endpoint label available to diagnostics surfaces. */ internal fun gatewayDiagnosticsEndpoint( remoteAddress: String?, diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/MobileUiTokens.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/MobileUiTokens.kt index 4258cf3a6f2c..8bd9baff18aa 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/MobileUiTokens.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/MobileUiTokens.kt @@ -122,7 +122,6 @@ internal object MobileColorsAccessor { // Keep these accessors while screens migrate to `MobileColorsAccessor.current`. // Each getter must stay composable so callers always read the active theme. internal val mobileSurface: Color @Composable get() = LocalMobileColors.current.surface -internal val mobileSurfaceStrong: Color @Composable get() = LocalMobileColors.current.surfaceStrong internal val mobileCardSurface: Color @Composable get() = LocalMobileColors.current.cardSurface internal val mobileBorder: Color @Composable get() = LocalMobileColors.current.border internal val mobileBorderStrong: Color @Composable get() = LocalMobileColors.current.borderStrong @@ -132,8 +131,6 @@ internal val mobileTextTertiary: Color @Composable get() = LocalMobileColors.cur internal val mobileAccent: Color @Composable get() = LocalMobileColors.current.accent internal val mobileAccentSoft: Color @Composable get() = LocalMobileColors.current.accentSoft internal val mobileAccentBorderStrong: Color @Composable get() = LocalMobileColors.current.accentBorderStrong -internal val mobileSuccess: Color @Composable get() = LocalMobileColors.current.success -internal val mobileSuccessSoft: Color @Composable get() = LocalMobileColors.current.successSoft internal val mobileWarning: Color @Composable get() = LocalMobileColors.current.warning internal val mobileWarningSoft: Color @Composable get() = LocalMobileColors.current.warningSoft internal val mobileDanger: Color @Composable get() = LocalMobileColors.current.danger @@ -141,7 +138,6 @@ internal val mobileDangerSoft: Color @Composable get() = LocalMobileColors.curre internal val mobileCodeBg: Color @Composable get() = LocalMobileColors.current.codeBg internal val mobileCodeText: Color @Composable get() = LocalMobileColors.current.codeText internal val mobileCodeBorder: Color @Composable get() = LocalMobileColors.current.codeBorder -internal val mobileCodeAccent: Color @Composable get() = LocalMobileColors.current.codeAccent // Build the page backdrop from semantic surfaces so light/dark palettes keep // their contrast relationship without duplicating raw color stops. @@ -165,15 +161,6 @@ internal val mobileFontFamily = Font(resId = R.font.manrope_700_bold, weight = FontWeight.Bold), ) -internal val mobileTitle1 = - TextStyle( - fontFamily = mobileFontFamily, - fontWeight = FontWeight.SemiBold, - fontSize = 24.sp, - lineHeight = 30.sp, - letterSpacing = (-0.5).sp, - ) - internal val mobileHeadline = TextStyle( fontFamily = mobileFontFamily, 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 deleted file mode 100644 index 1101e705ceb7..000000000000 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsSheet.kt +++ /dev/null @@ -1,1298 +0,0 @@ -package ai.openclaw.app.ui - -import ai.openclaw.app.BuildConfig -import ai.openclaw.app.LocationMode -import ai.openclaw.app.MainViewModel -import ai.openclaw.app.NotificationPackageFilterMode -import ai.openclaw.app.SensitiveFeatureConfig -import ai.openclaw.app.node.DeviceNotificationListenerService -import ai.openclaw.app.normalizeLocalHourMinute -import android.Manifest -import android.app.role.RoleManager -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.hardware.Sensor -import android.hardware.SensorManager -import android.net.Uri -import android.os.Build -import android.provider.Settings -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing -import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.ListItem -import androidx.compose.material3.ListItemDefaults -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults -import androidx.compose.material3.RadioButton -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.core.content.ContextCompat -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import androidx.lifecycle.compose.LocalLifecycleOwner - -/** Mobile settings surface for device permissions, forwarding, location, and app preferences. */ -@Composable -fun SettingsSheet(viewModel: MainViewModel) { - val context = LocalContext.current - val lifecycleOwner = LocalLifecycleOwner.current - val instanceId by viewModel.instanceId.collectAsState() - val displayName by viewModel.displayName.collectAsState() - val cameraEnabled by viewModel.cameraEnabled.collectAsState() - val locationMode by viewModel.locationMode.collectAsState() - val locationPreciseEnabled by viewModel.locationPreciseEnabled.collectAsState() - val preventSleep by viewModel.preventSleep.collectAsState() - val canvasDebugStatusEnabled by viewModel.canvasDebugStatusEnabled.collectAsState() - val notificationForwardingEnabled by viewModel.notificationForwardingEnabled.collectAsState() - val notificationForwardingMode by viewModel.notificationForwardingMode.collectAsState() - val notificationForwardingPackages by viewModel.notificationForwardingPackages.collectAsState() - val notificationForwardingQuietHoursEnabled by viewModel.notificationForwardingQuietHoursEnabled.collectAsState() - val notificationForwardingQuietStart by viewModel.notificationForwardingQuietStart.collectAsState() - val notificationForwardingQuietEnd by viewModel.notificationForwardingQuietEnd.collectAsState() - val notificationForwardingMaxEventsPerMinute by viewModel.notificationForwardingMaxEventsPerMinute.collectAsState() - val notificationForwardingSessionKey by viewModel.notificationForwardingSessionKey.collectAsState() - - var notificationQuietStartDraft by remember(notificationForwardingQuietStart) { - mutableStateOf(notificationForwardingQuietStart) - } - var notificationQuietEndDraft by remember(notificationForwardingQuietEnd) { - mutableStateOf(notificationForwardingQuietEnd) - } - var notificationRateDraft by remember(notificationForwardingMaxEventsPerMinute) { - mutableStateOf(notificationForwardingMaxEventsPerMinute.toString()) - } - var notificationSessionKeyDraft by remember(notificationForwardingSessionKey) { - mutableStateOf(notificationForwardingSessionKey.orEmpty()) - } - val normalizedQuietStartDraft = - remember(notificationQuietStartDraft) { - normalizeLocalHourMinute(notificationQuietStartDraft) - } - val normalizedQuietEndDraft = - remember(notificationQuietEndDraft) { - normalizeLocalHourMinute(notificationQuietEndDraft) - } - val quietHoursDraftValid = normalizedQuietStartDraft != null && normalizedQuietEndDraft != null - val selectedPackagesSummary = - remember(notificationForwardingMode, notificationForwardingPackages) { - when (notificationForwardingMode) { - NotificationPackageFilterMode.Allowlist -> - if (notificationForwardingPackages.isEmpty()) { - "Selected: none — allowlist mode forwards nothing until you add apps." - } else { - "Selected: ${notificationForwardingPackages.size} app(s) allowed." - } - NotificationPackageFilterMode.Blocklist -> - if (notificationForwardingPackages.isEmpty()) { - "Selected: none — blocklist mode forwards all apps except OpenClaw." - } else { - "Selected: ${notificationForwardingPackages.size} app(s) blocked." - } - } - } - val quietHoursCanEnable = notificationForwardingEnabled && quietHoursDraftValid - // Compare stored values against normalized drafts so equivalent HH:mm input - // does not keep the save button enabled. - val quietHoursDraftDirty = - notificationForwardingQuietStart != (normalizedQuietStartDraft ?: notificationQuietStartDraft.trim()) || - notificationForwardingQuietEnd != (normalizedQuietEndDraft ?: notificationQuietEndDraft.trim()) - val quietHoursSaveEnabled = notificationForwardingEnabled && quietHoursDraftValid && quietHoursDraftDirty - - val listState = rememberLazyListState() - val deviceModel = - remember { - listOfNotNull(Build.MANUFACTURER, Build.MODEL) - .joinToString(" ") - .trim() - .ifEmpty { "Android" } - } - val appVersion = - remember { - val versionName = BuildConfig.VERSION_NAME.trim().ifEmpty { "dev" } - if (BuildConfig.DEBUG && !versionName.contains("dev", ignoreCase = true)) { - "$versionName-dev" - } else { - versionName - } - } - var assistantRoleAvailable by remember(context) { mutableStateOf(isAssistantRoleAvailable(context)) } - var assistantRoleHeld by remember(context) { mutableStateOf(isAssistantRoleHeld(context)) } - val listItemColors = - ListItemDefaults.colors( - containerColor = Color.Transparent, - headlineColor = mobileText, - supportingColor = mobileTextSecondary, - trailingIconColor = mobileTextSecondary, - leadingIconColor = mobileTextSecondary, - ) - - val permissionLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { perms -> - val cameraOk = perms[Manifest.permission.CAMERA] == true - viewModel.setCameraEnabled(cameraOk) - } - - var pendingLocationRequest by remember { mutableStateOf(false) } - var pendingPreciseToggle by remember { mutableStateOf(false) } - - val locationPermissionLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { perms -> - val fineOk = perms[Manifest.permission.ACCESS_FINE_LOCATION] == true - val coarseOk = perms[Manifest.permission.ACCESS_COARSE_LOCATION] == true - val granted = fineOk || coarseOk - - if (pendingPreciseToggle) { - pendingPreciseToggle = false - viewModel.setLocationPreciseEnabled(fineOk) - return@rememberLauncherForActivityResult - } - - if (pendingLocationRequest) { - pendingLocationRequest = false - viewModel.setLocationMode(if (granted) LocationMode.WhileUsing else LocationMode.Off) - } - } - - var micPermissionGranted by - remember { - mutableStateOf( - ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == - PackageManager.PERMISSION_GRANTED, - ) - } - val audioPermissionLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> - micPermissionGranted = granted - } - - val smsPermissionAvailable = - remember { - SensitiveFeatureConfig.smsEnabled && - context.packageManager?.hasSystemFeature(PackageManager.FEATURE_TELEPHONY) == true - } - val callLogPermissionAvailable = remember { SensitiveFeatureConfig.callLogEnabled } - val photosPermissionAvailable = remember { SensitiveFeatureConfig.photosEnabled } - val photosPermission = - if (Build.VERSION.SDK_INT >= 33) { - Manifest.permission.READ_MEDIA_IMAGES - } else { - Manifest.permission.READ_EXTERNAL_STORAGE - } - val motionPermissionRequired = true - val motionAvailable = remember(context) { hasMotionCapabilities(context) } - - var notificationsPermissionGranted by - remember { - mutableStateOf(hasNotificationsPermission(context)) - } - val notificationsPermissionLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> - notificationsPermissionGranted = granted - } - - var notificationListenerEnabled by - remember { - mutableStateOf(isNotificationListenerEnabled(context)) - } - val notificationForwardingAvailable = notificationForwardingEnabled && notificationListenerEnabled - val notificationForwardingControlsAlpha = if (notificationForwardingAvailable) 1f else 0.6f - - var notificationPickerExpanded by remember { mutableStateOf(false) } - var notificationAppSearch by remember { mutableStateOf("") } - var notificationShowSystemApps by remember { mutableStateOf(false) } - var installedNotificationApps by - remember(context, notificationForwardingPackages) { - mutableStateOf(queryInstalledApps(context, notificationForwardingPackages)) - } - - var photosPermissionGranted by - remember { - mutableStateOf( - if (photosPermissionAvailable) { - ContextCompat.checkSelfPermission(context, photosPermission) == PackageManager.PERMISSION_GRANTED - } else { - false - }, - ) - } - val photosPermissionLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> - photosPermissionGranted = granted - } - - var contactsPermissionGranted by - remember { - mutableStateOf( - ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) == - PackageManager.PERMISSION_GRANTED && - ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_CONTACTS) == - PackageManager.PERMISSION_GRANTED, - ) - } - val contactsPermissionLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { perms -> - val readOk = perms[Manifest.permission.READ_CONTACTS] == true - val writeOk = perms[Manifest.permission.WRITE_CONTACTS] == true - contactsPermissionGranted = readOk && writeOk - } - - var calendarPermissionGranted by - remember { - mutableStateOf( - ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) == - PackageManager.PERMISSION_GRANTED && - ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_CALENDAR) == - PackageManager.PERMISSION_GRANTED, - ) - } - val calendarPermissionLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { perms -> - val readOk = perms[Manifest.permission.READ_CALENDAR] == true - val writeOk = perms[Manifest.permission.WRITE_CALENDAR] == true - calendarPermissionGranted = readOk && writeOk - } - - var callLogPermissionGranted by - remember { - mutableStateOf( - ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALL_LOG) == - PackageManager.PERMISSION_GRANTED, - ) - } - val callLogPermissionLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> - callLogPermissionGranted = granted - } - - var motionPermissionGranted by - remember { - mutableStateOf( - !motionPermissionRequired || - ContextCompat.checkSelfPermission(context, Manifest.permission.ACTIVITY_RECOGNITION) == - PackageManager.PERMISSION_GRANTED, - ) - } - val motionPermissionLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> - motionPermissionGranted = granted - } - - var smsPermissionGranted by - remember { - mutableStateOf( - ContextCompat.checkSelfPermission(context, Manifest.permission.SEND_SMS) == - PackageManager.PERMISSION_GRANTED || - ContextCompat.checkSelfPermission(context, Manifest.permission.READ_SMS) == - PackageManager.PERMISSION_GRANTED, - ) - } - val smsPermissionLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { - smsPermissionGranted = - ContextCompat.checkSelfPermission(context, Manifest.permission.SEND_SMS) == - PackageManager.PERMISSION_GRANTED || - ContextCompat.checkSelfPermission(context, Manifest.permission.READ_SMS) == - PackageManager.PERMISSION_GRANTED - viewModel.refreshGatewayConnection() - } - - val assistantRoleLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { - assistantRoleAvailable = isAssistantRoleAvailable(context) - assistantRoleHeld = isAssistantRoleHeld(context) - } - - DisposableEffect(lifecycleOwner, context) { - val observer = - LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_RESUME) { - // Permission and role screens live outside Compose; refresh all derived - // toggles whenever Android returns to this settings surface. - micPermissionGranted = - ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) == - PackageManager.PERMISSION_GRANTED - notificationsPermissionGranted = hasNotificationsPermission(context) - notificationListenerEnabled = isNotificationListenerEnabled(context) - installedNotificationApps = queryInstalledApps(context, notificationForwardingPackages) - photosPermissionGranted = - if (photosPermissionAvailable) { - ContextCompat.checkSelfPermission(context, photosPermission) == PackageManager.PERMISSION_GRANTED - } else { - false - } - contactsPermissionGranted = - ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) == - PackageManager.PERMISSION_GRANTED && - ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_CONTACTS) == - PackageManager.PERMISSION_GRANTED - calendarPermissionGranted = - ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) == - PackageManager.PERMISSION_GRANTED && - ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_CALENDAR) == - PackageManager.PERMISSION_GRANTED - callLogPermissionGranted = - ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALL_LOG) == - PackageManager.PERMISSION_GRANTED - motionPermissionGranted = - !motionPermissionRequired || - ContextCompat.checkSelfPermission(context, Manifest.permission.ACTIVITY_RECOGNITION) == - PackageManager.PERMISSION_GRANTED - smsPermissionGranted = - ContextCompat.checkSelfPermission(context, Manifest.permission.SEND_SMS) == - PackageManager.PERMISSION_GRANTED || - ContextCompat.checkSelfPermission(context, Manifest.permission.READ_SMS) == - PackageManager.PERMISSION_GRANTED - assistantRoleAvailable = isAssistantRoleAvailable(context) - assistantRoleHeld = isAssistantRoleHeld(context) - } - } - lifecycleOwner.lifecycle.addObserver(observer) - onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } - } - - fun setCameraEnabledChecked(checked: Boolean) { - if (!checked) { - viewModel.setCameraEnabled(false) - return - } - - val cameraOk = - ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == - PackageManager.PERMISSION_GRANTED - if (cameraOk) { - viewModel.setCameraEnabled(true) - } else { - permissionLauncher.launch(arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)) - } - } - - fun requestLocationPermissions() { - val fineOk = - ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == - PackageManager.PERMISSION_GRANTED - val coarseOk = - ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == - PackageManager.PERMISSION_GRANTED - if (fineOk || coarseOk) { - viewModel.setLocationMode(LocationMode.WhileUsing) - } else { - pendingLocationRequest = true - locationPermissionLauncher.launch( - arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION), - ) - } - } - - fun setPreciseLocationChecked(checked: Boolean) { - if (!checked) { - viewModel.setLocationPreciseEnabled(false) - return - } - val fineOk = - ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == - PackageManager.PERMISSION_GRANTED - if (fineOk) { - viewModel.setLocationPreciseEnabled(true) - } else { - pendingPreciseToggle = true - locationPermissionLauncher.launch(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)) - } - } - - val normalizedAppSearch = notificationAppSearch.trim().lowercase() - val filteredNotificationApps = - remember(installedNotificationApps, normalizedAppSearch, notificationShowSystemApps) { - installedNotificationApps - .asSequence() - .filter { app -> notificationShowSystemApps || !app.isSystemApp } - .filter { app -> - normalizedAppSearch.isEmpty() || - app.label.lowercase().contains(normalizedAppSearch) || - app.packageName.lowercase().contains(normalizedAppSearch) - }.toList() - } - - Box( - modifier = - Modifier - .fillMaxSize() - .background(mobileBackgroundGradient), - ) { - LazyColumn( - state = listState, - modifier = - Modifier - .fillMaxWidth() - .fillMaxHeight() - .imePadding() - .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom)), - contentPadding = PaddingValues(horizontal = 20.dp, vertical = 16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - // ── Node ── - item { - Text( - "DEVICE", - style = mobileCaption1.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.sp), - color = mobileAccent, - ) - } - item { - Column(modifier = Modifier.settingsRowModifier()) { - OutlinedTextField( - value = displayName, - onValueChange = viewModel::setDisplayName, - label = { Text("Name", style = mobileCaption1, color = mobileTextSecondary) }, - modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp, vertical = 10.dp), - textStyle = mobileBody.copy(color = mobileText), - colors = settingsTextFieldColors(), - ) - HorizontalDivider(color = mobileBorder) - Column( - modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp), - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { - Text("$deviceModel · $appVersion", style = mobileCallout, color = mobileTextSecondary) - Text( - instanceId.take(8) + "…", - style = mobileCaption1.copy(fontFamily = FontFamily.Monospace), - color = mobileTextTertiary, - ) - } - if (assistantRoleAvailable) { - HorizontalDivider(color = mobileBorder) - ListItem( - modifier = Modifier.fillMaxWidth(), - colors = listItemColors, - headlineContent = { Text("Default Assistant", style = mobileHeadline) }, - supportingContent = { - Text( - if (assistantRoleHeld) { - "OpenClaw is registered as the device assistant." - } else { - "Let Android launch OpenClaw from the assistant gesture. Google Assistant App Actions still work separately." - }, - style = mobileCallout, - ) - }, - trailingContent = { - Button( - onClick = { - assistantRoleLauncher.launch( - context - .getSystemService(RoleManager::class.java) - .createRequestRoleIntent(RoleManager.ROLE_ASSISTANT), - ) - }, - colors = settingsPrimaryButtonColors(), - shape = RoundedCornerShape(14.dp), - ) { - Text( - if (assistantRoleHeld) "Manage" else "Enable", - style = mobileCallout.copy(fontWeight = FontWeight.Bold), - ) - } - }, - ) - } - } - } - - // ── Media ── - item { - Text( - "MEDIA", - style = mobileCaption1.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.sp), - color = mobileAccent, - ) - } - item { - Column(modifier = Modifier.settingsRowModifier()) { - ListItem( - modifier = Modifier.fillMaxWidth(), - colors = listItemColors, - headlineContent = { Text("Microphone", style = mobileHeadline) }, - supportingContent = { - Text( - if (micPermissionGranted) "Granted" else "Required for voice transcription.", - style = mobileCallout, - ) - }, - trailingContent = { - Button( - onClick = { - if (micPermissionGranted) { - openAppSettings(context) - } else { - audioPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO) - } - }, - colors = settingsPrimaryButtonColors(), - shape = RoundedCornerShape(14.dp), - ) { - Text( - if (micPermissionGranted) "Manage" else "Grant", - style = mobileCallout.copy(fontWeight = FontWeight.Bold), - ) - } - }, - ) - HorizontalDivider(color = mobileBorder) - ListItem( - modifier = Modifier.fillMaxWidth(), - colors = listItemColors, - headlineContent = { Text("Camera", style = mobileHeadline) }, - supportingContent = { Text("Photos and video clips (foreground only).", style = mobileCallout) }, - trailingContent = { Switch(checked = cameraEnabled, onCheckedChange = ::setCameraEnabledChecked) }, - ) - } - } - - // ── Notifications & Messaging ── - item { - Text( - "NOTIFICATIONS", - style = mobileCaption1.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.sp), - color = mobileAccent, - ) - } - item { - Column(modifier = Modifier.settingsRowModifier()) { - ListItem( - modifier = Modifier.fillMaxWidth(), - colors = listItemColors, - headlineContent = { Text("System Notifications", style = mobileHeadline) }, - supportingContent = { - Text("Alerts and foreground service.", style = mobileCallout) - }, - trailingContent = { - Button( - onClick = { - if (notificationsPermissionGranted || Build.VERSION.SDK_INT < 33) { - openAppSettings(context) - } else { - notificationsPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) - } - }, - colors = settingsPrimaryButtonColors(), - shape = RoundedCornerShape(14.dp), - ) { - Text( - if (notificationsPermissionGranted) "Manage" else "Grant", - style = mobileCallout.copy(fontWeight = FontWeight.Bold), - ) - } - }, - ) - HorizontalDivider(color = mobileBorder) - ListItem( - modifier = Modifier.fillMaxWidth(), - colors = listItemColors, - headlineContent = { Text("Notification Listener Access", style = mobileHeadline) }, - supportingContent = { - Text( - "Required for `notifications.list`, `notifications.actions`, and forwarded notification events.", - style = mobileCallout, - ) - }, - trailingContent = { - Button( - onClick = { openNotificationListenerSettings(context) }, - colors = settingsPrimaryButtonColors(), - shape = RoundedCornerShape(14.dp), - ) { - Text( - if (notificationListenerEnabled) "Manage" else "Enable", - style = mobileCallout.copy(fontWeight = FontWeight.Bold), - ) - } - }, - ) - if (smsPermissionAvailable) { - HorizontalDivider(color = mobileBorder) - ListItem( - modifier = Modifier.fillMaxWidth(), - colors = listItemColors, - headlineContent = { Text("SMS", style = mobileHeadline) }, - supportingContent = { - Text( - "Grants Android SMS access. The Gateway must separately allow `sms.search` and, only if needed, `sms.send`.", - style = mobileCallout, - ) - }, - trailingContent = { - Button( - onClick = { - if (smsPermissionGranted) { - openAppSettings(context) - } else { - smsPermissionLauncher.launch( - arrayOf(Manifest.permission.SEND_SMS, Manifest.permission.READ_SMS), - ) - } - }, - colors = settingsPrimaryButtonColors(), - shape = RoundedCornerShape(14.dp), - ) { - Text( - if (smsPermissionGranted) { - "Manage" - } else { - "Grant" - }, - style = mobileCallout.copy(fontWeight = FontWeight.Bold), - ) - } - }, - ) - } - } - } - item { - ListItem( - modifier = Modifier.settingsRowModifier(), - colors = listItemColors, - headlineContent = { Text("Forward Notification Events", style = mobileHeadline) }, - supportingContent = { - Text( - if (notificationListenerEnabled) { - "Forward listener events into gateway node events. Off by default until you enable it." - } else { - "Notification listener access is off, so no notification events can be forwarded yet." - }, - style = mobileCallout, - ) - }, - trailingContent = { - Switch( - checked = notificationForwardingEnabled, - onCheckedChange = viewModel::setNotificationForwardingEnabled, - enabled = notificationListenerEnabled, - ) - }, - ) - } - item { - Text( - if (notificationListenerEnabled) { - "Forwarding is available when enabled below." - } else { - "Forwarding controls stay disabled until Notification Listener Access is enabled in system Settings." - }, - style = mobileCallout, - color = mobileTextSecondary, - ) - } - item { - Column( - modifier = Modifier.settingsRowModifier().alpha(notificationForwardingControlsAlpha), - verticalArrangement = Arrangement.spacedBy(0.dp), - ) { - ListItem( - modifier = Modifier.fillMaxWidth(), - colors = listItemColors, - headlineContent = { Text("Package Filter: Allowlist", style = mobileHeadline) }, - supportingContent = { - Text("Only listed package IDs are forwarded.", style = mobileCallout) - }, - trailingContent = { - RadioButton( - selected = notificationForwardingMode == NotificationPackageFilterMode.Allowlist, - onClick = { - viewModel.setNotificationForwardingMode(NotificationPackageFilterMode.Allowlist) - }, - enabled = notificationForwardingAvailable, - ) - }, - ) - HorizontalDivider(color = mobileBorder) - ListItem( - modifier = Modifier.fillMaxWidth(), - colors = listItemColors, - headlineContent = { Text("Package Filter: Blocklist", style = mobileHeadline) }, - supportingContent = { - Text("All packages except listed IDs are forwarded.", style = mobileCallout) - }, - trailingContent = { - RadioButton( - selected = notificationForwardingMode == NotificationPackageFilterMode.Blocklist, - onClick = { - viewModel.setNotificationForwardingMode(NotificationPackageFilterMode.Blocklist) - }, - enabled = notificationForwardingAvailable, - ) - }, - ) - } - } - item { - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { - Button( - onClick = { notificationPickerExpanded = !notificationPickerExpanded }, - enabled = notificationForwardingAvailable, - colors = settingsPrimaryButtonColors(), - shape = RoundedCornerShape(14.dp), - ) { - Text( - if (notificationPickerExpanded) "Close App Picker" else "Open App Picker", - style = mobileCallout.copy(fontWeight = FontWeight.Bold), - ) - } - } - } - item { - Text( - selectedPackagesSummary, - style = mobileCallout, - color = mobileTextSecondary, - ) - } - if (notificationPickerExpanded) { - item { - OutlinedTextField( - value = notificationAppSearch, - onValueChange = { notificationAppSearch = it }, - label = { - Text("Search apps", style = mobileCaption1, color = mobileTextSecondary) - }, - modifier = Modifier.fillMaxWidth(), - textStyle = mobileBody.copy(color = mobileText), - colors = settingsTextFieldColors(), - enabled = notificationForwardingAvailable, - ) - } - item { - ListItem( - modifier = Modifier.settingsRowModifier().alpha(notificationForwardingControlsAlpha), - colors = listItemColors, - headlineContent = { Text("Show System Apps", style = mobileHeadline) }, - supportingContent = { - Text("Include Android/system packages in results.", style = mobileCallout) - }, - trailingContent = { - Switch( - checked = notificationShowSystemApps, - onCheckedChange = { notificationShowSystemApps = it }, - enabled = notificationForwardingAvailable, - ) - }, - ) - } - items(filteredNotificationApps, key = { it.packageName }) { app -> - ListItem( - modifier = Modifier.settingsRowModifier().alpha(notificationForwardingControlsAlpha), - colors = listItemColors, - headlineContent = { Text(app.label, style = mobileHeadline) }, - supportingContent = { Text(app.packageName, style = mobileCallout) }, - trailingContent = { - Switch( - checked = notificationForwardingPackages.contains(app.packageName), - onCheckedChange = { checked -> - val next = notificationForwardingPackages.toMutableSet() - if (checked) { - next.add(app.packageName) - } else { - next.remove(app.packageName) - } - viewModel.setNotificationForwardingPackagesCsv(next.sorted().joinToString(",")) - }, - enabled = notificationForwardingAvailable, - ) - }, - ) - } - } - item { - ListItem( - modifier = Modifier.settingsRowModifier().alpha(notificationForwardingControlsAlpha), - colors = listItemColors, - headlineContent = { Text("Quiet Hours", style = mobileHeadline) }, - supportingContent = { - Text("Suppress forwarding during a local time window.", style = mobileCallout) - }, - trailingContent = { - Switch( - checked = notificationForwardingQuietHoursEnabled, - onCheckedChange = { - if (!quietHoursCanEnable && it) return@Switch - viewModel.setNotificationForwardingQuietHours( - enabled = it, - start = notificationQuietStartDraft, - end = notificationQuietEndDraft, - ) - }, - enabled = if (notificationForwardingQuietHoursEnabled) notificationForwardingAvailable else quietHoursCanEnable, - ) - }, - ) - } - item { - OutlinedTextField( - value = notificationQuietStartDraft, - onValueChange = { notificationQuietStartDraft = it }, - label = { Text("Quiet Start (HH:mm)", style = mobileCaption1, color = mobileTextSecondary) }, - modifier = Modifier.fillMaxWidth(), - textStyle = mobileBody.copy(color = mobileText), - colors = settingsTextFieldColors(), - enabled = notificationForwardingAvailable, - isError = notificationForwardingAvailable && normalizedQuietStartDraft == null, - supportingText = { - if (notificationForwardingAvailable && normalizedQuietStartDraft == null) { - Text("Use 24-hour HH:mm format, for example 22:00.", style = mobileCaption1, color = mobileDanger) - } - }, - ) - } - item { - OutlinedTextField( - value = notificationQuietEndDraft, - onValueChange = { notificationQuietEndDraft = it }, - label = { Text("Quiet End (HH:mm)", style = mobileCaption1, color = mobileTextSecondary) }, - modifier = Modifier.fillMaxWidth(), - textStyle = mobileBody.copy(color = mobileText), - colors = settingsTextFieldColors(), - enabled = notificationForwardingAvailable, - isError = notificationForwardingAvailable && normalizedQuietEndDraft == null, - supportingText = { - if (notificationForwardingAvailable && normalizedQuietEndDraft == null) { - Text("Use 24-hour HH:mm format, for example 07:00.", style = mobileCaption1, color = mobileDanger) - } - }, - ) - } - item { - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { - Button( - onClick = { - viewModel.setNotificationForwardingQuietHours( - enabled = notificationForwardingQuietHoursEnabled, - start = notificationQuietStartDraft, - end = notificationQuietEndDraft, - ) - }, - enabled = quietHoursSaveEnabled, - colors = settingsPrimaryButtonColors(), - shape = RoundedCornerShape(14.dp), - ) { - Text("Save Quiet Hours", style = mobileCallout.copy(fontWeight = FontWeight.Bold)) - } - } - } - item { - OutlinedTextField( - value = notificationRateDraft, - onValueChange = { notificationRateDraft = it.filter { c -> c.isDigit() } }, - label = { Text("Max Events / Minute", style = mobileCaption1, color = mobileTextSecondary) }, - modifier = Modifier.fillMaxWidth(), - textStyle = mobileBody.copy(color = mobileText), - colors = settingsTextFieldColors(), - enabled = notificationForwardingAvailable, - ) - } - item { - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { - Button( - onClick = { - val parsed = notificationRateDraft.toIntOrNull() ?: notificationForwardingMaxEventsPerMinute - viewModel.setNotificationForwardingMaxEventsPerMinute(parsed) - }, - enabled = notificationForwardingAvailable, - colors = settingsPrimaryButtonColors(), - shape = RoundedCornerShape(14.dp), - ) { - Text("Save Rate", style = mobileCallout.copy(fontWeight = FontWeight.Bold)) - } - } - } - item { - OutlinedTextField( - value = notificationSessionKeyDraft, - onValueChange = { notificationSessionKeyDraft = it }, - label = { - Text( - "Route Session Key (optional)", - style = mobileCaption1, - color = mobileTextSecondary, - ) - }, - placeholder = { - Text( - "Blank keeps notification events on this device's default notification route. Set a key only to pin forwarding into a different session.", - style = mobileCaption1, - color = mobileTextSecondary, - ) - }, - modifier = Modifier.fillMaxWidth(), - textStyle = mobileBody.copy(color = mobileText), - colors = settingsTextFieldColors(), - enabled = notificationForwardingAvailable, - ) - } - item { - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { - Button( - onClick = { - viewModel.setNotificationForwardingSessionKey(notificationSessionKeyDraft.trim().ifEmpty { null }) - }, - enabled = notificationForwardingAvailable, - colors = settingsPrimaryButtonColors(), - shape = RoundedCornerShape(14.dp), - ) { - Text("Save Session Route", style = mobileCallout.copy(fontWeight = FontWeight.Bold)) - } - } - } - item { HorizontalDivider(color = mobileBorder) } - - // ── Data Access ── - item { - Text( - "DATA ACCESS", - style = mobileCaption1.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.sp), - color = mobileAccent, - ) - } - item { - Column(modifier = Modifier.settingsRowModifier()) { - if (photosPermissionAvailable) { - ListItem( - modifier = Modifier.fillMaxWidth(), - colors = listItemColors, - headlineContent = { Text("Photos", style = mobileHeadline) }, - supportingContent = { Text("Access recent photos.", style = mobileCallout) }, - trailingContent = { - Button( - onClick = { - if (photosPermissionGranted) { - openAppSettings(context) - } else { - photosPermissionLauncher.launch(photosPermission) - } - }, - colors = settingsPrimaryButtonColors(), - shape = RoundedCornerShape(14.dp), - ) { - Text( - if (photosPermissionGranted) "Manage" else "Grant", - style = mobileCallout.copy(fontWeight = FontWeight.Bold), - ) - } - }, - ) - HorizontalDivider(color = mobileBorder) - } - ListItem( - modifier = Modifier.fillMaxWidth(), - colors = listItemColors, - headlineContent = { Text("Contacts", style = mobileHeadline) }, - supportingContent = { Text("Search and add contacts.", style = mobileCallout) }, - trailingContent = { - Button( - onClick = { - if (contactsPermissionGranted) { - openAppSettings(context) - } else { - contactsPermissionLauncher.launch( - arrayOf(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS), - ) - } - }, - colors = settingsPrimaryButtonColors(), - shape = RoundedCornerShape(14.dp), - ) { - Text( - if (contactsPermissionGranted) "Manage" else "Grant", - style = mobileCallout.copy(fontWeight = FontWeight.Bold), - ) - } - }, - ) - HorizontalDivider(color = mobileBorder) - ListItem( - modifier = Modifier.fillMaxWidth(), - colors = listItemColors, - headlineContent = { Text("Calendar", style = mobileHeadline) }, - supportingContent = { Text("Read and create events.", style = mobileCallout) }, - trailingContent = { - Button( - onClick = { - if (calendarPermissionGranted) { - openAppSettings(context) - } else { - calendarPermissionLauncher.launch( - arrayOf(Manifest.permission.READ_CALENDAR, Manifest.permission.WRITE_CALENDAR), - ) - } - }, - colors = settingsPrimaryButtonColors(), - shape = RoundedCornerShape(14.dp), - ) { - Text( - if (calendarPermissionGranted) "Manage" else "Grant", - style = mobileCallout.copy(fontWeight = FontWeight.Bold), - ) - } - }, - ) - if (callLogPermissionAvailable) { - HorizontalDivider(color = mobileBorder) - ListItem( - modifier = Modifier.fillMaxWidth(), - colors = listItemColors, - headlineContent = { Text("Call Log", style = mobileHeadline) }, - supportingContent = { Text("Search recent call history.", style = mobileCallout) }, - trailingContent = { - Button( - onClick = { - if (callLogPermissionGranted) { - openAppSettings(context) - } else { - callLogPermissionLauncher.launch(Manifest.permission.READ_CALL_LOG) - } - }, - colors = settingsPrimaryButtonColors(), - shape = RoundedCornerShape(14.dp), - ) { - Text( - if (callLogPermissionGranted) "Manage" else "Grant", - style = mobileCallout.copy(fontWeight = FontWeight.Bold), - ) - } - }, - ) - } - if (motionAvailable) { - HorizontalDivider(color = mobileBorder) - ListItem( - modifier = Modifier.fillMaxWidth(), - colors = listItemColors, - headlineContent = { Text("Motion", style = mobileHeadline) }, - supportingContent = { Text("Track steps and activity.", style = mobileCallout) }, - trailingContent = { - val motionButtonLabel = - when { - !motionPermissionRequired -> "Manage" - motionPermissionGranted -> "Manage" - else -> "Grant" - } - Button( - onClick = { - if (!motionPermissionRequired || motionPermissionGranted) { - openAppSettings(context) - } else { - motionPermissionLauncher.launch(Manifest.permission.ACTIVITY_RECOGNITION) - } - }, - colors = settingsPrimaryButtonColors(), - shape = RoundedCornerShape(14.dp), - ) { - Text(motionButtonLabel, style = mobileCallout.copy(fontWeight = FontWeight.Bold)) - } - }, - ) - } - } - } - - // ── Location ── - item { - Text( - "LOCATION", - style = mobileCaption1.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.sp), - color = mobileAccent, - ) - } - item { - Column(modifier = Modifier.settingsRowModifier()) { - ListItem( - modifier = Modifier.fillMaxWidth(), - colors = listItemColors, - headlineContent = { Text("Off", style = mobileHeadline) }, - supportingContent = { Text("Disable location sharing.", style = mobileCallout) }, - trailingContent = { - RadioButton( - selected = locationMode == LocationMode.Off, - onClick = { viewModel.setLocationMode(LocationMode.Off) }, - ) - }, - ) - HorizontalDivider(color = mobileBorder) - ListItem( - modifier = Modifier.fillMaxWidth(), - colors = listItemColors, - headlineContent = { Text("While Using", style = mobileHeadline) }, - supportingContent = { Text("Only while OpenClaw is open.", style = mobileCallout) }, - trailingContent = { - RadioButton( - selected = locationMode == LocationMode.WhileUsing, - onClick = { requestLocationPermissions() }, - ) - }, - ) - HorizontalDivider(color = mobileBorder) - ListItem( - modifier = Modifier.fillMaxWidth(), - colors = listItemColors, - headlineContent = { Text("Precise Location", style = mobileHeadline) }, - supportingContent = { Text("Use precise GPS when available.", style = mobileCallout) }, - trailingContent = { - Switch( - checked = locationPreciseEnabled, - onCheckedChange = ::setPreciseLocationChecked, - enabled = locationMode != LocationMode.Off, - ) - }, - ) - } - } - - // ── Preferences ── - item { - Text( - "PREFERENCES", - style = mobileCaption1.copy(fontWeight = FontWeight.Bold, letterSpacing = 1.sp), - color = mobileAccent, - ) - } - item { - Column(modifier = Modifier.settingsRowModifier()) { - ListItem( - modifier = Modifier.fillMaxWidth(), - colors = listItemColors, - headlineContent = { Text("Prevent Sleep", style = mobileHeadline) }, - supportingContent = { Text("Keep screen awake while open.", style = mobileCallout) }, - trailingContent = { Switch(checked = preventSleep, onCheckedChange = viewModel::setPreventSleep) }, - ) - HorizontalDivider(color = mobileBorder) - ListItem( - modifier = Modifier.fillMaxWidth(), - colors = listItemColors, - headlineContent = { Text("Debug Canvas", style = mobileHeadline) }, - supportingContent = { Text("Show status overlay on canvas.", style = mobileCallout) }, - trailingContent = { - Switch( - checked = canvasDebugStatusEnabled, - onCheckedChange = viewModel::setCanvasDebugStatusEnabled, - ) - }, - ) - } - } - - item { Spacer(modifier = Modifier.height(24.dp)) } - } - } -} - -/** Shared Material text-field colors for the legacy mobile settings sheet. */ -@Composable -private fun settingsTextFieldColors() = - OutlinedTextFieldDefaults.colors( - focusedContainerColor = mobileSurface, - unfocusedContainerColor = mobileSurface, - focusedBorderColor = mobileAccent, - unfocusedBorderColor = mobileBorder, - focusedTextColor = mobileText, - unfocusedTextColor = mobileText, - cursorColor = mobileAccent, - ) - -/** Applies the legacy mobile card border/background used by settings rows. */ -@Composable -private fun Modifier.settingsRowModifier() = - this - .fillMaxWidth() - .border(width = 1.dp, color = mobileBorder, shape = RoundedCornerShape(14.dp)) - .background(mobileCardSurface, RoundedCornerShape(14.dp)) - -/** Primary button colors for the legacy mobile settings sheet. */ -@Composable -private fun settingsPrimaryButtonColors() = - ButtonDefaults.buttonColors( - containerColor = mobileAccent, - contentColor = Color.White, - disabledContainerColor = mobileAccent.copy(alpha = 0.45f), - disabledContentColor = Color.White.copy(alpha = 0.9f), - ) - -/** Opens this app's Android settings page for permissions that require system UI. */ -private fun openAppSettings(context: Context) { - val intent = - Intent( - Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.fromParts("package", context.packageName, null), - ) - context.startActivity(intent) -} - -/** Opens notification-listener settings, falling back to app settings if the intent is unavailable. */ -private fun openNotificationListenerSettings(context: Context) { - val intent = Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS) - runCatching { - context.startActivity(intent) - }.getOrElse { - openAppSettings(context) - } -} - -/** Android 13+ notification permission check; earlier versions grant posting at install time. */ -private fun hasNotificationsPermission(context: Context): Boolean { - if (Build.VERSION.SDK_INT < 33) return true - return ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == - PackageManager.PERMISSION_GRANTED -} - -/** Mirrors the notification listener service access check for UI enablement. */ -private fun isNotificationListenerEnabled(context: Context): Boolean = DeviceNotificationListenerService.isAccessEnabled(context) - -/** Checks whether the device exposes motion sensors needed by motion-related capabilities. */ -private fun hasMotionCapabilities(context: Context): Boolean { - val sensorManager = context.getSystemService(SensorManager::class.java) ?: return false - return sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null || - sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) != null -} - -private fun isAssistantRoleAvailable(context: Context): Boolean = context.getSystemService(RoleManager::class.java).isRoleAvailable(RoleManager.ROLE_ASSISTANT) - -private fun isAssistantRoleHeld(context: Context): Boolean = context.getSystemService(RoleManager::class.java).isRoleHeld(RoleManager.ROLE_ASSISTANT) diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt index 69f6dcb84e1e..6fe24d2ae7a6 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt @@ -106,6 +106,28 @@ import java.util.Date import java.util.Locale import kotlin.math.roundToInt +/** Returns a pending assistant prompt only when chat can accept it immediately. */ +internal fun resolvePendingAssistantAutoSend( + pendingPrompt: String?, + healthOk: Boolean, + pendingRunCount: Int, +): String? { + val prompt = pendingPrompt?.trim()?.ifEmpty { null } ?: return null + if (!healthOk || pendingRunCount > 0) return null + return prompt +} + +/** Chooses the session key to load for initial chat hydration, if any. */ +internal fun resolveInitialChatLoadSessionKey( + sessionKey: String, + mainSessionKey: String, +): String? { + val current = sessionKey.trim() + val main = mainSessionKey.trim().ifEmpty { "main" } + if (current.isNotEmpty() && current != "main" && current != main) return null + return main +} + /** Full chat surface that wires MainViewModel state to messages, attachments, voice, and composer actions. */ @Composable fun ChatScreen( diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatSheetContent.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatSheetContent.kt deleted file mode 100644 index c6386ee54598..000000000000 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatSheetContent.kt +++ /dev/null @@ -1,304 +0,0 @@ -package ai.openclaw.app.ui.chat - -import ai.openclaw.app.MainViewModel -import ai.openclaw.app.chat.ChatSessionEntry -import ai.openclaw.app.ui.mobileAccent -import ai.openclaw.app.ui.mobileAccentBorderStrong -import ai.openclaw.app.ui.mobileBorderStrong -import ai.openclaw.app.ui.mobileCallout -import ai.openclaw.app.ui.mobileCaption1 -import ai.openclaw.app.ui.mobileCaption2 -import ai.openclaw.app.ui.mobileCardSurface -import ai.openclaw.app.ui.mobileDanger -import ai.openclaw.app.ui.mobileDangerSoft -import ai.openclaw.app.ui.mobileText -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -/** Returns a pending assistant prompt only when chat can accept it immediately. */ -internal fun resolvePendingAssistantAutoSend( - pendingPrompt: String?, - healthOk: Boolean, - pendingRunCount: Int, -): String? { - val prompt = pendingPrompt?.trim()?.ifEmpty { null } ?: return null - if (!healthOk || pendingRunCount > 0) return null - return prompt -} - -/** Chooses the session key to load for initial chat hydration, if any. */ -internal fun resolveInitialChatLoadSessionKey( - sessionKey: String, - mainSessionKey: String, -): String? { - val current = sessionKey.trim() - val main = mainSessionKey.trim().ifEmpty { "main" } - if (current.isNotEmpty() && current != "main" && current != main) return null - return main -} - -/** Main Android chat sheet content: session picker, message list, and composer. */ -@Composable -fun ChatSheetContent(viewModel: MainViewModel) { - val messages by viewModel.chatMessages.collectAsState() - val historyLoading by viewModel.chatHistoryLoading.collectAsState() - val errorText by viewModel.chatError.collectAsState() - val pendingRunCount by viewModel.pendingRunCount.collectAsState() - val healthOk by viewModel.chatHealthOk.collectAsState() - val sessionKey by viewModel.chatSessionKey.collectAsState() - val mainSessionKey by viewModel.mainSessionKey.collectAsState() - val thinkingLevel by viewModel.chatThinkingLevel.collectAsState() - val selectedModelRef by viewModel.chatSelectedModelRef.collectAsState() - // Gate from the controller's agent-scoped catalog — the same source the send gate reads — - // so the visible control can never disagree with what sends actually carry. - val modelCatalog by viewModel.chatModelCatalog.collectAsState() - val thinkingSupported = thinkingSupportedForSelection(selectedModelRef, modelCatalog) - val streamingAssistantText by viewModel.chatStreamingAssistantText.collectAsState() - val pendingToolCalls by viewModel.chatPendingToolCalls.collectAsState() - val sessions by viewModel.chatSessions.collectAsState() - val chatCommands by viewModel.chatCommands.collectAsState() - val chatDraft by viewModel.chatDraft.collectAsState() - val pendingAssistantAutoSend by viewModel.pendingAssistantAutoSend.collectAsState() - val assistantAutoSendInFlight by viewModel.assistantAutoSendInFlight.collectAsState() - val outboxItems by viewModel.chatOutboxItems.collectAsState() - val messageSpeechState by viewModel.chatMessageSpeech.collectAsState() - val gatewayConnectionDisplay by viewModel.gatewayConnectionDisplay.collectAsState() - val gatewayOffline = !gatewayConnectionDisplay.isConnected - val micEnabled by viewModel.micEnabled.collectAsState() - val micIsListening by viewModel.micIsListening.collectAsState() - val micCooldown by viewModel.micCooldown.collectAsState() - val talkModeEnabled by viewModel.talkModeEnabled.collectAsState() - val talkModeListening by viewModel.talkModeListening.collectAsState() - - DisposableEffect(viewModel) { - onDispose(viewModel::stopChatMessageSpeech) - } - - LaunchedEffect(Unit) { - val loadSessionKey = resolveInitialChatLoadSessionKey(sessionKey, mainSessionKey) - if (loadSessionKey != null) { - viewModel.loadChat(loadSessionKey) - } - viewModel.refreshChatCommands() - } - - LaunchedEffect(pendingAssistantAutoSend, assistantAutoSendInFlight, healthOk, pendingRunCount, thinkingLevel) { - // Assistant-launch prompts should wait for a healthy idle chat so they do - // not race an already-running turn. - if (!healthOk) return@LaunchedEffect - val prompt = - resolvePendingAssistantAutoSend( - pendingPrompt = pendingAssistantAutoSend, - healthOk = healthOk, - pendingRunCount = pendingRunCount, - ) ?: return@LaunchedEffect - viewModel.dispatchPendingAssistantAutoSend( - pendingPrompt = prompt, - thinking = thinkingLevel, - ) - } - - val context = LocalContext.current - val resolver = context.contentResolver - val scope = rememberCoroutineScope() - - val attachments = remember { mutableStateListOf() } - val micCaptureActive = micEnabled || micIsListening || micCooldown || talkModeEnabled || talkModeListening - val voiceNoteRecorder = - rememberVoiceNoteRecorderController( - viewModel = viewModel, - onFinished = attachments::add, - ) - val voiceNoteState by voiceNoteRecorder.state.collectAsState() - val voiceNoteElapsedMs by voiceNoteRecorder.elapsedMs.collectAsState() - - val pickImages = - rememberLauncherForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris -> - if (uris.isNullOrEmpty()) return@rememberLauncherForActivityResult - scope.launch(Dispatchers.IO) { - // Bound both count and encoded size before attachments enter Compose - // state; sending uses these already-compressed payloads directly. - val next = - uris.take(8).mapNotNull { uri -> - try { - loadSizedImageAttachment(resolver, uri) - } catch (_: Throwable) { - null - } - } - withContext(Dispatchers.Main) { - attachments.addAll(next) - } - } - } - - Column( - modifier = - Modifier - .fillMaxSize() - .padding(horizontal = 20.dp, vertical = 12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - ChatThreadSelector( - sessionKey = sessionKey, - sessions = sessions, - mainSessionKey = mainSessionKey, - onSelectSession = { key -> viewModel.switchChatSession(key) }, - ) - - if (!errorText.isNullOrBlank()) { - ChatErrorRail(errorText = errorText!!) - } - - ChatMessageListCard( - sessionKey = sessionKey, - messages = messages, - historyLoading = historyLoading, - pendingRunCount = pendingRunCount, - pendingToolCalls = pendingToolCalls, - streamingAssistantText = streamingAssistantText, - healthOk = healthOk, - gatewayOffline = gatewayOffline, - modifier = Modifier.weight(1f, fill = true), - outboxItems = - outboxItemsForSession( - items = outboxItems, - sessionKey = sessionKey, - mainSessionKey = mainSessionKey, - ), - onRetryOutbox = viewModel::retryChatOutboxCommand, - onDeleteOutbox = viewModel::deleteChatOutboxCommand, - onReplyMessage = viewModel::setChatReplyDraft, - speechState = messageSpeechState, - onToggleListen = viewModel::toggleChatMessageSpeech, - ) - - Row(modifier = Modifier.fillMaxWidth().imePadding()) { - ChatComposer( - draftText = chatDraft, - healthOk = healthOk, - thinkingLevel = thinkingLevel, - thinkingSupported = thinkingSupported, - pendingRunCount = pendingRunCount, - commands = chatCommands, - attachments = attachments, - onDraftApplied = viewModel::clearChatDraft, - onPickImages = { pickImages.launch("image/*") }, - onRemoveAttachment = { id -> attachments.removeAll { it.id == id } }, - voiceNoteState = voiceNoteState, - voiceNoteElapsedMs = voiceNoteElapsedMs, - recordVoiceNoteEnabled = pendingRunCount == 0 && !micCaptureActive, - onStartVoiceNote = { scope.launch { voiceNoteRecorder.start() } }, - onCancelVoiceNote = voiceNoteRecorder::cancel, - onFinishVoiceNote = voiceNoteRecorder::finish, - onSetThinkingLevel = { level -> viewModel.setChatThinkingLevel(level) }, - onRefresh = { - viewModel.refreshChat() - viewModel.refreshChatSessions(limit = 200) - viewModel.refreshChatCommands() - }, - onAbort = { viewModel.abortChat() }, - onSend = { text -> - val outgoing = attachments.map(PendingAttachment::toOutgoingAttachment) - val pendingAttachments = attachments.toList() - attachments.clear() - val accepted = viewModel.sendChatAwaitAcceptance(message = text, thinking = thinkingLevel, attachments = outgoing) - if (!accepted && attachments.isEmpty()) { - // Refused sends must not silently drop selected attachments either. - attachments.addAll(pendingAttachments) - } - accepted - }, - ) - } - } -} - -@Composable -private fun ChatThreadSelector( - sessionKey: String, - sessions: List, - mainSessionKey: String, - onSelectSession: (String) -> Unit, -) { - val sessionOptions = - remember(sessionKey, sessions, mainSessionKey) { - resolveSessionChoices(sessionKey, sessions, mainSessionKey = mainSessionKey) - } - - Row( - modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - for (entry in sessionOptions) { - val active = entry.key == sessionKey - Surface( - onClick = { onSelectSession(entry.key) }, - shape = RoundedCornerShape(14.dp), - color = if (active) mobileAccent else mobileCardSurface, - border = BorderStroke(1.dp, if (active) mobileAccentBorderStrong else mobileBorderStrong), - tonalElevation = 0.dp, - shadowElevation = 0.dp, - ) { - Text( - text = friendlySessionName(entry.displayName ?: entry.key), - style = mobileCaption1.copy(fontWeight = if (active) FontWeight.Bold else FontWeight.SemiBold), - color = if (active) Color.White else mobileText, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), - ) - } - } - } -} - -@Composable -private fun ChatErrorRail(errorText: String) { - Surface( - modifier = Modifier.fillMaxWidth(), - color = mobileDangerSoft, - shape = RoundedCornerShape(12.dp), - border = androidx.compose.foundation.BorderStroke(1.dp, mobileDanger), - ) { - Column(modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(2.dp)) { - Text( - text = "CHAT ERROR", - style = mobileCaption2.copy(letterSpacing = 0.6.sp), - color = mobileDanger, - ) - Text(text = errorText, style = mobileCallout, color = mobileText) - } - } -} diff --git a/apps/android/app/src/test/java/ai/openclaw/app/ui/SettingsSheetNotificationAppsTest.kt b/apps/android/app/src/test/java/ai/openclaw/app/ui/SettingsScreensNotificationAppsTest.kt similarity index 98% rename from apps/android/app/src/test/java/ai/openclaw/app/ui/SettingsSheetNotificationAppsTest.kt rename to apps/android/app/src/test/java/ai/openclaw/app/ui/SettingsScreensNotificationAppsTest.kt index effd5837c5c1..2a14db4c40a9 100644 --- a/apps/android/app/src/test/java/ai/openclaw/app/ui/SettingsSheetNotificationAppsTest.kt +++ b/apps/android/app/src/test/java/ai/openclaw/app/ui/SettingsScreensNotificationAppsTest.kt @@ -3,7 +3,7 @@ package ai.openclaw.app.ui import org.junit.Assert.assertEquals import org.junit.Test -class SettingsSheetNotificationAppsTest { +class SettingsScreensNotificationAppsTest { @Test fun resolveNotificationCandidatePackages_keepsConfiguredPackagesVisible() { val packages = diff --git a/apps/android/app/src/test/java/ai/openclaw/app/ui/chat/ChatSheetContentTest.kt b/apps/android/app/src/test/java/ai/openclaw/app/ui/chat/ChatScreenTest.kt similarity index 98% rename from apps/android/app/src/test/java/ai/openclaw/app/ui/chat/ChatSheetContentTest.kt rename to apps/android/app/src/test/java/ai/openclaw/app/ui/chat/ChatScreenTest.kt index cb24bb780b5c..f430860963e5 100644 --- a/apps/android/app/src/test/java/ai/openclaw/app/ui/chat/ChatSheetContentTest.kt +++ b/apps/android/app/src/test/java/ai/openclaw/app/ui/chat/ChatScreenTest.kt @@ -7,7 +7,7 @@ import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test -class ChatSheetContentTest { +class ChatScreenTest { @Test fun agentChipUsesEmojiAndFallsBackToId() { assertEquals( diff --git a/apps/android/style.md b/apps/android/style.md index f2b892ac6ff8..0c824a5af477 100644 --- a/apps/android/style.md +++ b/apps/android/style.md @@ -104,10 +104,10 @@ Hard rule: avoid ultra-thin weights on light backgrounds. ## 11. Source Of Truth -- `app/src/main/java/ai/openclaw/android/ui/OpenClawTheme.kt` -- `app/src/main/java/ai/openclaw/android/ui/OnboardingFlow.kt` -- `app/src/main/java/ai/openclaw/android/ui/RootScreen.kt` -- `app/src/main/java/ai/openclaw/android/ui/SettingsSheet.kt` -- `app/src/main/java/ai/openclaw/android/MainViewModel.kt` +- `app/src/main/java/ai/openclaw/app/ui/OpenClawTheme.kt` +- `app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt` +- `app/src/main/java/ai/openclaw/app/ui/RootScreen.kt` +- `app/src/main/java/ai/openclaw/app/ui/SettingsScreens.kt` +- `app/src/main/java/ai/openclaw/app/MainViewModel.kt` If style and implementation diverge, update both in the same change.