diff options
4 files changed, 340 insertions, 34 deletions
diff --git a/services/core/java/com/android/server/wm/PresentationController.java b/services/core/java/com/android/server/wm/PresentationController.java index b3cff9c6cc3d..913f0b9a981d 100644 --- a/services/core/java/com/android/server/wm/PresentationController.java +++ b/services/core/java/com/android/server/wm/PresentationController.java @@ -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; + } - // TODO(b/395475549): Add support for display add/remove, and activity move across displays. - private final IntArray mPresentingDisplayIds = new IntArray(); + private boolean hasPresentationWindow(int displayId) { + return mPresentations.contains(displayId); + } - PresentationController() {} + private boolean isPresentationVisible(int displayId) { + final Presentation presentation = mPresentations.get(displayId); + return presentation != null && presentation.mWin.mToken.isVisibleRequested(); + } - private boolean isPresenting(int displayId) { - return mPresentingDisplayIds.contains(displayId); + 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; + } + + final int displayId = displayContent.mDisplayId; + if (hasPresentationWindow(displayId) + && win != null && win != mPresentations.get(displayId).mWin) { + // A display can't have multiple presentations. + return false; + } + + 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; + } + + 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)) { - return; + 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; + } + 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; + } + } + 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); } - 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); + + 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(); + } + }); } - win.mWmService.mDisplayManagerInternal.onPresentation(displayId, /*isShown=*/ false); } + + @Override + public void onDisplayAdded(int displayId) {} + + @Override + public void onDisplayRemoved(int displayId) { + removePresentation(displayId, "display removed " + displayId); + } + + @Override + public void onDisplayChanged(int displayId) {} } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 8aed91b2dc66..6ebda77d3bec 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -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; diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index bfedc90497ae..17c5e96dea01 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -2439,7 +2439,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); diff --git a/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java index 2d4101e40615..6e0f7fbbf388 100644 --- a/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/PresentationControllerTests.java @@ -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); |