Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7cfa7aad authored by Shuming Hao's avatar Shuming Hao
Browse files

Add a new class to make split display aware

Add a helper class and a data class for managing and storing split
screen related tasks and stages. This will allow StageCoordinator to be
display aware in the follow up CLs.

Flag: com.android.window.flags.enable_multi_display_split

Test: manual
Bug: 393217881

Change-Id: I2ab78c4dd0525621a0db96ea9e147c5fa60578aa
parent 983e43a0
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -1324,6 +1324,9 @@ class DesktopTasksController(

        val stageCoordinatorRootTaskToken =
            splitScreenController.multiDisplayProvider.getDisplayRootForDisplayId(DEFAULT_DISPLAY)
        if (stageCoordinatorRootTaskToken == null) {
            return
        }
        wct.reparent(stageCoordinatorRootTaskToken, displayAreaInfo.token, true /* onTop */)

        val deactivationRunnable =
+113 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.wm.shell.splitscreen
import android.app.ActivityManager
import android.hardware.display.DisplayManager
import android.view.SurfaceControl
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.common.split.SplitLayout
import com.android.wm.shell.protolog.ShellProtoLogGroup

/**
 * Helper class for managing split-screen functionality across multiple displays.
 */
class SplitMultiDisplayHelper(private val displayManager: DisplayManager) {

    /**
     * A map that stores the [SplitTaskHierarchy] associated with each display ID.
     * The keys are display IDs (integers), and the values are [SplitTaskHierarchy] objects,
     * which encapsulate the information needed to manage split-screen tasks on that display.
     */
    private val displayTaskMap: MutableMap<Int, SplitTaskHierarchy> = mutableMapOf()

    /**
     * SplitTaskHierarchy is a class that encapsulates the components required
     * for managing split-screen functionality on a specific display.
     */
    data class SplitTaskHierarchy(
        var rootTaskInfo: ActivityManager.RunningTaskInfo? = null,
        var mainStage: StageTaskListener? = null,
        var sideStage: StageTaskListener? = null,
        var rootTaskLeash: SurfaceControl? = null,
        var splitLayout: SplitLayout? = null
    )

    /**
     * Returns a list of all currently connected display IDs.
     *
     * @return An ArrayList of display IDs.
     */
    fun getDisplayIds(): ArrayList<Int> {
        val displayIds = ArrayList<Int>()
        displayManager.displays?.forEach { display ->
            displayIds.add(display.displayId)
        }
        return displayIds
    }

    /**
     * Swaps the [SplitTaskHierarchy] objects associated with two different display IDs.
     *
     * @param firstDisplayId  The ID of the first display.
     * @param secondDisplayId The ID of the second display.
     */
    fun swapDisplayTaskHierarchy(firstDisplayId: Int, secondDisplayId: Int) {
        if (!displayTaskMap.containsKey(firstDisplayId) || !displayTaskMap.containsKey(secondDisplayId)) {
            ProtoLog.w(
                ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
                "Attempted to swap task hierarchies for invalid display IDs: %d, %d",
                firstDisplayId,
                secondDisplayId
            )
            return
        }

        if (firstDisplayId == secondDisplayId) {
            return
        }

        val firstHierarchy = displayTaskMap[firstDisplayId]
        val secondHierarchy = displayTaskMap[secondDisplayId]

        displayTaskMap[firstDisplayId] = checkNotNull(secondHierarchy)
        displayTaskMap[secondDisplayId] = checkNotNull(firstHierarchy)
    }

    /**
     * Gets the root task info for the given display ID.
     *
     * @param displayId The ID of the display.
     * @return The root task info, or null if not found.
     */
    fun getDisplayRootTaskInfo(displayId: Int): ActivityManager.RunningTaskInfo? {
        return displayTaskMap[displayId]?.rootTaskInfo
    }

    /**
     * Sets the root task info for the given display ID.
     *
     * @param displayId    The ID of the display.
     * @param rootTaskInfo The root task info to set.
     */
    fun setDisplayRootTaskInfo(
        displayId: Int,
        rootTaskInfo: ActivityManager.RunningTaskInfo?
    ) {
        val hierarchy = displayTaskMap.computeIfAbsent(displayId) { SplitTaskHierarchy() }
        hierarchy.rootTaskInfo = rootTaskInfo
    }
}
 No newline at end of file
+4 −4
Original line number Diff line number Diff line
@@ -14,16 +14,16 @@
 * limitations under the License.
 */

package com.android.wm.shell.splitscreen;
package com.android.wm.shell.splitscreen

import android.window.WindowContainerToken;
import android.window.WindowContainerToken

public interface SplitMultiDisplayProvider {
interface SplitMultiDisplayProvider {
    /**
     * Returns the WindowContainerToken for the root of the given display ID.
     *
     * @param displayId The ID of the display.
     * @return The {@link WindowContainerToken} associated with the display's root task.
     */
    WindowContainerToken getDisplayRootForDisplayId(int displayId);
    fun getDisplayRootForDisplayId(displayId: Int): WindowContainerToken?
}
+8 −0
Original line number Diff line number Diff line
@@ -101,6 +101,7 @@ import android.content.pm.LauncherApps;
import android.content.pm.ShortcutInfo;
import android.graphics.Rect;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.display.DisplayManager;
import android.os.Bundle;
import android.os.Debug;
import android.os.Handler;
@@ -278,6 +279,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
    // because we will be posting and removing it from the handler.
    private final Runnable mReEnableLaunchAdjacentOnRoot = () -> setLaunchAdjacentDisabled(false);

    private SplitMultiDisplayHelper mSplitMultiDisplayHelper;

    /**
     * Since StageCoordinator only coordinates MainStage and SideStage, it shouldn't support
     * CompatUI layouts. CompatUI is handled separately by MainStage and SideStage.
@@ -393,6 +396,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler,
        mDesktopTasksController = desktopTasksController;
        mRootTDAOrganizer = rootTDAOrganizer;

        DisplayManager displayManager = context.getSystemService(DisplayManager.class);

        mSplitMultiDisplayHelper = new SplitMultiDisplayHelper(
                Objects.requireNonNull(displayManager));

        taskOrganizer.createRootTask(displayId, WINDOWING_MODE_FULLSCREEN, this /* listener */);

        ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "Creating main/side root task");
+168 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.wm.shell.splitscreen

import android.app.ActivityManager
import android.hardware.display.DisplayManager
import android.view.Display
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.common.split.SplitLayout
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.mock
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations

/**
 * Unit tests for [SplitMultiDisplayHelper].
 */
@SmallTest
@RunWith(AndroidJUnit4::class)
class SplitMultiDisplayHelperTests : ShellTestCase() {

    private lateinit var splitMultiDisplayHelper: SplitMultiDisplayHelper

    @Mock
    private lateinit var mockDisplayManager: DisplayManager
    @Mock
    private lateinit var mockSplitLayout: SplitLayout
    @Mock
    private lateinit var mockDisplay1: Display
    @Mock
    private lateinit var mockDisplay2: Display

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        mockDisplay1 = mockDisplayManager.getDisplay(Display.DEFAULT_DISPLAY) ?: mock(Display::class.java)
        mockDisplay2 = mockDisplayManager.getDisplay(Display.DEFAULT_DISPLAY + 1) ?: mock(Display::class.java)

        `when`(mockDisplay1.displayId).thenReturn(Display.DEFAULT_DISPLAY)
        `when`(mockDisplay2.displayId).thenReturn(Display.DEFAULT_DISPLAY + 1)

        splitMultiDisplayHelper = SplitMultiDisplayHelper(mockDisplayManager)
    }

    @Test
    fun getDisplayIds_noDisplays_returnsEmptyList() {
        `when`(mockDisplayManager.displays).thenReturn(emptyArray())

        val displayIds = splitMultiDisplayHelper.getDisplayIds()

        assertThat(displayIds).isEmpty()
    }

    @Test
    fun getDisplayIds_singleDisplay_returnsCorrectId() {
        `when`(mockDisplayManager.displays).thenReturn(arrayOf(mockDisplay1))

        val displayIds = splitMultiDisplayHelper.getDisplayIds()

        assertThat(displayIds).containsExactly(Display.DEFAULT_DISPLAY)
    }

    @Test
    fun getDisplayIds_multiDisplays_returnsCorrectIds() {
        `when`(mockDisplayManager.displays).thenReturn(arrayOf(mockDisplay1, mockDisplay2))

        val displayIds = splitMultiDisplayHelper.getDisplayIds()

        assertThat(displayIds).containsExactly(Display.DEFAULT_DISPLAY, Display.DEFAULT_DISPLAY + 1)
    }

    @Test
    fun swapDisplayTaskHierarchy_validDisplays_swapsHierarchies() {
        val rootTaskInfo1 = ActivityManager.RunningTaskInfo().apply { taskId = 1 }
        val rootTaskInfo2 = ActivityManager.RunningTaskInfo().apply { taskId = 2 }

        splitMultiDisplayHelper.setDisplayRootTaskInfo(Display.DEFAULT_DISPLAY, rootTaskInfo1)
        splitMultiDisplayHelper.setDisplayRootTaskInfo(Display.DEFAULT_DISPLAY + 1, rootTaskInfo2)

        splitMultiDisplayHelper.swapDisplayTaskHierarchy(Display.DEFAULT_DISPLAY, Display.DEFAULT_DISPLAY + 1)

        assertThat(splitMultiDisplayHelper.getDisplayRootTaskInfo(Display.DEFAULT_DISPLAY)).isEqualTo(rootTaskInfo2)
        assertThat(splitMultiDisplayHelper.getDisplayRootTaskInfo(Display.DEFAULT_DISPLAY + 1)).isEqualTo(rootTaskInfo1)
    }

    @Test
    fun swapDisplayTaskHierarchy_invalidFirstDisplayId_doesNothing() {
        val rootTaskInfo2 = ActivityManager.RunningTaskInfo().apply { taskId = 2 }

        splitMultiDisplayHelper.setDisplayRootTaskInfo(Display.DEFAULT_DISPLAY + 1, rootTaskInfo2)

        splitMultiDisplayHelper.swapDisplayTaskHierarchy(Display.INVALID_DISPLAY, Display.DEFAULT_DISPLAY + 1)

        assertThat(splitMultiDisplayHelper.getDisplayRootTaskInfo(Display.INVALID_DISPLAY)).isNull()
        assertThat(splitMultiDisplayHelper.getDisplayRootTaskInfo(Display.DEFAULT_DISPLAY + 1)).isEqualTo(rootTaskInfo2)
    }

    @Test
    fun swapDisplayTaskHierarchy_invalidSecondDisplayId_doesNothing() {
        val rootTaskInfo1 = ActivityManager.RunningTaskInfo().apply { taskId = 1 }

        splitMultiDisplayHelper.setDisplayRootTaskInfo(Display.DEFAULT_DISPLAY, rootTaskInfo1)

        splitMultiDisplayHelper.swapDisplayTaskHierarchy(Display.DEFAULT_DISPLAY, Display.INVALID_DISPLAY)

        assertThat(splitMultiDisplayHelper.getDisplayRootTaskInfo(Display.DEFAULT_DISPLAY)).isEqualTo(rootTaskInfo1)
        assertThat(splitMultiDisplayHelper.getDisplayRootTaskInfo(Display.INVALID_DISPLAY)).isNull()
    }

    @Test
    fun swapDisplayTaskHierarchy_sameDisplayId_doesNothing() {
        val rootTaskInfo1 = ActivityManager.RunningTaskInfo().apply { taskId = 1 }

        splitMultiDisplayHelper.setDisplayRootTaskInfo(Display.DEFAULT_DISPLAY, rootTaskInfo1)

        splitMultiDisplayHelper.swapDisplayTaskHierarchy(Display.DEFAULT_DISPLAY, Display.DEFAULT_DISPLAY)

        assertThat(splitMultiDisplayHelper.getDisplayRootTaskInfo(Display.DEFAULT_DISPLAY)).isEqualTo(rootTaskInfo1)
    }

    @Test
    fun getDisplayRootTaskInfo_validDisplayId_returnsRootTaskInfo() {
        val rootTaskInfo = ActivityManager.RunningTaskInfo().apply { taskId = 123 }

        splitMultiDisplayHelper.setDisplayRootTaskInfo(Display.DEFAULT_DISPLAY, rootTaskInfo)

        val retrievedRootTaskInfo = splitMultiDisplayHelper.getDisplayRootTaskInfo(Display.DEFAULT_DISPLAY)

        assertThat(retrievedRootTaskInfo).isEqualTo(rootTaskInfo)
    }

    @Test
    fun getDisplayRootTaskInfo_invalidDisplayId_returnsNull() {
        val retrievedRootTaskInfo = splitMultiDisplayHelper.getDisplayRootTaskInfo(Display.INVALID_DISPLAY)

        assertThat(retrievedRootTaskInfo).isNull()
    }

    @Test
    fun setDisplayRootTaskInfo_setsRootTaskInfo() {
        val rootTaskInfo = ActivityManager.RunningTaskInfo().apply { taskId = 456 }

        splitMultiDisplayHelper.setDisplayRootTaskInfo(Display.DEFAULT_DISPLAY, rootTaskInfo)
        val retrievedRootTaskInfo = splitMultiDisplayHelper.getDisplayRootTaskInfo(Display.DEFAULT_DISPLAY)

        assertThat(retrievedRootTaskInfo).isEqualTo(rootTaskInfo)
    }
}
 No newline at end of file