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

Commit b8d3a4a9 authored by Kazuki Takise's avatar Kazuki Takise Committed by Android (Google) Code Review
Browse files

Merge "Introduce new presentation policies for Connected Displays" into main

parents 40747bb1 92ee62a1
Loading
Loading
Loading
Loading
+198 −26
Original line number Diff line number Diff line
@@ -16,10 +16,17 @@

package com.android.server.wm;

import static android.view.WindowManager.LayoutParams.TYPE_PRESENTATION;
import static android.view.WindowManager.LayoutParams.TYPE_PRIVATE_PRESENTATION;

import static com.android.internal.protolog.WmProtoLogGroups.WM_ERROR;
import static com.android.window.flags.Flags.enablePresentationForConnectedDisplays;

import android.annotation.NonNull;
import android.util.IntArray;
import android.annotation.Nullable;
import android.hardware.display.DisplayManager;
import android.util.SparseArray;
import android.view.WindowManager.LayoutParams.WindowType;

import com.android.internal.protolog.ProtoLog;
import com.android.internal.protolog.WmProtoLogGroups;
@@ -27,15 +34,125 @@ import com.android.internal.protolog.WmProtoLogGroups;
/**
 * Manages presentation windows.
 */
class PresentationController {
class PresentationController implements DisplayManager.DisplayListener {

    private static class Presentation {
        @NonNull final WindowState mWin;
        @NonNull final WindowContainerListener mPresentationListener;
        // This is the task which started this presentation. This shouldn't be null in most cases
        // because the intended usage of the Presentation API is that an activity that started a
        // presentation should control the UI and lifecycle of the presentation window.
        // However, the API doesn't necessarily requires a host activity to exist (e.g. a background
        // service can launch a presentation), so this can be null.
        @Nullable final Task mHostTask;
        @Nullable final WindowContainerListener mHostTaskListener;

        Presentation(@NonNull WindowState win,
                @NonNull WindowContainerListener presentationListener,
                @Nullable Task hostTask,
                @Nullable WindowContainerListener hostTaskListener) {
            mWin = win;
            mPresentationListener = presentationListener;
            mHostTask = hostTask;
            mHostTaskListener = hostTaskListener;
        }

        @Override
        public String toString() {
            return "{win: " + mWin.getName() + ", display: " + mWin.getDisplayId()
                    + ", hostTask: " + (mHostTask != null ? mHostTask.getName() : null) + "}";
        }
    }

    private final SparseArray<Presentation> mPresentations = new SparseArray();

    @Nullable
    private Presentation getPresentation(@Nullable WindowState win) {
        if (win == null) return null;
        for (int i = 0; i < mPresentations.size(); i++) {
            final Presentation presentation = mPresentations.valueAt(i);
            if (win == presentation.mWin) return presentation;
        }
        return null;
    }

    private boolean hasPresentationWindow(int displayId) {
        return mPresentations.contains(displayId);
    }

    private boolean isPresentationVisible(int displayId) {
        final Presentation presentation = mPresentations.get(displayId);
        return presentation != null && presentation.mWin.mToken.isVisibleRequested();
    }

    boolean canPresent(@NonNull WindowState win, @NonNull DisplayContent displayContent) {
        return canPresent(win, displayContent, win.mAttrs.type, win.getUid());
    }

    /**
     * Checks if a presentation window can be shown on the given display.
     * If the given |win| is empty, a new presentation window is being created.
     * If the given |win| is not empty, the window already exists as presentation, and we're
     * revalidate if the |win| is still qualified to be shown.
     */
    boolean canPresent(@Nullable WindowState win, @NonNull DisplayContent displayContent,
            @WindowType int type, int uid) {
        if (type == TYPE_PRIVATE_PRESENTATION) {
            // Private presentations can only be created on private displays.
            return displayContent.isPrivate();
        }

        if (type != TYPE_PRESENTATION) {
            return false;
        }

        if (!enablePresentationForConnectedDisplays()) {
            return displayContent.getDisplay().isPublicPresentation();
        }

        boolean allDisplaysArePresenting = true;
        for (int i = 0; i < displayContent.mWmService.mRoot.mChildren.size(); i++) {
            final DisplayContent dc = displayContent.mWmService.mRoot.mChildren.get(i);
            if (displayContent.mDisplayId != dc.mDisplayId
                    && !mPresentations.contains(dc.mDisplayId)) {
                allDisplaysArePresenting = false;
                break;
            }
        }
        if (allDisplaysArePresenting) {
            // All displays can't present simultaneously.
            return false;
        }

    // TODO(b/395475549): Add support for display add/remove, and activity move across displays.
    private final IntArray mPresentingDisplayIds = new IntArray();
        final int displayId = displayContent.mDisplayId;
        if (hasPresentationWindow(displayId)
                && win != null && win != mPresentations.get(displayId).mWin) {
            // A display can't have multiple presentations.
            return false;
        }

    PresentationController() {}
        Task hostTask = null;
        final Presentation presentation = getPresentation(win);
        if (presentation != null) {
            hostTask = presentation.mHostTask;
        } else if (win == null) {
            final Task globallyFocusedTask =
                    displayContent.mWmService.mRoot.getTopDisplayFocusedRootTask();
            if (globallyFocusedTask != null && uid == globallyFocusedTask.effectiveUid) {
                hostTask = globallyFocusedTask;
            }
        }
        if (hostTask != null && displayId == hostTask.getDisplayId()) {
            // A presentation can't cover its own host task.
            return false;
        }
        if (hostTask == null && !displayContent.getDisplay().isPublicPresentation()) {
            // A globally focused host task on a different display is needed to show a
            // presentation on a non-presenting display.
            return false;
        }

    private boolean isPresenting(int displayId) {
        return mPresentingDisplayIds.contains(displayId);
        return true;
    }

    boolean shouldOccludeActivities(int displayId) {
@@ -45,32 +162,87 @@ class PresentationController {
        // be shown on them.
        // TODO(b/390481621): Disallow a presentation from covering its controlling activity so that
        // the presentation won't stop its controlling activity.
        return enablePresentationForConnectedDisplays() && isPresenting(displayId);
        return enablePresentationForConnectedDisplays() && isPresentationVisible(displayId);
    }

    void onPresentationAdded(@NonNull WindowState win) {
    void onPresentationAdded(@NonNull WindowState win, int uid) {
        final int displayId = win.getDisplayId();
        if (isPresenting(displayId)) {
            return;
        }
        ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION, "Presentation added to display %d: %s",
                win.getDisplayId(), win);
        mPresentingDisplayIds.add(win.getDisplayId());
                displayId, win);
        win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ true);
    }

    void onPresentationRemoved(@NonNull WindowState win) {
        final int displayId = win.getDisplayId();
        if (!isPresenting(displayId)) {
        final WindowContainerListener presentationWindowListener = new WindowContainerListener() {
            @Override
            public void onRemoved() {
                if (!hasPresentationWindow(displayId)) {
                    ProtoLog.e(WM_ERROR, "Failed to remove presentation on"
                            + "non-presenting display %d: %s", displayId, win);
                    return;
                }
        ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION,
                "Presentation removed from display %d: %s", win.getDisplayId(), win);
        // TODO(b/393945496): Make sure that there's one presentation at most per display.
        final int displayIdIndex = mPresentingDisplayIds.indexOf(displayId);
        if (displayIdIndex != -1) {
            mPresentingDisplayIds.remove(displayIdIndex);
                final Presentation presentation = mPresentations.get(displayId);
                win.mToken.unregisterWindowContainerListener(presentation.mPresentationListener);
                if (presentation.mHostTask != null) {
                    presentation.mHostTask.unregisterWindowContainerListener(
                            presentation.mHostTaskListener);
                }
                mPresentations.remove(displayId);
                win.mWmService.mDisplayManagerInternal.onPresentation(displayId, false /*isShown*/);
            }
        };
        win.mToken.registerWindowContainerListener(presentationWindowListener);

        Task hostTask = null;
        if (enablePresentationForConnectedDisplays()) {
            final Task globallyFocusedTask =
                    win.mWmService.mRoot.getTopDisplayFocusedRootTask();
            if (globallyFocusedTask != null && uid == globallyFocusedTask.effectiveUid) {
                hostTask = globallyFocusedTask;
            }
        }
        win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ false);
        WindowContainerListener hostTaskListener = null;
        if (hostTask != null) {
            hostTaskListener = new WindowContainerListener() {
                public void onDisplayChanged(DisplayContent dc) {
                    final Presentation presentation = mPresentations.get(dc.getDisplayId());
                    if (presentation != null && !canPresent(presentation.mWin, dc)) {
                        removePresentation(dc.mDisplayId, "host task moved to display "
                                + dc.getDisplayId());
                    }
                }

                public void onRemoved() {
                    removePresentation(win.getDisplayId(), "host task removed");
                }
            };
            hostTask.registerWindowContainerListener(hostTaskListener);
        }

        mPresentations.put(displayId, new Presentation(win, presentationWindowListener, hostTask,
                hostTaskListener));
    }

    void removePresentation(int displayId, @NonNull String reason) {
        final Presentation presentation = mPresentations.get(displayId);
        if (enablePresentationForConnectedDisplays() && presentation != null) {
            ProtoLog.v(WmProtoLogGroups.WM_DEBUG_PRESENTATION, "Removing Presentation %s for "
                    + "reason %s", mPresentations.get(displayId), reason);
            final WindowState win = presentation.mWin;
            win.mWmService.mAtmService.mH.post(() -> {
                synchronized (win.mWmService.mGlobalLock) {
                    win.removeIfPossible();
                }
            });
        }
    }

    @Override
    public void onDisplayAdded(int displayId) {}

    @Override
    public void onDisplayRemoved(int displayId) {
        removePresentation(displayId, "display removed " + displayId);
    }

    @Override
    public void onDisplayChanged(int displayId) {}
}
+12 −6
Original line number Diff line number Diff line
@@ -1583,14 +1583,18 @@ public class WindowManagerService extends IWindowManager.Stub
                return WindowManagerGlobal.ADD_DUPLICATE_ADD;
            }

            if (type == TYPE_PRIVATE_PRESENTATION && !displayContent.isPrivate()) {
            if (type == TYPE_PRIVATE_PRESENTATION
                    && !mPresentationController.canPresent(null /*win*/, displayContent, type,
                    callingUid)) {
                ProtoLog.w(WM_ERROR,
                        "Attempted to add private presentation window to a non-private display.  "
                                + "Aborting.");
                return WindowManagerGlobal.ADD_PERMISSION_DENIED;
            }

            if (type == TYPE_PRESENTATION && !displayContent.getDisplay().isPublicPresentation()) {
            if (type == TYPE_PRESENTATION
                    && !mPresentationController.canPresent(null /*win*/, displayContent, type,
                    callingUid)) {
                ProtoLog.w(WM_ERROR,
                        "Attempted to add presentation window to a non-suitable display.  "
                                + "Aborting.");
@@ -1830,7 +1834,8 @@ public class WindowManagerService extends IWindowManager.Stub
                }
                win.mTransitionController.collect(win.mToken);
                res |= addWindowInner(win, displayPolicy, activity, displayContent, outInsetsState,
                        outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs);
                        outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs,
                        callingUid);
                // A presentation hides all activities behind on the same display.
                win.mDisplayContent.ensureActivitiesVisible(/*starting=*/ null,
                        /*notifyClients=*/ true);
@@ -1841,7 +1846,8 @@ public class WindowManagerService extends IWindowManager.Stub
                }
            } else {
                res |= addWindowInner(win, displayPolicy, activity, displayContent, outInsetsState,
                        outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs);
                        outAttachedFrame, outActiveControls, client, outSizeCompatScale, attrs,
                        callingUid);
            }
        }

@@ -1854,7 +1860,7 @@ public class WindowManagerService extends IWindowManager.Stub
            @NonNull ActivityRecord activity, @NonNull DisplayContent displayContent,
            @NonNull InsetsState outInsetsState, @NonNull Rect outAttachedFrame,
            @NonNull InsetsSourceControl.Array outActiveControls, @NonNull IWindow client,
            @NonNull float[] outSizeCompatScale, @NonNull LayoutParams attrs) {
            @NonNull float[] outSizeCompatScale, @NonNull LayoutParams attrs, int uid) {
        int res = 0;
        final int type = attrs.type;
        boolean imMayMove = true;
@@ -1971,7 +1977,7 @@ public class WindowManagerService extends IWindowManager.Stub
        outSizeCompatScale[0] = win.getCompatScaleForClient();

        if (res >= ADD_OKAY && win.isPresentation()) {
            mPresentationController.onPresentationAdded(win);
            mPresentationController.onPresentationAdded(win, uid);
        }

        return res;
+0 −1
Original line number Diff line number Diff line
@@ -2435,7 +2435,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
                mAnimatingExit = true;
                mRemoveOnExit = true;
                mToken.setVisibleRequested(false);
                mWmService.mPresentationController.onPresentationRemoved(this);
                // A presentation hides all activities behind on the same display.
                mDisplayContent.ensureActivitiesVisible(/*starting=*/ null,
                        /*notifyClients=*/ true);
+130 −1
Original line number Diff line number Diff line
@@ -16,9 +16,12 @@

package com.android.server.wm;

import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.FLAG_PRESENTATION;
import static android.view.Display.FLAG_TRUSTED;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_WAKE;

import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.window.flags.Flags.FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS;
@@ -30,6 +33,7 @@ import static org.mockito.ArgumentMatchers.eq;

import android.annotation.NonNull;
import android.graphics.Rect;
import android.os.Binder;
import android.os.UserHandle;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
@@ -118,6 +122,112 @@ public class PresentationControllerTests extends WindowTestsBase {
        assertFalse(window.isAttached());
    }

    @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS)
    @Test
    public void testPresentationCannotCoverHostTask() {
        int uid = Binder.getCallingUid();
        final DisplayContent presentationDisplay = createPresentationDisplay();
        final Task task = createTask(presentationDisplay);
        task.effectiveUid = uid;
        final ActivityRecord activity = createActivityRecord(task);
        assertTrue(activity.isVisible());

        // Adding a presentation window over its host task must fail.
        assertAddPresentationWindowFails(uid, presentationDisplay.mDisplayId);

        // Adding a presentation window on the other display must succeed.
        final WindowState window = addPresentationWindow(uid, DEFAULT_DISPLAY);
        final Transition addTransition = window.mTransitionController.getCollectingTransition();
        completeTransition(addTransition, /*abortSync=*/ true);
        assertTrue(window.isVisible());

        // Moving the host task to the presenting display will remove the presentation.
        task.reparent(mDefaultDisplay.getDefaultTaskDisplayArea(), true);
        waitHandlerIdle(window.mWmService.mAtmService.mH);
        final Transition removeTransition = window.mTransitionController.getCollectingTransition();
        assertEquals(TRANSIT_CLOSE, removeTransition.mType);
        completeTransition(removeTransition, /*abortSync=*/ false);
        assertFalse(window.isVisible());
    }

    @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS)
    @Test
    public void testPresentationCannotLaunchOnAllDisplays() {
        final int uid = Binder.getCallingUid();
        final DisplayContent presentationDisplay = createPresentationDisplay();
        final Task task = createTask(presentationDisplay);
        task.effectiveUid = uid;
        final ActivityRecord activity = createActivityRecord(task);
        assertTrue(activity.isVisible());

        // Add a presentation window on the default display.
        final WindowState window = addPresentationWindow(uid, DEFAULT_DISPLAY);
        final Transition addTransition = window.mTransitionController.getCollectingTransition();
        completeTransition(addTransition, /*abortSync=*/ true);
        assertTrue(window.isVisible());

        // Adding another presentation window over the task even if it's a different UID because
        // it would end up showing presentations on all displays.
        assertAddPresentationWindowFails(uid + 1, presentationDisplay.mDisplayId);
    }

    @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS)
    @Test
    public void testPresentationCannotLaunchOnNonPresentationDisplayWithoutHostHavingGlobalFocus() {
        final int uid = Binder.getCallingUid();
        // Adding a presentation window on an internal display requires a host task
        // with global focus on another display.
        assertAddPresentationWindowFails(uid, DEFAULT_DISPLAY);

        final DisplayContent presentationDisplay = createPresentationDisplay();
        final Task taskWiSameUid = createTask(presentationDisplay);
        taskWiSameUid.effectiveUid = uid;
        final ActivityRecord activity = createActivityRecord(taskWiSameUid);
        assertTrue(activity.isVisible());
        final Task taskWithDifferentUid = createTask(presentationDisplay);
        taskWithDifferentUid.effectiveUid = uid + 1;
        createActivityRecord(taskWithDifferentUid);
        assertEquals(taskWithDifferentUid, presentationDisplay.getFocusedRootTask());

        // The task with the same UID is covered by another task with a different UID, so this must
        // also fail.
        assertAddPresentationWindowFails(uid, DEFAULT_DISPLAY);

        // Moving the task with the same UID to front and giving it global focus allows a
        // presentation to show on the default display.
        taskWiSameUid.moveToFront("test");
        final WindowState window = addPresentationWindow(uid, DEFAULT_DISPLAY);
        final Transition addTransition = window.mTransitionController.getCollectingTransition();
        completeTransition(addTransition, /*abortSync=*/ true);
        assertTrue(window.isVisible());
    }

    @EnableFlags(FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS)
    @Test
    public void testReparentingActivityToSameDisplayClosesPresentation() {
        final int uid = Binder.getCallingUid();
        final Task task = createTask(mDefaultDisplay);
        task.effectiveUid = uid;
        final ActivityRecord activity = createActivityRecord(task);
        assertTrue(activity.isVisible());

        // Add a presentation window on a presentation display.
        final DisplayContent presentationDisplay = createPresentationDisplay();
        final WindowState window = addPresentationWindow(uid, presentationDisplay.getDisplayId());
        final Transition addTransition = window.mTransitionController.getCollectingTransition();
        completeTransition(addTransition, /*abortSync=*/ true);
        assertTrue(window.isVisible());

        // Reparenting the host task below the presentation must close the presentation.
        task.reparent(presentationDisplay.getDefaultTaskDisplayArea(), true);
        waitHandlerIdle(window.mWmService.mAtmService.mH);
        final Transition removeTransition = window.mTransitionController.getCollectingTransition();
        // It's a WAKE transition instead of CLOSE because
        assertEquals(TRANSIT_WAKE, removeTransition.mType);
        completeTransition(removeTransition, /*abortSync=*/ false);
        assertFalse(window.isVisible());
    }

    private WindowState addPresentationWindow(int uid, int displayId) {
        final Session session = createTestSession(mAtm, 1234 /* pid */, uid);
        final int userId = UserHandle.getUserId(uid);
@@ -134,10 +244,29 @@ public class PresentationControllerTests extends WindowTestsBase {
        return window;
    }

    private void assertAddPresentationWindowFails(int uid, int displayId) {
        final Session session = createTestSession(mAtm, 1234 /* pid */, uid);
        final IWindow clientWindow = new TestIWindow();
        final int res = addPresentationWindowInner(uid, displayId, session, clientWindow);
        assertEquals(WindowManagerGlobal.ADD_INVALID_DISPLAY, res);
    }

    private int addPresentationWindowInner(int uid, int displayId, Session session,
            IWindow clientWindow) {
        final int userId = UserHandle.getUserId(uid);
        doReturn(true).when(mWm.mUmInternal).isUserVisible(eq(userId), eq(displayId));
        final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.TYPE_PRESENTATION);
        return mWm.addWindow(session, clientWindow, params, View.VISIBLE, displayId, userId,
                WindowInsets.Type.defaultVisible(), null, new InsetsState(),
                new InsetsSourceControl.Array(), new Rect(), new float[1]);
    }

    private DisplayContent createPresentationDisplay() {
        final DisplayInfo displayInfo = new DisplayInfo();
        displayInfo.copyFrom(mDisplayInfo);
        displayInfo.flags = FLAG_PRESENTATION;
        displayInfo.flags = FLAG_PRESENTATION | FLAG_TRUSTED;
        displayInfo.displayId = DEFAULT_DISPLAY + 1;
        final DisplayContent dc = createNewDisplay(displayInfo);
        final int displayId = dc.getDisplayId();
        doReturn(dc).when(mWm.mRoot).getDisplayContentOrCreate(displayId);