Files
openclaw/apps/android
Peter Steinberger b22c36f112 fix: land ten small reliability fixes (#100399)
* fix(cron): reject sub-millisecond durations

* fix(skill-workshop): preserve proposal terminal newline

Preserve proposal_content exactly at the agent tool boundary and make
renderProposalMarkdown defensively emit a terminal newline.

Add focused regressions for the tool write path and markdown renderer.

* fix(skill-workshop): reject blank raw proposal content

* fix: treat empty-string optional integer tool params as unset

Optional positive-integer tool params (e.g. Telegram replyTo/threadId)
threw ToolInputError when a tool-calling model populated them with an
empty-string or whitespace-only default. Those defaults carry no value,
so readPositiveIntegerParam/readNonNegativeIntegerParam now treat a
blank string as unset (undefined) instead of throwing, while still
rejecting genuinely invalid present values (0, "42.5", "-3"). This
prevents silent message-delivery failures when models emit empty
routing-param defaults. Adds unit tests covering blank vs invalid.

* fix(gateway): log start session persistence failures

The gateway session-lifecycle "start" event persistence
(persistGatewaySessionLifecycleEvent, which surfaces write failures via
requireWriteSuccess) was fired as void ...catch(() => undefined), swallowing
the rejection with zero logging. A failed start-marker write silently dropped
the run's start record from restart-recovery accounting, with no
operator-visible trace.

The sibling terminal-phase catch already logs this since #97839; the start
path was the unfixed sibling. Mirror that fix: log the swallowed start-phase
persistence failure with the same redacted message shape via formatForLog,
keeping the fire-and-forget semantics unchanged. Adds a focused regression
test asserting the log fires on start-persist rejection.

* fix(memory): report close-time pending work failures

* fix(shared): return "" from sliceUtf16Safe when end <= start, matching native .slice

sliceUtf16Safe silently swapped reversed bounds (to < from) instead of
returning "" like String.prototype.slice, creating a subtle footgun for
callers with dynamic start/end pairs.

Caller scan across src/, extensions/, and packages/ confirmed no production
code relies on the old swap behavior — all callers use (text, 0, N) or
(text, -N) forms only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(plugins): require plugin manifest in npm verifier

* fix(android): propagate CancellationException in CameraHandler catch blocks

Catch (err: Throwable) swallows kotlinx.coroutines.CancellationException,
breaking structured concurrency when the coroutine scope is cancelled
during camera operations (handleList/handleSnap/handleClip).

Add CancellationException rethrow before each Throwable catch to match
the existing pattern used in GatewaySession and TalkModeManager.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(android): propagate CancellationException past GatewaySession invoke boundary

* chore: sync native i18n inventory after gateway session line shift

* fix(agents): prevent native hook relay bridge race condition on renew and registration

Remove synchronous bridge record write after server.listen() that races
before the TCP server binds, and guard renew handler with server.listening
check to prevent stale relay registrations.

Closes #98650

* fix: harden small reliability fixes

Co-authored-by: cxbAsDev <cxbAsDev@users.noreply.github.com>

* docs(agents): explain buffered LSP spawn failures

* docs(agents): clarify LSP spawn timeout invariant

---------

Co-authored-by: qingminlong <qing.minlong@xydigit.com>
Co-authored-by: anyech <anyech@gmail.com>
Co-authored-by: snotty <snotty@users.noreply.github.com>
Co-authored-by: masatohoshino <g515hoshino@gmail.com>
Co-authored-by: lin-hongkuan <lin-hongkuan@users.noreply.github.com>
Co-authored-by: simon-w <weng.qimeng@xydigit.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: 宇宙熊Yzx <53250620+849261680@users.noreply.github.com>
Co-authored-by: xialonglee <li.xialong@xydigit.com>
Co-authored-by: nankingjing <1079826437@qq.com>
Co-authored-by: cxbAsDev <cxbAsDev@users.noreply.github.com>
2026-07-05 11:12:55 -07:00
..
2026-07-01 18:09:57 -05:00

OpenClaw Android App

OpenClaw Android is the officially released Google Play app. It connects to an OpenClaw Gateway as a companion node for chat, voice, approvals, screen, and device-aware automation.

Current App Surface

  • New 4-step onboarding flow
  • Connect tab with Setup Code + Manual modes
  • Encrypted persistence for gateway setup/auth state
  • Chat UI restyled
  • Settings UI restyled and de-duplicated (gateway controls moved to Connect)
  • QR code scanning in onboarding
  • Performance improvements
  • Streaming support in chat UI
  • Request camera/location and other permissions in onboarding/settings flow
  • Push notifications for gateway/chat status updates
  • Security hardening (biometric lock, token handling, safer defaults)
  • Authenticated background presence beacons
  • Voice tab full functionality
  • Screen tab full functionality

Open in Android Studio

  • Open the folder apps/android.

Build / Run

cd apps/android
./gradlew :app:assemblePlayDebug
./gradlew :app:installPlayDebug
./gradlew :app:testPlayDebugUnitTest
cd ../..
pnpm android:release:archive

Third-party debug flavor:

cd apps/android
./gradlew :app:assembleThirdPartyDebug
./gradlew :app:installThirdPartyDebug
./gradlew :app:testThirdPartyDebugUnitTest

Android release archives use the pinned version in apps/android/version.json. Update it with:

pnpm android:version
pnpm android:version:check
pnpm android:version:pin -- --from-gateway
pnpm android:version:pin -- --version 2026.6.5 --version-code 2026060501

Release-owner signing sync:

pnpm android:release:signing:plan
MATCH_PASSWORD=<signing repo password> pnpm android:release:signing:sync:pull
MATCH_PASSWORD=<signing repo password> pnpm android:release:signing:check

The signing sync pulls encrypted Android upload-key assets from the shared apps-signing repo and materializes decrypted files under apps/android/build/release-signing/.

Generate raw Google Play screenshots:

pnpm android:screenshots

To make screenshot capture own emulator startup, pass a named AVD:

ANDROID_SCREENSHOT_AVD=OpenClaw_QA_API35 pnpm android:screenshots

The screenshot script uses one connected ADB device when available. If none is connected and ANDROID_SCREENSHOT_AVD is set, it boots that emulator headlessly, waits for Android to finish booting, disables animations, captures the screenshots, then shuts down the emulator it started.

pnpm android:release:archive builds signed release artifacts into apps/android/build/release-artifacts/ and writes .sha256 checksum files:

  • Play build: openclaw-<version>-play-release.aab
  • Third-party build: openclaw-<version>-third-party-release.apk

pnpm android:bundle:release is an alias for the same Fastlane archive lane.

pnpm android:release:archive is for local archive validation only. It is not a fallback upload path after pnpm android:release:upload fails.

Agent-driven Google Play uploads must use pnpm android:release:upload as the only release path. If that command fails, stop and fix the failing screenshot, metadata, signing, validation, archive, or upload step before trying again. Do not upload archived artifacts through direct Fastlane lanes, Gradle artifacts, Google Play API commands, or Play Console mutation commands.

See apps/android/VERSIONING.md and apps/android/fastlane/SETUP.md for the release workflow.

Flavor-specific direct Gradle tasks:

cd apps/android
./gradlew :app:bundlePlayRelease
./gradlew :app:bundleThirdPartyRelease

Kotlin Lint + Format

pnpm android:lint
pnpm android:format

Android framework/resource lint (separate pass):

pnpm android:lint:android

Direct Gradle tasks:

cd apps/android
./gradlew :app:ktlintCheck :benchmark:ktlintCheck
./gradlew :app:ktlintFormat :benchmark:ktlintFormat
./gradlew :app:lintPlayDebug :app:lintThirdPartyDebug

gradlew auto-detects the Android SDK at ~/Library/Android/sdk (macOS default) if ANDROID_SDK_ROOT / ANDROID_HOME are unset.

Macrobenchmark (Startup + Frame Timing)

cd apps/android
./gradlew :benchmark:connectedDebugAndroidTest

Reports are written under:

  • apps/android/benchmark/build/reports/androidTests/connected/

Perf CLI (low-noise)

Deterministic startup measurement + hotspot extraction with compact CLI output:

cd apps/android
./scripts/perf-startup-benchmark.sh
./scripts/perf-startup-hotspots.sh

Benchmark script behavior:

  • Runs only StartupMacrobenchmark#coldStartup (10 iterations).
  • Prints median/min/max/COV in one line.
  • Writes timestamped snapshot JSON to apps/android/benchmark/results/.
  • Auto-compares with previous local snapshot (or pass explicit baseline: --baseline <old-benchmarkData.json>).

Hotspot script behavior:

  • Ensures debug app installed, captures startup simpleperf data for .MainActivity.
  • Prints top DSOs, top symbols, and key app-path clues (Compose/MainActivity/WebView).
  • Writes raw perf.data path for deeper follow-up if needed.

Run on a Real Android Phone (USB)

  1. On phone, enable Developer options + USB debugging.
  2. Connect by USB and accept the debugging trust prompt on phone.
  3. Verify ADB can see the device:
adb devices -l
  1. Install + launch debug build:
pnpm android:install
pnpm android:run

If adb devices -l shows unauthorized, re-plug and accept the trust prompt again.

USB-only gateway testing (no LAN dependency)

Use adb reverse so Android localhost:18789 tunnels to your laptop localhost:18789.

Terminal A (gateway):

pnpm openclaw gateway --port 18789 --verbose

Terminal B (USB tunnel):

adb reverse tcp:18789 tcp:18789

Then in app Connect → Manual:

  • Host: 127.0.0.1
  • Port: 18789
  • TLS: off

Hot Reload / Fast Iteration

This app is native Kotlin + Jetpack Compose.

  • For Compose UI edits: use Android Studio Live Edit on a debug build (works on physical devices; project minSdk=31 already meets API requirement).
  • For many non-structural code/resource changes: use Android Studio Apply Changes.
  • For structural/native/manifest/Gradle changes: do full reinstall (pnpm android:run).
  • Canvas web content already supports live reload when loaded from Gateway __openclaw__/canvas/ (see docs/platforms/android.md).

Connect / Pair

  1. Start the gateway (on your main machine):
pnpm openclaw gateway --port 18789 --verbose
  1. In the Android app:
  • Open the Connect tab.
  • Use Setup Code or Manual mode to connect.
  1. Approve pairing (on the gateway machine):
openclaw devices list
openclaw devices approve <requestId>

More details: docs/platforms/android.md.

Permissions

  • Discovery:
    • Android 13+ (API 33+): NEARBY_WIFI_DEVICES
    • Android 12 and below: ACCESS_FINE_LOCATION (required for NSD scanning)
  • Foreground service notification (Android 13+): POST_NOTIFICATIONS
  • Camera:
    • CAMERA for camera.snap and camera.clip
    • RECORD_AUDIO for camera.clip when includeAudio=true

Google Play Restricted Permissions

As of March 19, 2026, these manifest permissions are the main Google Play policy risk for this app:

  • READ_SMS
  • SEND_SMS
  • READ_CALL_LOG

Why these matter:

  • 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.
  • Review usually involves a Permissions Declaration Form, policy justification, and demo video evidence in Play Console.
  • The Play build removes these behind the play flavor.
  • Photo library access is also removed from the Play build. Use third-party builds for photos.latest.

Current OpenClaw Android implication:

  • APK / sideload build can keep SMS, Call Log, and recent-photo features.
  • Google Play build excludes SMS send/search, Call Log search, and recent-photo access unless the product is intentionally positioned and approved under the relevant policy exception.
  • The repo now ships this split as Android product flavors:
    • play: removes READ_SMS, SEND_SMS, READ_CALL_LOG, READ_MEDIA_IMAGES, READ_MEDIA_VISUAL_USER_SELECTED, and READ_EXTERNAL_STORAGE; hides SMS, Call Log, and Photos surfaces in onboarding, settings, and advertised node capabilities.
    • Installed-app listing is user controlled. device.apps is advertised only after the user enables Settings > Phone Capabilities > Installed Apps. The command defaults to launcher-visible apps and does not require QUERY_ALL_PACKAGES.
    • thirdParty: keeps the full permission set and the existing SMS / Call Log / Photos functionality.

Policy links:

Other Play-restricted surfaces to watch if added later:

  • ACCESS_BACKGROUND_LOCATION
  • MANAGE_EXTERNAL_STORAGE
  • QUERY_ALL_PACKAGES
  • REQUEST_INSTALL_PACKAGES
  • AccessibilityService

Reference links:

Integration Capability Test (Preconditioned)

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

Pre-req checklist:

  1. Gateway is running and reachable from the Android app.
  2. Android app is connected to that gateway and openclaw nodes status shows it as paired + connected.
  3. App stays unlocked and in foreground for the whole run.
  4. Open the app Screen tab and keep it active during the run (canvas/A2UI commands require the canvas WebView attached there).
  5. Grant runtime permissions for capabilities you expect to pass (camera/mic/location/notification listener/location, etc.).
  6. No interactive system dialogs should be pending before test start.
  7. Canvas host is enabled and reachable from the device for remote Canvas checks (do not run gateway with OPENCLAW_SKIP_CANVAS_HOST=1; startup logs should include canvas host mounted at .../__openclaw__/).
  8. Local operator test client pairing is approved. If first run fails with pairing required, preview the latest pending request, approve the printed request ID, then rerun:
  9. For A2UI checks, keep the app on Screen tab; the node uses its bundled app-owned A2UI page for message application.
openclaw devices list
openclaw devices approve --latest   # preview only; copy the requestId from output
openclaw devices approve <requestId>

Run:

pnpm android:test:integration

Optional overrides:

  • OPENCLAW_ANDROID_GATEWAY_URL=ws://... (default: from your local OpenClaw config)
  • OPENCLAW_ANDROID_GATEWAY_TOKEN=...
  • OPENCLAW_ANDROID_GATEWAY_PASSWORD=...
  • OPENCLAW_ANDROID_NODE_ID=... or OPENCLAW_ANDROID_NODE_NAME=...

What it does:

  • Reads node.describe command list from the selected Android node.
  • Invokes advertised non-interactive commands.
  • Skips screen.record in this suite (Android requires interactive per-invocation screen-capture consent).
  • Asserts command contracts (success or expected deterministic error for safe-invalid calls like sms.send and notifications.actions).

Common failure quick-fixes:

  • pairing required before tests start:
    • list pending requests (openclaw devices list), then approve with the exact ID (openclaw devices approve <requestId>) and rerun.
  • A2UI host not reachable / A2UI_HOST_UNAVAILABLE:
    • keep the app foregrounded on the Screen tab and rerun. A2UI commands use the bundled app-owned A2UI page; the Gateway Canvas host is still needed for remote Canvas checks, but not for A2UI message application.
  • NODE_BACKGROUND_UNAVAILABLE: canvas unavailable:
    • app is not effectively ready for canvas commands; keep app foregrounded and Screen tab active.

Contributions

Maintainer: @obviyus. For issues/questions/contributions, please open an issue or reach out on Discord.