Support desktop windowing

Desktop windowing enables users to run multiple apps simultaneously in resizable app windows for a versatile, desktop-like experience.

In figure 1, you can see the organization of the screen with desktop windowing enabled. Things to note:

  • Users can run multiple apps side by side simultaneously.
  • Taskbar is in a fixed position at the bottom of the display showing the running apps. Users can pin apps for quick access.
  • New customizable header bar decorates the top of each window with controls such as minimize and maximize.
A tablet display showing multiple apps running in resizable windows with a taskbar at the bottom.
Figure 1. Desktop windowing on a tablet.

By default, apps open full screen on Android tablets. To launch an app in desktop windowing, press and hold the window handle at the top of the screen and drag the handle within the UI, as seen in figure 2.

When an app is open in desktop windowing, other apps open in desktop windows as well.

Figure 2. Press, hold, and drag the app window handle to enter desktop windowing.

Users can also invoke desktop windowing from the menu that shows up below the window handle when you tap or click the handle or use the keyboard shortcut Meta key (Windows, Command, or Search) + Ctrl + Down.

Users exit desktop windowing by closing all active windows or by grabbing the window handle at the top of a desktop window and dragging the app to the top of the screen. The Meta + H keyboard shortcut also exits desktop windowing and runs apps full screen again.

To return to desktop windowing, tap or click the desktop space tile in the Recents screen.

Resizability and compatibility mode

In desktop windowing, apps with locked orientation are freely resizable. That means even if an activity is locked to portrait orientation, users can still resize the app to a landscape orientation window.

Figure 3. Resizing the window of a portrait-restricted app to landscape.

Apps declared as nonresizable (that is, resizeableActivity = false) have their UI scaled while keeping the same aspect ratio.

Figure 4. The UI of a nonresizable app scales as the window resizes.

Camera apps that lock the orientation or are declared as nonresizable have a special treatment for their camera viewfinders: the window is fully resizable, but the viewfinder keeps the same aspect ratio. By assuming apps always run in portrait or landscape, the apps hardcode or otherwise make assumptions that lead to miscalculations of the preview or captured image orientation or aspect ratio, resulting in stretched, sideways, or upside-down images.

Until apps are ready to implement fully responsive camera viewfinders, the special treatment provides a more basic user experience that mitigates the effects wrong assumptions may cause.

To learn more about compatibility mode for camera apps, see Device compatibility mode.

Figure 5. Camera viewfinder retains its aspect ratio as the window resizes.

Customizable header insets

All apps running in desktop windowing have a header bar, even in immersive mode. Verify your app's content isn't obscured by the header bar. The header bar is a caption bar inset type: WindowInsets.Companion.captionBar(); in views, WindowInsets.Type.captionBar(), which is part of the system bars.

You can learn more about handling insets in Display content edge-to-edge in your app and handle window insets in Compose.

The header bar is also customizable. Android 15 introduced the appearance type APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND to make the header bar transparent to allow apps to draw custom content inside the header bar.

Apps then become responsible for styling the top portion of their content to look like the caption bar (background, custom content, and so forth) with the exception of the system caption elements (close and maximize buttons), which are drawn by the system on the transparent caption bar on top of the app.

Apps can toggle the appearance of the system elements inside the caption for light and dark themes using APPEARANCE_LIGHT_CAPTION_BARS, similar to how the status bar and navbar are toggled.

Android 15 also introduced the WindowInsets#getBoundingRects() method which enables apps to introspect caption bar insets in more detail. Apps can differentiate between areas where the system draws system elements and unutilized areas where apps can place custom content without overlapping system elements.

The list of Rect objects returned by the API indicate system regions that should be avoided. Any remaining space (calculated by subtracting the rectangles from the caption bar Insets) is where the app can draw without overlapping system elements and with the ability to receive input.

Chrome before and after implementing custom headers.
Figure 6. Chrome before and after implementing custom headers.

To set system gesture exclusion rects for a custom header, implement the following in your view or Composable:

// In a custom View's onLayout or a similar lifecycle method
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
    super.onLayout(changed, left, top, right, bottom)
    if (changed) {
        // Calculate the height of your custom header
        val customHeaderHeight = 100 // Replace with your actual header height in pixels

        // Create a Rect covering your custom header area
        val exclusionRect = Rect(0, 0, width, customHeaderHeight)

        // Set the exclusion rects for the system
        systemGestureExclusionRects = listOf(exclusionRect)
    }
}

Multitasking and multi-instance support

Multitasking is at the core of desktop windowing, and allowing multiple instances of your app can highly increase users productivity.

Android 15 introduces PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI, which apps can set to specify that system UI should be shown for the app to allow it to be launched as multiple instances.

You can declare PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI in your app's AndroidManifest.xml within the <activity> tag:

<activity
    android:name=".MyActivity"
    android:exported="true"
    android:resizeableActivity="true">
    <meta-data
        android:name="android.window.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI"
        android:value="true" />
</activity>

Manage app instances with dragging gestures

In multi-window mode, users can start a new app instance by dragging a view element out of the app's window. Users can also move elements between instances of the same app.

Figure 7. Start a new instance of Chrome by dragging a tab out of the desktop window.

Android 15 introduces two flags to customize drag behavior:

  • DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG: Indicates that an unhandled drag should be delegated to the system to be started if no visible window handles the drop. When using this flag, the caller must provide ClipData with an ClipData.Item that contains an immutable IntentSender to an activity to be launched (see ClipData.Item.Builder#setIntentSender()). The system can launch the intent or not based on factors like the current screen size or windowing mode. If the system does not launch the intent, the intent is canceled by means of the normal drag flow.

  • DRAG_FLAG_GLOBAL_SAME_APPLICATION: Indicates that a drag operation can cross window boundaries (for multiple instances of the same application).

    When [startDragAndDrop()][20] is called with this flag set, only visible windows belonging to the same application are able to participate in the drag operation and receive the dragged content.

The following example demonstrates how to use these flags with startDragAndDrop():

// Assuming 'view' is the View that initiates the drag
view.setOnLongClickListener {
    // Create an IntentSender for the activity you want to launch
    val launchIntent = Intent(view.context, NewInstanceActivity::class.java)
    val pendingIntent = PendingIntent.getActivity(
        view.context,
        0,
        launchIntent,
        PendingIntent.FLAG_IMMUTABLE // Ensure the PendingIntent is immutable
    )

    // Build the ClipData.Item with the IntentSender
    val item = ClipData.Item.Builder()
        .setIntentSender(pendingIntent.intentSender)
        .build()

    // Create ClipData with a simple description and the item
    val dragData = ClipData(
        ClipDescription("New Instance Drag", arrayOf(ClipDescription.MIMETYPE_TEXT_PLAIN)),
        item
    )

    // Combine the drag flags
    val dragFlags = View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG or
                    View.DRAG_FLAG_GLOBAL_SAME_APPLICATION

    // Start the drag operation
    view.startDragAndDrop(
        dragData,                     // The ClipData to drag
        View.DragShadowBuilder(view), // A visual representation of the dragged item
        null,                         // Local state object (not used here)
        dragFlags                     // The drag flags
    )
    true // Indicate that the long click was consumed
}
Figure 8. Move a tab between two instances of the Chrome app.

Additional optimizations

Customize app launches and transition apps from desktop windowing to full screen.

Specify default size and position

Not all apps, even if resizable, need a large window to offer user value. You can use the ActivityOptions#setLaunchBounds() method to specify a default size and position when an activity is launched.

Here's an example of how to set launch bounds for an activity:

val options = ActivityOptions.makeBasic()

// Define the desired launch bounds (left, top, right, bottom in pixels)
val launchBounds = Rect(100, 100, 700, 600) // Example: 600x500 window at (100,100)

// Apply the launch bounds to the ActivityOptions
options.setLaunchBounds(launchBounds)

// Start the activity with the specified options
val intent = Intent(this, MyActivity::class.java)
startActivity(intent, options.toBundle())

Enter full-screen from the desktop space

Apps can go full-screen by calling Activity#requestFullScreenMode(). The method displays the app full screen directly from desktop windowing.

To request full-screen mode from an activity, use the following code:

// In an Activity
fun enterFullScreen() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // Android 15 (U)
        requestFullScreenMode()
    }
}