Skip to content

Add more androidDevice actions to the mobile-2 sample - #2964

Draft
jebrans wants to merge 1 commit into
microsoft:mainfrom
jebrans:dev/jebransyed/android-tier2
Draft

Add more androidDevice actions to the mobile-2 sample#2964
jebrans wants to merge 1 commit into
microsoft:mainfrom
jebrans:dev/jebransyed/android-tier2

Conversation

@jebrans

@jebrans jebrans commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

What this changes

  • Adds composeEmail, shareText, openSettings, createCalendarEvent and
    playMusicFromSearch to androidDeviceSchema.ts and wires each one through
    AndroidDeviceAgent, WebSocketManager, ChatViewModel and MainActivity.
  • Uses ACTION_SENDTO with a bare mailto: URI for composeEmail, so only
    mail apps resolve rather than every share target, and carries recipients as
    EXTRA_EMAIL/EXTRA_CC/EXTRA_BCC arrays instead of splicing them into the
    URI. A single unusable address fails the whole action, because a draft
    addressed to fewer people than the user asked for would look like success.
  • Resolves the inner ACTION_SEND intent before wrapping it in
    Intent.createChooser for shareText. The chooser is a system activity that
    always resolves, so an unchecked wrap would report success on a device with
    no text handler.
  • Restricts openSettings to a closed AndroidSettingsScreen enum mapped
    exhaustively to Settings.ACTION_* constants. The model never supplies a raw
    intent action string, appInfo is pinned to this app's own package, and the
    screens only display settings rather than toggling them.
  • Adds createCalendarEvent as ACTION_INSERT on CalendarContract.Events,
    which opens the calendar app's pre-filled new-event editor and so needs no
    WRITE_CALENDAR permission. ISO-8601 times are resolved without java.time,
    since the module targets minSdk 24 with no core-library desugaring.
  • Builds calendar offset zones from a raw integer offset via SimpleTimeZone
    rather than by formatting a GMT+hh:mm string. String.format is
    locale-sensitive, and under a locale with non-Latin digits
    TimeZone.getTimeZone cannot read the result and silently returns GMT,
    turning +05:30 into a five-and-a-half-hour shift with no error.
  • Anchors all-day events at UTC midnight as CalendarContract requires, so a
    user in UTC+10 does not see them land a day early.
  • Fails rather than guessing on an unknown music focus, an unknown settings
    screen, an all-day event that also carries a time of day, and an event
    spanning more than 366 days.
  • Adds a <queries> entry for every new implicit intent. Without one, Android
    11+ package visibility makes resolveActivity return null and the action
    falsely reports that no app is available.
  • Extends AndroidDeviceSchemaAssetTest to check that the settings-screen and
    music-focus unions in the schema match the Kotlin enums, so the two cannot
    drift apart silently.

Every action reaches the OS through the existing launchExternalIntent funnel,
keeping the current resolveActivity check, foreground-lifecycle check and
exception handling, and completing its callback exactly once. The ten existing
Tier-1 actions are unchanged.

Verification

Unit tests and the debug build completed successfully:

  • gradlew testDebugUnitTest — 234 tests, 0 failures (previously 179).
  • gradlew assembleDebug — successful.
  • gradlew lintDebug — clean; the only findings are pre-existing
    informational items unrelated to this change.

Review confirmed:

  • A GPT-5.3-Codex review reported no Critical or High findings. Its Medium
    finding (locale-sensitive offset formatting) and Low finding (fractional
    seconds rejected) are both fixed here, each with a regression test. The
    locale defect was reproduced on the JVM before and after the fix: +05:30
    resolved to offset 0 instead of 19800000.
  • Hours-only ISO offsets such as +05 are now accepted, with a test.
  • No new permissions are declared in AndroidManifest.xml.

Extends the `androidDevice` client agent with the five medium-risk
actions from the Android feature plan. Every one stays a compose/open
intent that the user confirms, so no new Android permissions are needed
and a hallucinated action cannot complete on its own.

## Actions

- `composeEmail` - `ACTION_SENDTO` with a bare `mailto:` URI. `SENDTO`
  rather than `SEND` so only mail apps resolve, not every share target.
  Recipients ride as `EXTRA_EMAIL`/`EXTRA_CC`/`EXTRA_BCC` arrays rather
  than being spliced into the URI. One unusable address fails the whole
  action: a draft addressed to fewer people than asked for would look
  like success.
- `shareText` - `ACTION_SEND` (`text/plain`) inside `createChooser`, so
  the user picks the destination. The inner intent is resolved before
  wrapping, because the chooser is a system activity that always
  resolves and would otherwise report a false success on a device with
  no text handler.
- `openSettings` - the screen comes from a closed `AndroidSettingsScreen`
  enum mapped exhaustively to `Settings.ACTION_*`; the model never
  supplies a raw action string, and `appInfo` is pinned to this app's own
  package. Screens only display settings; nothing is toggled.
- `createCalendarEvent` - `ACTION_INSERT` on `CalendarContract.Events`,
  which opens the calendar app's pre-filled new-event editor, so no
  `WRITE_CALENDAR` permission is involved. ISO-8601 times are resolved
  without `java.time` (minSdk 24, no core-library desugaring).
- `playMusicFromSearch` - `MEDIA_PLAY_FROM_SEARCH`. What actually plays
  is up to the installed app, so the action reports what it dispatched.

## Invariants

- Every action reaches the OS through `launchExternalIntent`, keeping the
  existing resolveActivity / foreground-lifecycle / exception funnel, and
  completes its callback exactly once.
- Each new implicit intent has a matching `<queries>` entry; without one
  Android 11+ package visibility makes `resolveActivity` return null and
  the action falsely reports that no app is available.
- Parsers read strings via `opt(name) as? String`, never `optString`,
  because Android's `org.json` renders a JSON null as the string "null".
- Ambiguity fails the action rather than being guessed at: an unknown
  music `focus`, an unknown settings screen, an all-day event that also
  carries a time of day, and a span over 366 days are all rejected.
- Calendar offsets are built from a raw integer offset via
  `SimpleTimeZone`, not by formatting a `GMT+hh:mm` string. `String.format`
  is locale-sensitive, and under a locale with non-Latin digits
  `TimeZone.getTimeZone` cannot read the result and silently falls back to
  GMT - turning `+05:30` into a five-and-a-half-hour shift with no error.
- All-day events are anchored at UTC midnight as `CalendarContract`
  requires, so a user in UTC+10 does not see them land a day early.
- `AndroidDeviceSchemaAssetTest` now checks that the settings-screen and
  music-focus unions in `androidDeviceSchema.ts` match the Kotlin enums,
  so the two cannot drift apart silently.

## Validation

- `gradlew testDebugUnitTest` - 234 tests, 0 failures (was 179).
- `gradlew assembleDebug lintDebug` - clean; lint reports only
  pre-existing informational items.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant