Skip to content

Commit c86de67

Browse files
lixuankailixuankaiobviyus
authored
feat(android): support android node sms.search (#48299)
* feat(android): support android node sms.search * feat(android): support android node sms.search * fix(android): split sms search permissions * fix: document android sms.search landing (#48299) (thanks @lixuankai) --------- Co-authored-by: lixuankai <[email protected]> Co-authored-by: Ayaan Zaidi <[email protected]>
1 parent 58cf9b8 commit c86de67

File tree

19 files changed

+640
-28
lines changed

19 files changed

+640
-28
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ Docs: https://docs.openclaw.ai
2323
- Feishu/cards: add structured interactive approval and quick-action launcher cards, preserve callback user and conversation context through routing, and keep legacy card-action fallback behavior so common actions can run without typing raw commands. (#47873) Thanks @Takhoffman.
2424
- Feishu/streaming: add `onReasoningStream` and `onReasoningEnd` support to streaming cards, so `/reasoning stream` renders thinking tokens as markdown blockquotes in the same card — matching the Telegram channel's reasoning lane behavior. (#46029) Thanks @day253.
2525
- Feishu/cards: add identity-aware structured card headers and note footers for Feishu replies and direct sends, while keeping that presentation wired through the shared outbound identity path. (#29938) Thanks @nszhsl.
26-
- Android/nodes: add `callLog.search` plus shared Call Log permission wiring so Android nodes can search recent call history through the gateway. (#44073) Thanks @lxk7280.
26+
- Android/nodes: add `callLog.search` plus shared Call Log permission wiring so Android nodes can search recent call history through the gateway. (#44073) Thanks @lixuankai.
27+
- Android/nodes: add `sms.search` plus shared SMS permission wiring so Android nodes can search device text messages through the gateway. (#48299) Thanks @lixuankai.
2728
- Plugins/MiniMax: merge the bundled MiniMax API and MiniMax OAuth plugin surfaces into a single default-on `minimax` plugin, while keeping legacy `minimax-portal-auth` config ids aliased for compatibility.
2829
- Telegram/actions: add `topic-edit` for forum-topic renames and icon updates while sharing the same Telegram topic-edit transport used by the plugin runtime. (#47798) Thanks @obviyus.
2930
- Telegram/error replies: add a default-off `channels.telegram.silentErrorReplies` setting so bot error replies can be delivered silently across regular replies, native commands, and fallback sends. (#19776) Thanks @ImLukeF.

apps/android/README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,45 @@ More details: `docs/platforms/android.md`.
176176
- `CAMERA` for `camera.snap` and `camera.clip`
177177
- `RECORD_AUDIO` for `camera.clip` when `includeAudio=true`
178178

179+
## Google Play Restricted Permissions
180+
181+
As of March 19, 2026, these manifest permissions are the main Google Play policy risk for this app:
182+
183+
- `READ_SMS`
184+
- `SEND_SMS`
185+
- `READ_CALL_LOG`
186+
187+
Why these matter:
188+
189+
- Google Play treats SMS and Call Log access as highly restricted. In most cases, Play only allows them for the default SMS app, default Phone app, default Assistant, or a narrow policy exception.
190+
- Review usually involves a `Permissions Declaration Form`, policy justification, and demo video evidence in Play Console.
191+
- If we want a Play-safe build, these should be the first permissions removed behind a dedicated product flavor / variant.
192+
193+
Current OpenClaw Android implication:
194+
195+
- APK / sideload build can keep SMS and Call Log features.
196+
- Google Play build should exclude SMS send/search and Call Log search unless the product is intentionally positioned and approved as a default-handler exception case.
197+
198+
Policy links:
199+
200+
- [Google Play SMS and Call Log policy](https://support.google.com/googleplay/android-developer/answer/10208820?hl=en)
201+
- [Google Play sensitive permissions policy hub](https://support.google.com/googleplay/android-developer/answer/16558241)
202+
- [Android default handlers guide](https://developer.android.com/guide/topics/permissions/default-handlers)
203+
204+
Other Play-restricted surfaces to watch if added later:
205+
206+
- `ACCESS_BACKGROUND_LOCATION`
207+
- `MANAGE_EXTERNAL_STORAGE`
208+
- `QUERY_ALL_PACKAGES`
209+
- `REQUEST_INSTALL_PACKAGES`
210+
- `AccessibilityService`
211+
212+
Reference links:
213+
214+
- [Background location policy](https://support.google.com/googleplay/android-developer/answer/9799150)
215+
- [AccessibilityService policy](https://support.google.com/googleplay/android-developer/answer/10964491?hl=en-GB)
216+
- [Photo and Video Permissions policy](https://support.google.com/googleplay/android-developer/answer/14594990)
217+
179218
## Integration Capability Test (Preconditioned)
180219

181220
This suite assumes setup is already done manually. It does **not** install/run/pair automatically.

apps/android/app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<uses-permission android:name="android.permission.CAMERA" />
1313
<uses-permission android:name="android.permission.RECORD_AUDIO" />
1414
<uses-permission android:name="android.permission.SEND_SMS" />
15+
<uses-permission android:name="android.permission.READ_SMS" />
1516
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
1617
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
1718
<uses-permission

apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,8 @@ class NodeRuntime(
137137
voiceWakeMode = { VoiceWakeMode.Off },
138138
motionActivityAvailable = { motionHandler.isActivityAvailable() },
139139
motionPedometerAvailable = { motionHandler.isPedometerAvailable() },
140-
smsAvailable = { sms.canSendSms() },
140+
sendSmsAvailable = { sms.canSendSms() },
141+
readSmsAvailable = { sms.canReadSms() },
141142
hasRecordAudioPermission = { hasRecordAudioPermission() },
142143
manualTls = { manualTls.value },
143144
)
@@ -160,7 +161,8 @@ class NodeRuntime(
160161
isForeground = { _isForeground.value },
161162
cameraEnabled = { cameraEnabled.value },
162163
locationEnabled = { locationMode.value != LocationMode.Off },
163-
smsAvailable = { sms.canSendSms() },
164+
sendSmsAvailable = { sms.canSendSms() },
165+
readSmsAvailable = { sms.canReadSms() },
164166
debugBuild = { BuildConfig.DEBUG },
165167
refreshNodeCanvasCapability = { nodeSession.refreshNodeCanvasCapability() },
166168
onCanvasA2uiPush = {

apps/android/app/src/main/java/ai/openclaw/app/node/ConnectionManager.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ class ConnectionManager(
1717
private val voiceWakeMode: () -> VoiceWakeMode,
1818
private val motionActivityAvailable: () -> Boolean,
1919
private val motionPedometerAvailable: () -> Boolean,
20-
private val smsAvailable: () -> Boolean,
20+
private val sendSmsAvailable: () -> Boolean,
21+
private val readSmsAvailable: () -> Boolean,
2122
private val hasRecordAudioPermission: () -> Boolean,
2223
private val manualTls: () -> Boolean,
2324
) {
@@ -78,7 +79,8 @@ class ConnectionManager(
7879
NodeRuntimeFlags(
7980
cameraEnabled = cameraEnabled(),
8081
locationEnabled = locationMode() != LocationMode.Off,
81-
smsAvailable = smsAvailable(),
82+
sendSmsAvailable = sendSmsAvailable(),
83+
readSmsAvailable = readSmsAvailable(),
8284
voiceWakeEnabled = voiceWakeMode() != VoiceWakeMode.Off && hasRecordAudioPermission(),
8385
motionActivityAvailable = motionActivityAvailable(),
8486
motionPedometerAvailable = motionPedometerAvailable(),

apps/android/app/src/main/java/ai/openclaw/app/node/InvokeCommandRegistry.kt

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import ai.openclaw.app.protocol.OpenClawSystemCommand
1818
data class NodeRuntimeFlags(
1919
val cameraEnabled: Boolean,
2020
val locationEnabled: Boolean,
21-
val smsAvailable: Boolean,
21+
val sendSmsAvailable: Boolean,
22+
val readSmsAvailable: Boolean,
2223
val voiceWakeEnabled: Boolean,
2324
val motionActivityAvailable: Boolean,
2425
val motionPedometerAvailable: Boolean,
@@ -29,7 +30,8 @@ enum class InvokeCommandAvailability {
2930
Always,
3031
CameraEnabled,
3132
LocationEnabled,
32-
SmsAvailable,
33+
SendSmsAvailable,
34+
ReadSmsAvailable,
3335
MotionActivityAvailable,
3436
MotionPedometerAvailable,
3537
DebugBuild,
@@ -187,7 +189,11 @@ object InvokeCommandRegistry {
187189
),
188190
InvokeCommandSpec(
189191
name = OpenClawSmsCommand.Send.rawValue,
190-
availability = InvokeCommandAvailability.SmsAvailable,
192+
availability = InvokeCommandAvailability.SendSmsAvailable,
193+
),
194+
InvokeCommandSpec(
195+
name = OpenClawSmsCommand.Search.rawValue,
196+
availability = InvokeCommandAvailability.ReadSmsAvailable,
191197
),
192198
InvokeCommandSpec(
193199
name = OpenClawCallLogCommand.Search.rawValue,
@@ -213,7 +219,7 @@ object InvokeCommandRegistry {
213219
NodeCapabilityAvailability.Always -> true
214220
NodeCapabilityAvailability.CameraEnabled -> flags.cameraEnabled
215221
NodeCapabilityAvailability.LocationEnabled -> flags.locationEnabled
216-
NodeCapabilityAvailability.SmsAvailable -> flags.smsAvailable
222+
NodeCapabilityAvailability.SmsAvailable -> flags.sendSmsAvailable || flags.readSmsAvailable
217223
NodeCapabilityAvailability.VoiceWakeEnabled -> flags.voiceWakeEnabled
218224
NodeCapabilityAvailability.MotionAvailable -> flags.motionActivityAvailable || flags.motionPedometerAvailable
219225
}
@@ -228,7 +234,8 @@ object InvokeCommandRegistry {
228234
InvokeCommandAvailability.Always -> true
229235
InvokeCommandAvailability.CameraEnabled -> flags.cameraEnabled
230236
InvokeCommandAvailability.LocationEnabled -> flags.locationEnabled
231-
InvokeCommandAvailability.SmsAvailable -> flags.smsAvailable
237+
InvokeCommandAvailability.SendSmsAvailable -> flags.sendSmsAvailable
238+
InvokeCommandAvailability.ReadSmsAvailable -> flags.readSmsAvailable
232239
InvokeCommandAvailability.MotionActivityAvailable -> flags.motionActivityAvailable
233240
InvokeCommandAvailability.MotionPedometerAvailable -> flags.motionPedometerAvailable
234241
InvokeCommandAvailability.DebugBuild -> flags.debugBuild

apps/android/app/src/main/java/ai/openclaw/app/node/InvokeDispatcher.kt

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ class InvokeDispatcher(
3232
private val isForeground: () -> Boolean,
3333
private val cameraEnabled: () -> Boolean,
3434
private val locationEnabled: () -> Boolean,
35-
private val smsAvailable: () -> Boolean,
35+
private val sendSmsAvailable: () -> Boolean,
36+
private val readSmsAvailable: () -> Boolean,
3637
private val debugBuild: () -> Boolean,
3738
private val refreshNodeCanvasCapability: suspend () -> Boolean,
3839
private val onCanvasA2uiPush: () -> Unit,
@@ -162,6 +163,7 @@ class InvokeDispatcher(
162163

163164
// SMS command
164165
OpenClawSmsCommand.Send.rawValue -> smsHandler.handleSmsSend(paramsJson)
166+
OpenClawSmsCommand.Search.rawValue -> smsHandler.handleSmsSearch(paramsJson)
165167

166168
// CallLog command
167169
OpenClawCallLogCommand.Search.rawValue -> callLogHandler.handleCallLogSearch(paramsJson)
@@ -256,8 +258,17 @@ class InvokeDispatcher(
256258
message = "PEDOMETER_UNAVAILABLE: step counter not available",
257259
)
258260
}
259-
InvokeCommandAvailability.SmsAvailable ->
260-
if (smsAvailable()) {
261+
InvokeCommandAvailability.SendSmsAvailable ->
262+
if (sendSmsAvailable()) {
263+
null
264+
} else {
265+
GatewaySession.InvokeResult.error(
266+
code = "SMS_UNAVAILABLE",
267+
message = "SMS_UNAVAILABLE: SMS not available on this device",
268+
)
269+
}
270+
InvokeCommandAvailability.ReadSmsAvailable ->
271+
if (readSmsAvailable()) {
261272
null
262273
} else {
263274
GatewaySession.InvokeResult.error(

apps/android/app/src/main/java/ai/openclaw/app/node/SmsHandler.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,16 @@ class SmsHandler(
1616
return GatewaySession.InvokeResult.error(code = code, message = error)
1717
}
1818
}
19+
20+
suspend fun handleSmsSearch(paramsJson: String?): GatewaySession.InvokeResult {
21+
val res = sms.search(paramsJson)
22+
if (res.ok) {
23+
return GatewaySession.InvokeResult.ok(res.payloadJson)
24+
} else {
25+
val error = res.error ?: "SMS_SEARCH_FAILED"
26+
val idx = error.indexOf(':')
27+
val code = if (idx > 0) error.substring(0, idx).trim() else "SMS_SEARCH_FAILED"
28+
return GatewaySession.InvokeResult.error(code = code, message = error)
29+
}
30+
}
1931
}

0 commit comments

Comments
 (0)