Android windowing: split screen, picture-in-picture, freeform¶
Spike notes for #14115 and the windowMode tool.
Warning
Not yet verified on an emulator. Everything below about device behavior was
written from the AOSP sources (ActivityManagerShellCommand, ActivityStarter,
TaskDisplayArea, Task, WindowConfiguration and the WM Shell’s
ShellCommandHandlers), with no emulator available. Treat each command as a
candidate. The verification checklist at the end lists what to run on API 30, 34
and 36 images before these notes count as evidence.
How the tool decides that a command worked¶
The commands differ by API level and image, and several have changed shape between
releases. So windowMode does not trust any single command. It runs candidates in
order and re-reads dumpsys activity activities after each one for up to 3 seconds
(polling every 250 ms). It stops at the first candidate whose read-back shows the
requested mode, and bounds or side when those were asked for. Every attempt is
returned (attempts), along with the command that reached the mode (method). A
command that a release lacks therefore costs one attempt and shows up as failed or
not_reached. It is never reported as success.
Reading the mode and bounds¶
The task’s mode and bounds come from dumpsys activity activities, the output the
back stack parser already reads, so observe reports them in backStack.tasks[]
(windowingMode, bounds) at no extra adb cost. The real captures in
test/features/observe/activityActivitiesDumps/ (API 24-36) show these sources:
| Source | API | Notes |
|---|---|---|
* Task{... mode=<mode> ...} task header |
30+ | WindowConfiguration.windowingModeToString |
Stack #N: type=... mode=<mode> root-task header |
28-30 | RootTask #N: on API 31. Tasks inside inherit its mode on API 28-29. |
bounds=[l,t][r,b] under each task |
30+ | “Application tokens in top down Z order” section: resolved getBounds() |
mBounds=Rect(l, t - r, b) after a task header |
24+ | Requested override bounds. Rect(0, 0 - 0, 0) means “inherit” and is ignored. |
CurrentConfiguration=... winConfig={ mBounds=Rect(...) |
28+ | Top activity’s resolved bounds; the last fallback |
Mode strings: fullscreen, pinned (PiP), freeform, multi-window (split screen
on API 32+, and the split stage roots), split-screen-primary and
split-screen-secondary (legacy split screen on API 31 and below), undefined. API
24-27 print no mode, so none is reported.
Commands per API level¶
<launch> below is the launcher-icon start the tool uses to bring an app forward
without stacking a new activity on its task:
am start -a android.intent.action.MAIN -c android.intent.category.LAUNCHER \
-f 0x10200000 -n <pkg>/<LauncherActivity> --windowingMode <N>
0x10200000 is FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_RESET_TASK_IF_NEEDED, the
launcher’s own flags. --windowingMode takes WindowConfiguration.WINDOWING_MODE_*:
1 fullscreen, 2 pinned, 3 split-screen primary, 4 split-screen secondary (both removed
in API 33), 5 freeform, 6 multi-window. On API 31+ TaskDisplayArea.getOrCreateRootTask
applies the launch windowing mode to a reused root task. On API 30 the reuse path goes
through getLaunchStack, so whether an already-running task changes mode there is the
first thing to verify.
WM Shell commands go through
dumpsys activity service SystemUIService WMShell <command>. Android 14+ registers
them under a module prefix (splitscreen moveToSideStage ...), while 12L and 13
accept them bare. The tool tries the prefixed form first, then the bare form.
Freeform¶
| Step | API 30 | API 34 | API 36 |
|---|---|---|---|
| Enable | settings put global enable_freeform_support 1, settings put global force_resizable_activities 1 (WMS observes both; no reboot) |
same | same; desktop windowing may also need the developer option (override_desktop_mode_features) and a reboot, which the tool does not do |
| Enter | <launch> with --windowingMode 5 |
same | same, then WMShell desktopmode moveToDesktop <taskId> |
| Resize | am task resize <taskId> <l> <t> <r> <b> |
same | same |
| Leave | <launch> with --windowingMode 1 |
same | same |
am task resize only works on a task that is already freeform or multi-window
(WindowConfiguration.canResizeTask). Its help text shows one comma-separated
argument, but runTaskResize reads four separate integers; the tool passes four. The
system may clamp bounds to the display or to the app’s minimum size. The tool reports
that as a mismatch rather than a success. Phone images whose
config_freeformWindowManagement is false may refuse freeform even with both settings
on; the tool then returns success: false with the settings receipt.
The two settings are written only when they are not already 1. Their prior values
(null when unset) are returned as restore.settings, bound to the device serial and
/proc/sys/kernel/random/boot_id like the setDeviceResources receipts. Restore writes
them back with settings put or settings delete and reads them back.
Picture-in-picture¶
There is no shell command that puts an arbitrary app into PiP. The app has to declare
android:supportsPictureInPicture="true" and enter PiP itself when the user leaves it:
setAutoEnterEnabled(true) on API 31+, or enterPictureInPictureMode() from
onUserLeaveHint(). So {pip: "enter"} brings the app forward with <launch> (mode 1)
when it is not the resumed task, then sends input keyevent KEYCODE_HOME and waits for
mode=pinned. {pip: "exit"} relaunches with <launch> and --windowingMode 1. The
same command is expected on 30, 34 and 36. API 30 also has
am stack move-top-activity-to-pinned-stack, which the tool does not use because it was
removed later.
Split screen¶
| API | Enter | Leave |
|---|---|---|
| 30 (and 31) | <launch primary> with --windowingMode 3, then <launch secondary> with --windowingMode 4. Without with, the system shows recents on the other side. |
<launch> with --windowingMode 1 |
| 32+ (34, 36) | Start with if it has no task, bring the app to the top with <launch> (mode 1), then WMShell [splitscreen] moveToSideStage <withTaskId> <position> (0 top/left, 1 bottom/right). The app on top becomes the main stage. |
WMShell [splitscreen] exitSplitScreen <appTaskId>, then <launch> mode 1 |
The side is checked from the read-back bounds: primary is the task whose top (or, at
equal tops, left) edge is smaller. When the app lands on the wrong side, the tool runs
WMShell [splitscreen] switchSplitPosition once. There is no shell command for the
split ratio, so the tool does not accept one. #14095 tracks a split_screen system
action, which is the accessibility route (GLOBAL_ACTION_TOGGLE_SPLIT_SCREEN).
Coordinates in freeform and split screen¶
No mapping is needed for element taps. CtrlProxy builds the hierarchy from
AccessibilityNodeInfo.getBoundsInScreen (see ViewHierarchyExtractor.kt). tapOn
and the other element tools tap the centre of those bounds through input tap or an
accessibility gesture, both of which take absolute display coordinates. So a tap on an
element inside a freeform window at [120,300][960,1500] lands on that element without
any offset. tapAt and swipeAt also take absolute coordinates, matching observe’s
screenSize. A window-relative coordinate option (offsetting by backStack.tasks[].bounds)
is a possible follow-up if callers want it; it is not needed for correctness.
Two cases to check on a device: an app in size-compat mode inside a freeform window, where the accessibility bounds are scaled by the framework, and a freeform window that extends past the display edge.
Restore receipts¶
Every mutation returns what it changed:
previous: the app task’s mode and bounds before the call.restore.mode: themodeargument that puts the window back ("fullscreen",{freeform: {bounds}},{pip: "enter"}, or{splitScreen: {with, position}}with the previous partner). It is present only when the window changed.restore.settings: the freeform settings receipt described above.
windowMode({appId, restore}) applies the mode first, while freeform support is still
on, then writes the settings back.
Verification checklist¶
None of these has been run. Use the Playground (dev.jasonpearson.automobile.playground)
and Settings (com.android.settings) as the second app. Capture
adb shell dumpsys activity activities in each state, replace the hand-reconstructed
files in test/fixtures/android-windowing/ with the captures, and record the image and
build in that directory’s README.
Images: API 30 phone (google_apis), API 34 phone and tablet (pixel_tablet), API 36
phone, tablet, and a desktop-capable image with desktop windowing enabled.
- [ ] Freeform settings: on each image, does
--windowingMode 5reachmode=freeformafter the two settings, with no reboot? Which images also needoverride_desktop_mode_featuresor a reboot? - [ ] API 30: does
<launch>with--windowingMode 5move an already-running task, or only a new one? - [ ] API 36 desktop: does
WMShell desktopmode moveToDesktop <taskId>exist, and does--windowingMode 5alone work there? - [ ]
am task resize <id> l t r b: four arguments accepted on 30, 34 and 36? Are the bounds applied exactly or clamped? - [ ] Freeform
bounds=[...]in the Z-order section matches the window on screen. - [ ] A
tapOnon a Playground element inside a freeform window lands on the element, including when the window is not at the display origin. - [ ] PiP: with a PiP-capable activity, HOME produces
mode=pinnedon 30, 34 and 36, and the<launch>mode-1 relaunch expands it. The Playground does not support PiP yet (see follow-ups). - [ ] API 30 and 31:
--windowingMode 3then4producesplit-screen-primaryandsplit-screen-secondaryapp tasks. - [ ] API 34 and 36: which form of
moveToSideStageworks (prefixed or bare)? Does the position argument apply when it is the last argument, or only when another argument follows it? - [ ] API 34 and 36:
exitSplitScreen <taskId>leaves that task fullscreen on top. - [ ] Restore: a
restore.settingsreceipt puts both settings back, and is refused after a reboot.
Follow-ups¶
- Playground PiP: an activity or screen that opts into PiP with
setAutoEnterEnabled, without changing what HOME does on the Playground’s other screens. - Split ratio: needs a WM Shell divider command or a drag on the divider.
- Verified captures for each mode (see the checklist above).