Android Desktop Windowing and Freeform Windows: From Display Partitioning to Multi-Instance Multitasking

Last year, while adapting an app for tablets, the product manager asked: can users open two documents side by side and edit them, like on a PC? My first reaction was that Android split-screen only does left-right halves, and Activity recreation alone was painful enough.

Android 15 QPR builds surfaced desktop windowing as a developer-preview feature on selected large-screen devices, and newer Android releases have made this adaptation path increasingly relevant. This article walks the system-level management chain for freeform windows, the architecture of multi-instance parallelism, and the pitfalls I hit during adaptation.

Desktop Mode Is Really a Separate Display

Desktop mode is not “projecting the phone UI onto a big screen.” At the system level, it creates an independent virtual Display and a Launcher Task to host the desktop environment.

The core entry point is DesktopModeController:

// frameworks/base/services/core/java/com/android/server/wm/DesktopModeController.java
boolean isDesktopModeEnabled() {
    return mContext.getResources().getBoolean(
        com.android.internal.R.bool.config_enableDesktopMode)
        || Settings.Global.getInt(mContext.getContentResolver(),
            "force_desktop_mode_on_external_display", 0) == 1;
}

config_enableDesktopMode is an AOSP compile-time switch, off by default. OEMs or custom ROMs enable it via overlay. force_desktop_mode_on_external_display is a runtime switch for debugging.

When an external display is attached or desktop mode is triggered, the system creates a Display of type TYPE_VIRTUAL or TYPE_EXTERNAL via DisplayManager. This Display has its own DisplayContent, fully isolated from the main-screen window stack.

The desktop-mode Display and the main-screen Display share the same WindowManager service, but WindowManager internally partitions by DisplayContent — understand this, and the multi-window management logic clicks into place.

The Window Management Chain for Freeform Windows

In freeform mode, each Activity corresponds to a Task. The Task bounds are no longer screen-sized — they are draggable, resizable, stackable rectangles.

Task Bounds Control

Window position and size are controlled by Task#setBounds(), which ultimately lands on WindowContainer’s mLastSurfacePosition and mSurfaceSize:

// Task.java (simplified)
void setBounds(Rect bounds) {
    mBounds.set(bounds);
    // Trigger WindowContainer surface re-layout
    onConfigurationChanged(newConfig);
    // Notify SurfaceControl to update position
    getSyncTransaction().setPosition(getSurfaceControl(),
        bounds.left, bounds.top);
}

Every bounds change dispatches a Configuration to the Activity. Unlike split-screen, where the system computes bounds for you, freeform bounds are produced by user dragging in real time — much higher frequency.

Z-Order and Focus Management

Z-Order (stacking order) is easy to overlook in freeform windows. Whichever window the user clicks rises to the top. WindowManagerService handles this via DisplayContent#moveTaskToFront():

// DisplayContent.java
void moveTaskToFront(Task task) {
    // Move the Task to the front of mChildren
    positionChildAt(POSITION_TOP, task, true /* includingParents */);
    // Update input focus
    mFocusedApp = task.topRunningActivity();
    // InputDispatcher dispatches touch events accordingly
    mService.mInputManager.setFocusedApplication(...);
}

mChildren is the child list of WindowContainer — essentially an ordered window tree. Z-Order is the traversal order of this tree: later nodes draw on top.

While debugging I noticed a detail: focus switching during window scaling has a 50ms debounce. If a user quickly drags to resize and then switches to another window, InputDispatcher sends an ACTION_CANCEL to the previous window to avoid mis-touches. This logic hides inside InputManagerService’s transferTouchGesture — hard to guess without reading the source.

Multi-Instance Parallelism: The Role of TaskFragment

Multi-instance parallelism means the same Activity can launch multiple times, each instance running in its own Task. This relies on the TaskFragment mechanism introduced in Android 12.

TaskFragment is a sub-container inside a Task. A Task can have multiple TaskFragments, each hosting an Activity stack. In desktop mode, standard launch mode creates a new Task for each instance:

// Multi-instance launch in desktop mode
val intent = Intent(this, DocumentActivity::class.java).apply {
    // FLAG_ACTIVITY_MULTIPLE_TASK guarantees a new Task
    addFlags(Intent.FLAG_ACTIVITY_NEW_TASK 
        or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
}
startActivity(intent)

A pitfall here: FLAG_ACTIVITY_NEW_TASK in non-desktop mode reuses an existing Task (if affinity matches). FLAG_ACTIVITY_MULTIPLE_TASK forces no reuse. In desktop mode, the combination of both flags is the safe choice.

Configuration Changes and State Saving

The hardest part of multi-instance parallelism is not launching — it is configuration changes. Window scaling triggers onConfigurationChanged, but does not recreate the Activity. Split-screen bounds changes can trigger recreation; freeform does not, because the framework handles window-size config changes by default.

If the user adjusts DPI or font size in system settings, every multi-instance Activity receives onConfigurationChanged. Each instance’s Resources object updates independently, without interfering with each other.

In my project, I bound a ViewModel to each DocumentActivity instance. The ViewModel lifecycle is tied to the Task, not the process. When the user closes a window, the corresponding ViewModel’s onCleared() fires correctly — no leaks.

Input Event Dispatch

With multiple freeform windows coexisting, how do touch events land in the right window?

When InputDispatcher receives a touch event, it queries WindowManagerService for the focused window region. Then, using the touch coordinates (MotionEvent x/y), it walks the window tree from the topmost Z-Order downward, finding the first window whose touchable region contains the point:

// InputDispatcher pseudocode
sp<InputChannel> findTouchedWindow(int32_t x, int32_t y) {
    // Walk touchable windows from top down
    for (auto& window : mWindows.reverse()) {
        if (!window->canReceiveKeys()) continue;
        if (window->touchableRegionContains(x, y)) {
            return window->getInputChannel();
        }
    }
    return nullptr;
}

This traversal is O(n) — negligible with few windows. But once a user opens a dozen, the per-touch overhead shows. Android 15 optimizes this with an R-Tree indexing window touch regions, dropping query complexity to O(log n).

Real Adaptation Issues

Issue 1: DecorView insets handling

Freeform windows have their own title bar and shadow border, drawn by the system’s DecorCaptionView. If you have custom WindowInsets handling, the content area may be covered by the title bar.

Fix: in onApplyWindowInsets, consume the system-status-bar insets and keep only the caption-bar insets:

ViewCompat.setOnApplyWindowInsetsListener(rootView) { _, insets ->
    val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
    // In freeform windows, consume system bars but keep caption-bar space
    if (isFreeformWindow()) {
        rootView.setPadding(0, systemBars.top, 0, 0)
    }
    insets
}

Issue 2: Lifecycle timing differences

In desktop mode, when a user closes a window via the external display’s taskbar, the Activity’s finish() originates from DesktopModeTaskBarService, not from direct user action. There can be a 500ms+ delay between onStop() and onDestroy() — the system plays the window-close animation before actually destroying. If your analytics reporting depends on onDestroy(), this delay skews the timing.

My fix: persist critical state in onStop(). Use onDestroy() only for resource release.

Issue 3: Surface layers and hardware compositing

With multiple windows rendering simultaneously, SurfaceFlinger manages multiple Layers. Once the window count exceeds the hardware overlay limit (typically 4-6 layers), excess windows go through GLES compositing, and power consumption and latency climb noticeably. On a low-end tablet with 8 windows open, frame rate drops visibly. This is not a bug — it is a hardware limit. Adapt by guiding users on window counts or providing a graceful-degradation strategy.


Desktop mode and freeform windows are not Android “catching up to PC features” — they are a bottom-up restructuring of the multi-Display architecture. Master three chains — DisplayContent partitioning, TaskFragment hosting, and InputDispatcher coordinate-based dispatch — and you have essentially nailed freeform window development.

For deep adaptation, start with three things: normalize WindowInsets handling, move state saving forward to onStop(), and test Surface compositing behavior under multi-window. In-window split views and drag-and-drop data exchange are application-layer polish — get the three fundamentals down first.