diff options
Diffstat (limited to 'libs')
26 files changed, 965 insertions, 177 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 0f04321d1483..46a3e7f38bed 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -861,9 +861,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // container update. updateDivider(wct, taskContainer); - // If the last direct activity of the host task is dismissed and the overlay container is - // the only taskFragment, the overlay container should also be dismissed. - dismissOverlayContainerIfNeeded(wct, taskContainer); + // If the last direct activity of the host task is dismissed and there's an always-on-top + // overlay container in the task, the overlay container should also be dismissed. + dismissAlwaysOnTopOverlayIfNeeded(wct, taskContainer); if (!shouldUpdateContainer) { return; @@ -1990,7 +1990,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @NonNull TaskFragmentContainer container) { final TaskContainer taskContainer = container.getTaskContainer(); - if (dismissOverlayContainerIfNeeded(wct, taskContainer)) { + if (dismissAlwaysOnTopOverlayIfNeeded(wct, taskContainer)) { return; } @@ -2014,22 +2014,27 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } } - /** Dismisses the overlay container in the {@code taskContainer} if needed. */ + /** + * Dismisses {@link TaskFragmentContainer#isAlwaysOnTopOverlay()} in the {@code taskContainer} + * if needed. + */ @GuardedBy("mLock") - private boolean dismissOverlayContainerIfNeeded(@NonNull WindowContainerTransaction wct, - @NonNull TaskContainer taskContainer) { - final TaskFragmentContainer overlayContainer = taskContainer.getOverlayContainer(); - if (overlayContainer == null) { + private boolean dismissAlwaysOnTopOverlayIfNeeded(@NonNull WindowContainerTransaction wct, + @NonNull TaskContainer taskContainer) { + // Dismiss always-on-top overlay container if it's the only container in the task and + // there's no direct activity in the parent task. + final List<TaskFragmentContainer> containers = taskContainer.getTaskFragmentContainers(); + if (containers.size() != 1 || taskContainer.hasDirectActivity()) { return false; } - // Dismiss the overlay container if it's the only container in the task and there's no - // direct activity in the parent task. - if (taskContainer.getTaskFragmentContainers().size() == 1 - && !taskContainer.hasDirectActivity()) { - mPresenter.cleanupContainer(wct, overlayContainer, false /* shouldFinishDependant */); - return true; + + final TaskFragmentContainer container = containers.getLast(); + if (!container.isAlwaysOnTopOverlay()) { + return false; } - return false; + + mPresenter.cleanupContainer(wct, container, false /* shouldFinishDependant */); + return true; } /** @@ -2620,25 +2625,42 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen /** * Gets all overlay containers from all tasks in this process, or an empty list if there's * no overlay container. - * <p> - * Note that we only support one overlay container for each task, but an app could have multiple - * tasks. */ @VisibleForTesting @GuardedBy("mLock") @NonNull - List<TaskFragmentContainer> getAllOverlayTaskFragmentContainers() { + List<TaskFragmentContainer> getAllNonFinishingOverlayContainers() { final List<TaskFragmentContainer> overlayContainers = new ArrayList<>(); for (int i = 0; i < mTaskContainers.size(); i++) { final TaskContainer taskContainer = mTaskContainers.valueAt(i); - final TaskFragmentContainer overlayContainer = taskContainer.getOverlayContainer(); - if (overlayContainer != null) { - overlayContainers.add(overlayContainer); - } + final List<TaskFragmentContainer> overlayContainersPerTask = taskContainer + .getTaskFragmentContainers() + .stream() + .filter(c -> c.isOverlay() && !c.isFinished()) + .toList(); + overlayContainers.addAll(overlayContainersPerTask); } return overlayContainers; } + /** + * Creates an overlay container or updates a visible overlay container if its + * {@link TaskFragmentContainer#getTaskId()}, {@link TaskFragmentContainer#getOverlayTag()} + * and {@link TaskFragmentContainer#getAssociatedActivityToken()} matches. + * <p> + * This method will also dismiss any existing overlay container if: + * <ul> + * <li>it's visible but not meet the criteria to update overlay</li> + * <li>{@link TaskFragmentContainer#getOverlayTag()} matches but not meet the criteria to + * update overlay</li> + * </ul> + * + * @param wct the {@link WindowContainerTransaction} + * @param options the {@link ActivityOptions} to launch the overlay + * @param intent the intent of activity to launch + * @param launchActivity the activity to launch the overlay container + * @return the overlay container + */ @VisibleForTesting // Suppress GuardedBy warning because lint ask to mark this method as // @GuardedBy(container.mController.mLock), which is mLock itself @@ -2649,7 +2671,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @NonNull WindowContainerTransaction wct, @NonNull Bundle options, @NonNull Intent intent, @NonNull Activity launchActivity) { final List<TaskFragmentContainer> overlayContainers = - getAllOverlayTaskFragmentContainers(); + getAllNonFinishingOverlayContainers(); final String overlayTag = Objects.requireNonNull(options.getString(KEY_OVERLAY_TAG)); final boolean associateLaunchingActivity = options .getBoolean(KEY_OVERLAY_ASSOCIATE_WITH_LAUNCHING_ACTIVITY, true); @@ -2673,62 +2695,86 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen final int taskId = getTaskId(launchActivity); if (!overlayContainers.isEmpty()) { for (final TaskFragmentContainer overlayContainer : overlayContainers) { - if (!overlayTag.equals(overlayContainer.getOverlayTag()) - && taskId == overlayContainer.getTaskId()) { - // If there's an overlay container with different tag shown in the same - // task, dismiss the existing overlay container. - mPresenter.cleanupContainer(wct, overlayContainer, - false /* shouldFinishDependant */); - } - if (overlayTag.equals(overlayContainer.getOverlayTag()) - && taskId != overlayContainer.getTaskId()) { - Log.w(TAG, "The overlay container with tag:" - + overlayContainer.getOverlayTag() + " is dismissed because" - + " there's an existing overlay container with the same tag but" - + " different task ID:" + overlayContainer.getTaskId() + ". " - + "The new associated activity is " + launchActivity); + final boolean isTopNonFinishingOverlay = overlayContainer.equals( + overlayContainer.getTaskContainer().getTopNonFinishingTaskFragmentContainer( + true /* includePin */, true /* includeOverlay */)); + if (taskId != overlayContainer.getTaskId()) { // If there's an overlay container with same tag in a different task, // dismiss the overlay container since the tag must be unique per process. - mPresenter.cleanupContainer(wct, overlayContainer, - false /* shouldFinishDependant */); - } - if (overlayTag.equals(overlayContainer.getOverlayTag()) - && taskId == overlayContainer.getTaskId()) { - if (associateLaunchingActivity && !launchActivity.getActivityToken() - .equals(overlayContainer.getAssociatedActivityToken())) { + if (overlayTag.equals(overlayContainer.getOverlayTag())) { Log.w(TAG, "The overlay container with tag:" + overlayContainer.getOverlayTag() + " is dismissed because" + " there's an existing overlay container with the same tag but" - + " different associated launching activity. The new associated" - + " activity is " + launchActivity); - // The associated activity must be the same, or it will be dismissed. + + " different task ID:" + overlayContainer.getTaskId() + ". " + + "The new associated activity is " + launchActivity); + mPresenter.cleanupContainer(wct, overlayContainer, + false /* shouldFinishDependant */); + } + continue; + } + if (!overlayTag.equals(overlayContainer.getOverlayTag())) { + // If there's an overlay container with different tag on top in the same + // task, dismiss the existing overlay container. + if (isTopNonFinishingOverlay) { mPresenter.cleanupContainer(wct, overlayContainer, false /* shouldFinishDependant */); - } else if (!associateLaunchingActivity - && overlayContainer.isAssociatedWithActivity()) { + } + continue; + } + // The overlay container has the same tag and task ID with the new launching + // overlay container. + if (!isTopNonFinishingOverlay) { + // Dismiss the invisible overlay container regardless of activity + // association if it collides the tag of new launched overlay container . + Log.w(TAG, "The invisible overlay container with tag:" + + overlayContainer.getOverlayTag() + " is dismissed because" + + " there's a launching overlay container with the same tag." + + " The new associated activity is " + launchActivity); + mPresenter.cleanupContainer(wct, overlayContainer, + false /* shouldFinishDependant */); + continue; + } + // Requesting an always-on-top overlay. + if (!associateLaunchingActivity) { + if (overlayContainer.isAssociatedWithActivity()) { + // Dismiss the overlay container since it has associated with an activity. Log.w(TAG, "The overlay container with tag:" + overlayContainer.getOverlayTag() + " is dismissed because" + " there's an existing overlay container with the same tag but" + " different associated launching activity. The overlay container" + " doesn't associate with any activity."); - // Dismiss the overlay container since it has been associated with an - // activity. mPresenter.cleanupContainer(wct, overlayContainer, false /* shouldFinishDependant */); + continue; } else { - // Just update the overlay container if - // - should associate with an activity and associated activity matches - // - should not associate with an activity and the overlay container - // don't have an associated activity + // The existing overlay container doesn't associate an activity as well. + // Just update the overlay and return. + // Note that going to this condition means the tag, task ID matches a + // visible always-on-top overlay, and won't dismiss any overlay any more. mPresenter.applyActivityStackAttributes(wct, overlayContainer, attrs, getMinDimensions(intent)); - // We can just return the updated overlay container and don't need to - // check other condition since we only have one OverlayCreateParams, and - // if the tag and task are matched, it's impossible to match another task - // or tag since tags and tasks are all unique. return overlayContainer; } } + if (launchActivity.getActivityToken() + != overlayContainer.getAssociatedActivityToken()) { + Log.w(TAG, "The overlay container with tag:" + + overlayContainer.getOverlayTag() + " is dismissed because" + + " there's an existing overlay container with the same tag but" + + " different associated launching activity. The new associated" + + " activity is " + launchActivity); + // The associated activity must be the same, or it will be dismissed. + mPresenter.cleanupContainer(wct, overlayContainer, + false /* shouldFinishDependant */); + continue; + } + // Reaching here means the launching activity launch an overlay container with the + // same task ID, tag, while there's a previously launching visible overlay + // container. We'll regard it as updating the existing overlay container. + mPresenter.applyActivityStackAttributes(wct, overlayContainer, attrs, + getMinDimensions(intent)); + return overlayContainer; + } } // Launch the overlay container to the task with taskId. diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index b56f671359c6..6231ea09e5cc 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -467,6 +467,11 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { reorderTaskFragmentToFront(wct, pinnedContainer.getSecondaryContainer().getTaskFragmentToken()); } + final TaskFragmentContainer alwaysOnTopOverlayContainer = container.getTaskContainer() + .getAlwaysOnTopOverlayContainer(); + if (alwaysOnTopOverlayContainer != null) { + reorderTaskFragmentToFront(wct, alwaysOnTopOverlayContainer.getTaskFragmentToken()); + } } @Override diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index 3dd96c468f9b..fdf0910519b5 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -70,9 +70,11 @@ class TaskContainer { @Nullable private SplitPinContainer mSplitPinContainer; - /** The overlay container in this Task. */ + /** + * The {@link TaskFragmentContainer#isAlwaysOnTopOverlay()} in the Task. + */ @Nullable - private TaskFragmentContainer mOverlayContainer; + private TaskFragmentContainer mAlwaysOnTopOverlayContainer; @NonNull private final Configuration mConfiguration; @@ -316,10 +318,12 @@ class TaskContainer { return null; } - /** Returns the overlay container in the task, or {@code null} if it doesn't exist. */ + /** + * Returns the always-on-top overlay container in the task, or {@code null} if it doesn't exist. + */ @Nullable - TaskFragmentContainer getOverlayContainer() { - return mOverlayContainer; + TaskFragmentContainer getAlwaysOnTopOverlayContainer() { + return mAlwaysOnTopOverlayContainer; } int indexOf(@NonNull TaskFragmentContainer child) { @@ -531,7 +535,21 @@ class TaskContainer { updateSplitPinContainerIfNecessary(); // Update overlay container after split pin container since the overlay should be on top of // pin container. - updateOverlayContainerIfNecessary(); + updateAlwaysOnTopOverlayIfNecessary(); + } + + private void updateAlwaysOnTopOverlayIfNecessary() { + final List<TaskFragmentContainer> alwaysOnTopOverlays = mContainers + .stream().filter(TaskFragmentContainer::isAlwaysOnTopOverlay).toList(); + if (alwaysOnTopOverlays.size() > 1) { + throw new IllegalStateException("There must be at most one always-on-top overlay " + + "container per Task"); + } + mAlwaysOnTopOverlayContainer = alwaysOnTopOverlays.isEmpty() + ? null : alwaysOnTopOverlays.getFirst(); + if (mAlwaysOnTopOverlayContainer != null) { + moveContainerToLastIfNecessary(mAlwaysOnTopOverlayContainer); + } } private void updateSplitPinContainerIfNecessary() { @@ -559,18 +577,6 @@ class TaskContainer { } } - private void updateOverlayContainerIfNecessary() { - final List<TaskFragmentContainer> overlayContainers = mContainers.stream() - .filter(TaskFragmentContainer::isOverlay).toList(); - if (overlayContainers.size() > 1) { - throw new IllegalStateException("There must be at most one overlay container per Task"); - } - mOverlayContainer = overlayContainers.isEmpty() ? null : overlayContainers.get(0); - if (mOverlayContainer != null) { - moveContainerToLastIfNecessary(mOverlayContainer); - } - } - /** Moves the {@code container} to the last to align taskFragments' z-order. */ private void moveContainerToLastIfNecessary(@NonNull TaskFragmentContainer container) { final int index = mContainers.indexOf(container); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index 5dbb016bb7e0..094ebcb470f2 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -1023,6 +1023,14 @@ class TaskFragmentContainer { return mAssociatedActivityToken != null; } + /** + * Returns {@code true} if the overlay container should be always on top, which should be + * a non-fill-parent overlay without activity association. + */ + boolean isAlwaysOnTopOverlay() { + return isOverlay() && !isAssociatedWithActivity(); + } + @Override public String toString() { return toString(true /* includeContainersToFinishOnExit */); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java index dcdbe59cb7cb..b1b1984e3a70 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java @@ -178,37 +178,59 @@ public class OverlayPresentationTest { } @Test - public void testGetOverlayContainers() { - assertThat(mSplitController.getAllOverlayTaskFragmentContainers()).isEmpty(); + public void testGetAllNonFinishingOverlayContainers() { + assertThat(mSplitController.getAllNonFinishingOverlayContainers()).isEmpty(); final TaskFragmentContainer overlayContainer1 = createTestOverlayContainer(TASK_ID, "test1"); - assertThat(mSplitController.getAllOverlayTaskFragmentContainers()) + assertThat(mSplitController.getAllNonFinishingOverlayContainers()) .containsExactly(overlayContainer1); - assertThrows( - "The exception must throw if there are two overlay containers in the same task.", - IllegalStateException.class, - () -> createTestOverlayContainer(TASK_ID, "test2")); + final TaskFragmentContainer overlayContainer2 = + createTestOverlayContainer(TASK_ID, "test2"); + + assertThat(mSplitController.getAllNonFinishingOverlayContainers()) + .containsExactly(overlayContainer1, overlayContainer2); final TaskFragmentContainer overlayContainer3 = createTestOverlayContainer(TASK_ID + 1, "test3"); - assertThat(mSplitController.getAllOverlayTaskFragmentContainers()) - .containsExactly(overlayContainer1, overlayContainer3); + assertThat(mSplitController.getAllNonFinishingOverlayContainers()) + .containsExactly(overlayContainer1, overlayContainer2, overlayContainer3); + + final TaskFragmentContainer finishingOverlayContainer = + createTestOverlayContainer(TASK_ID, "test4"); + spyOn(finishingOverlayContainer); + doReturn(true).when(finishingOverlayContainer).isFinished(); + + assertThat(mSplitController.getAllNonFinishingOverlayContainers()) + .containsExactly(overlayContainer1, overlayContainer2, overlayContainer3); } @Test - public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_anotherTagInTask_dismissOverlay() { + public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_anotherTagInTask() { + createExistingOverlayContainers(false /* visible */); + createMockTaskFragmentContainer(mActivity); + + final TaskFragmentContainer overlayContainer = + createOrUpdateOverlayTaskFragmentIfNeeded("test3"); + + assertWithMessage("overlayContainer1 is still there since it's not visible.") + .that(mSplitController.getAllNonFinishingOverlayContainers()) + .containsExactly(mOverlayContainer1, mOverlayContainer2, overlayContainer); + } + + @Test + public void testCreateOrUpdateOverlay_visibleOverlaySameTagInTask_dismissOverlay() { createExistingOverlayContainers(); final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded("test3"); - assertWithMessage("overlayContainer1 must be dismissed since the new overlay container" - + " is launched to the same task") - .that(mSplitController.getAllOverlayTaskFragmentContainers()) + assertWithMessage("overlayContainer1 must be dismissed since it's visible" + + " in the same task.") + .that(mSplitController.getAllNonFinishingOverlayContainers()) .containsExactly(mOverlayContainer2, overlayContainer); } @@ -223,7 +245,7 @@ public class OverlayPresentationTest { assertWithMessage("overlayContainer1 must be dismissed since the new overlay container" + " is launched with the same tag as an existing overlay container in a different " + "task") - .that(mSplitController.getAllOverlayTaskFragmentContainers()) + .that(mSplitController.getAllNonFinishingOverlayContainers()) .containsExactly(mOverlayContainer2, overlayContainer); } @@ -240,7 +262,7 @@ public class OverlayPresentationTest { assertWithMessage("overlayContainer1 must be updated since the new overlay container" + " is launched with the same tag and task") - .that(mSplitController.getAllOverlayTaskFragmentContainers()) + .that(mSplitController.getAllNonFinishingOverlayContainers()) .containsExactly(mOverlayContainer1, mOverlayContainer2); assertThat(overlayContainer).isEqualTo(mOverlayContainer1); @@ -260,11 +282,26 @@ public class OverlayPresentationTest { assertWithMessage("overlayContainer1 must be dismissed since the new overlay container" + " is associated with different launching activity") - .that(mSplitController.getAllOverlayTaskFragmentContainers()) + .that(mSplitController.getAllNonFinishingOverlayContainers()) .containsExactly(mOverlayContainer2, overlayContainer); } @Test + public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_dismissOverlay() { + createExistingOverlayContainers(false /* visible */); + createMockTaskFragmentContainer(mActivity); + + final TaskFragmentContainer overlayContainer = + createOrUpdateOverlayTaskFragmentIfNeeded("test2"); + + // OverlayContainer2 is dismissed since new container is launched with the + // same tag in different task. + assertWithMessage("overlayContainer1 must be dismissed") + .that(mSplitController.getAllNonFinishingOverlayContainers()) + .containsExactly(mOverlayContainer1, overlayContainer); + } + + @Test public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_dismissMultipleOverlays() { createExistingOverlayContainers(); @@ -275,15 +312,19 @@ public class OverlayPresentationTest { // different tag. OverlayContainer2 is dismissed since new container is launched with the // same tag in different task. assertWithMessage("overlayContainer1 and overlayContainer2 must be dismissed") - .that(mSplitController.getAllOverlayTaskFragmentContainers()) + .that(mSplitController.getAllNonFinishingOverlayContainers()) .containsExactly(overlayContainer); } private void createExistingOverlayContainers() { - mOverlayContainer1 = createTestOverlayContainer(TASK_ID, "test1"); - mOverlayContainer2 = createTestOverlayContainer(TASK_ID + 1, "test2"); + createExistingOverlayContainers(true /* visible */); + } + + private void createExistingOverlayContainers(boolean visible) { + mOverlayContainer1 = createTestOverlayContainer(TASK_ID, "test1", visible); + mOverlayContainer2 = createTestOverlayContainer(TASK_ID + 1, "test2", visible); List<TaskFragmentContainer> overlayContainers = mSplitController - .getAllOverlayTaskFragmentContainers(); + .getAllNonFinishingOverlayContainers(); assertThat(overlayContainers).containsExactly(mOverlayContainer1, mOverlayContainer2); } @@ -314,7 +355,7 @@ public class OverlayPresentationTest { createOrUpdateOverlayTaskFragmentIfNeeded("test"); setupTaskFragmentInfo(overlayContainer, mActivity, true /* isVisible */); - assertThat(mSplitController.getAllOverlayTaskFragmentContainers()) + assertThat(mSplitController.getAllNonFinishingOverlayContainers()) .containsExactly(overlayContainer); assertThat(overlayContainer.getTaskId()).isEqualTo(TASK_ID); assertThat(overlayContainer.areLastRequestedBoundsEqual(bounds)).isTrue(); @@ -322,14 +363,16 @@ public class OverlayPresentationTest { } @Test - public void testGetTopNonFishingTaskFragmentContainerWithOverlay() { - final TaskFragmentContainer overlayContainer = - createTestOverlayContainer(TASK_ID, "test1"); - - // Add a SplitPinContainer, the overlay should be on top + public void testGetTopNonFishingTaskFragmentContainerWithoutAssociatedOverlay() { final Activity primaryActivity = createMockActivity(); final Activity secondaryActivity = createMockActivity(); - + final Rect bounds = new Rect(0, 0, 100, 100); + mSplitController.setActivityStackAttributesCalculator(params -> + new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build()); + final TaskFragmentContainer overlayContainer = + createTestOverlayContainer(TASK_ID, "test1", true /* isVisible */, + false /* shouldAssociateWithActivity */); + overlayContainer.setIsolatedNavigationEnabled(true); final TaskFragmentContainer primaryContainer = createMockTaskFragmentContainer(primaryActivity); final TaskFragmentContainer secondaryContainer = @@ -371,10 +414,10 @@ public class OverlayPresentationTest { @Test public void testGetTopNonFinishingActivityWithOverlay() { - TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test1"); - final Activity activity = createMockActivity(); final TaskFragmentContainer container = createMockTaskFragmentContainer(activity); + final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, + "test1"); final TaskContainer task = container.getTaskContainer(); assertThat(task.getTopNonFinishingActivity(true /* includeOverlay */)) @@ -391,8 +434,9 @@ public class OverlayPresentationTest { } @Test - public void testUpdateOverlayContainer_dismissOverlayIfNeeded() { - TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test"); + public void testUpdateOverlayContainer_dismissNonAssociatedOverlayIfNeeded() { + TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test", + true /* isVisible */, false /* associatedLaunchingActivity */); mSplitController.updateOverlayContainer(mTransaction, overlayContainer); @@ -461,11 +505,10 @@ public class OverlayPresentationTest { @Test public void testOnTaskFragmentParentInfoChanged_positionOnlyChange_earlyReturn() { - final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test"); + final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test", + true /* isVisible */, false /* associatedLaunchingActivity */); final TaskContainer taskContainer = overlayContainer.getTaskContainer(); - assertThat(taskContainer.getOverlayContainer()).isEqualTo(overlayContainer); - spyOn(taskContainer); final TaskContainer.TaskProperties taskProperties = taskContainer.getTaskProperties(); final TaskFragmentParentInfo parentInfo = new TaskFragmentParentInfo( @@ -481,16 +524,15 @@ public class OverlayPresentationTest { assertWithMessage("The overlay container must still be dismissed even if " + "#updateContainer is not called") - .that(taskContainer.getOverlayContainer()).isNull(); + .that(taskContainer.getTaskFragmentContainers()).isEmpty(); } @Test - public void testOnTaskFragmentParentInfoChanged_invisibleTask_callDismissOverlayContainer() { - final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test"); + public void testOnTaskFragmentParentInfoChanged_invisibleTask_callDismissNonAssocOverlay() { + final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test", + true /* isVisible */, false /* associatedLaunchingActivity */); final TaskContainer taskContainer = overlayContainer.getTaskContainer(); - assertThat(taskContainer.getOverlayContainer()).isEqualTo(overlayContainer); - spyOn(taskContainer); final TaskContainer.TaskProperties taskProperties = taskContainer.getTaskProperties(); final TaskFragmentParentInfo parentInfo = new TaskFragmentParentInfo( @@ -505,7 +547,7 @@ public class OverlayPresentationTest { assertWithMessage("The overlay container must still be dismissed even if " + "#updateContainer is not called") - .that(taskContainer.getOverlayContainer()).isNull(); + .that(taskContainer.getTaskFragmentContainers()).isEmpty(); } @Test @@ -529,8 +571,7 @@ public class OverlayPresentationTest { @Test public void testApplyActivityStackAttributesForOverlayContainerAssociatedWithActivity() { - final TaskFragmentContainer container = createTestOverlayContainer(TASK_ID, - TEST_TAG, true /* associatedWithLaunchingActivity */); + final TaskFragmentContainer container = createTestOverlayContainer(TASK_ID, TEST_TAG); final IBinder token = container.getTaskFragmentToken(); final ActivityStackAttributes attributes = new ActivityStackAttributes.Builder() .setRelativeBounds(new Rect(0, 0, 200, 200)) @@ -555,7 +596,7 @@ public class OverlayPresentationTest { @Test public void testApplyActivityStackAttributesForOverlayContainerWithoutAssociatedActivity() { final TaskFragmentContainer container = createTestOverlayContainer(TASK_ID, TEST_TAG, - false /* associatedWithLaunchingActivity */); + true, /* isVisible */ false /* associatedWithLaunchingActivity */); final IBinder token = container.getTaskFragmentToken(); final ActivityStackAttributes attributes = new ActivityStackAttributes.Builder() .setRelativeBounds(new Rect(0, 0, 200, 200)) @@ -610,8 +651,7 @@ public class OverlayPresentationTest { mSplitPresenter.applyActivityStackAttributes(mTransaction, container, attributes, new Size(relativeBounds.width() + 1, relativeBounds.height())); - verify(mSplitPresenter).resizeTaskFragmentIfRegistered(mTransaction, container, - new Rect()); + verify(mSplitPresenter).resizeTaskFragmentIfRegistered(mTransaction, container, new Rect()); verify(mSplitPresenter).updateTaskFragmentWindowingModeIfRegistered(mTransaction, container, WINDOWING_MODE_UNDEFINED); verify(mSplitPresenter).updateAnimationParams(mTransaction, token, @@ -635,14 +675,14 @@ public class OverlayPresentationTest { mActivity.getActivityToken()); verify(mSplitPresenter, never()).cleanupContainer(any(), any(), anyBoolean()); - assertThat(mSplitController.getAllOverlayTaskFragmentContainers()) + assertThat(mSplitController.getAllNonFinishingOverlayContainers()) .contains(overlayWithoutAssociation); TaskFragmentContainer overlayWithAssociation = createOrUpdateOverlayTaskFragmentIfNeeded("test"); overlayWithAssociation.setInfo(mTransaction, createMockTaskFragmentInfo( overlayWithAssociation, mActivity, true /* isVisible */)); - assertThat(mSplitController.getAllOverlayTaskFragmentContainers()) + assertThat(mSplitController.getAllNonFinishingOverlayContainers()) .contains(overlayWithAssociation); clearInvocations(mSplitPresenter); @@ -655,7 +695,7 @@ public class OverlayPresentationTest { verify(mSplitPresenter).cleanupContainer(mTransaction, overlayWithAssociation, false); - assertThat(mSplitController.getAllOverlayTaskFragmentContainers()) + assertThat(mSplitController.getAllNonFinishingOverlayContainers()) .doesNotContain(overlayWithAssociation); } @@ -692,7 +732,14 @@ public class OverlayPresentationTest { @NonNull private TaskFragmentContainer createTestOverlayContainer(int taskId, @NonNull String tag) { - return createTestOverlayContainer(taskId, tag, + return createTestOverlayContainer(taskId, tag, false /* isVisible */, + true /* associateLaunchingActivity */); + } + + @NonNull + private TaskFragmentContainer createTestOverlayContainer(int taskId, @NonNull String tag, + boolean isVisible) { + return createTestOverlayContainer(taskId, tag, isVisible, true /* associateLaunchingActivity */); } @@ -700,13 +747,13 @@ public class OverlayPresentationTest { // once we have use cases. @NonNull private TaskFragmentContainer createTestOverlayContainer(int taskId, @NonNull String tag, - boolean associateLaunchingActivity) { + boolean isVisible, boolean associateLaunchingActivity) { Activity activity = createMockActivity(); TaskFragmentContainer overlayContainer = mSplitController.newContainer( null /* pendingAppearedActivity */, mIntent, activity, taskId, null /* pairedPrimaryContainer */, tag, Bundle.EMPTY, associateLaunchingActivity); - setupTaskFragmentInfo(overlayContainer, activity, false /* isVisible */); + setupTaskFragmentInfo(overlayContainer, activity, isVisible); return overlayContainer; } diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml index 36d3313a9f3b..7a986835359a 100644 --- a/libs/WindowManager/Shell/AndroidManifest.xml +++ b/libs/WindowManager/Shell/AndroidManifest.xml @@ -23,4 +23,12 @@ <uses-permission android:name="android.permission.ROTATE_SURFACE_FLINGER" /> <uses-permission android:name="android.permission.WAKEUP_SURFACE_FLINGER" /> <uses-permission android:name="android.permission.READ_FRAME_BUFFER" /> + + <application> + <activity + android:name=".desktopmode.DesktopWallpaperActivity" + android:excludeFromRecents="true" + android:launchMode="singleInstance" + android:theme="@style/DesktopWallpaperTheme" /> + </application> </manifest> diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml index 08c2a02acf55..13c0e6646002 100644 --- a/libs/WindowManager/Shell/res/values/styles.xml +++ b/libs/WindowManager/Shell/res/values/styles.xml @@ -23,6 +23,14 @@ <item name="android:windowAnimationStyle">@style/Animation.ForcedResizable</item> </style> + <!-- Theme used for the activity that shows below the desktop mode windows to show wallpaper --> + <style name="DesktopWallpaperTheme" parent="@android:style/Theme.Wallpaper.NoTitleBar"> + <item name="android:statusBarColor">@android:color/transparent</item> + <item name="android:navigationBarColor">@android:color/transparent</item> + <item name="android:windowDrawsSystemBarBackgrounds">true</item> + <item name="android:windowAnimationStyle">@null</item> + </style> + <style name="Animation.ForcedResizable" parent="@android:style/Animation"> <item name="android:activityOpenEnterAnimation">@anim/forced_resizable_enter</item> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index ff00a7b9ea59..1408eadf544e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -59,6 +59,7 @@ import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver; import com.android.wm.shell.desktopmode.DesktopModeStatus; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.desktopmode.DesktopTasksController; +import com.android.wm.shell.desktopmode.DesktopTasksTransitionObserver; import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler; import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler; import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler; @@ -569,6 +570,18 @@ public abstract class WMShellModule { @WMSingleton @Provides + static Optional<DesktopTasksTransitionObserver> provideDesktopTasksTransitionObserver( + Optional<DesktopModeTaskRepository> desktopModeTaskRepository, + Transitions transitions, + ShellInit shellInit + ) { + return desktopModeTaskRepository.flatMap(repository -> + Optional.of(new DesktopTasksTransitionObserver(repository, transitions, shellInit)) + ); + } + + @WMSingleton + @Provides static DesktopModeLoggerTransitionObserver provideDesktopModeLoggerTransitionObserver( ShellInit shellInit, Transitions transitions, @@ -623,7 +636,8 @@ public abstract class WMShellModule { @Provides static Object provideIndependentShellComponentsToCreate( DragAndDropController dragAndDropController, - DefaultMixedHandler defaultMixedHandler) { + DefaultMixedHandler defaultMixedHandler, + Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional) { return new Object(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt index e1e41ee1e64d..f1a475a42452 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt @@ -36,7 +36,14 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl true } } - + "moveToNextDisplay" -> { + if (!runMoveToNextDisplay(args, pw)) { + pw.println("Task not found. Please enter a valid taskId.") + false + } else { + true + } + } else -> { pw.println("Invalid command: ${args[0]}") false @@ -61,8 +68,28 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl return controller.moveToDesktop(taskId, WindowContainerTransaction()) } + private fun runMoveToNextDisplay(args: Array<String>, pw: PrintWriter): Boolean { + if (args.size < 2) { + // First argument is the action name. + pw.println("Error: task id should be provided as arguments") + return false + } + + val taskId = try { + args[1].toInt() + } catch (e: NumberFormatException) { + pw.println("Error: task id should be an integer") + return false + } + + controller.moveToNextDisplay(taskId) + return true + } + override fun printShellCommandHelp(pw: PrintWriter, prefix: String) { pw.println("$prefix moveToDesktop <taskId> ") pw.println("$prefix Move a task with given id to desktop mode.") + pw.println("$prefix moveToNextDisplay <taskId> ") + pw.println("$prefix Move a task with given id to next display.") } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt index 120d68120771..50cea01fa281 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt @@ -22,6 +22,7 @@ import android.util.ArrayMap import android.util.ArraySet import android.util.SparseArray import android.view.Display.INVALID_DISPLAY +import android.window.WindowContainerToken import androidx.core.util.forEach import androidx.core.util.keyIterator import androidx.core.util.valueIterator @@ -49,6 +50,8 @@ class DesktopModeTaskRepository { var stashed: Boolean = false ) + // Token of the current wallpaper activity, used to remove it when the last task is removed + var wallpaperActivityToken: WindowContainerToken? = null // Tasks currently in freeform mode, ordered from top to bottom (top is at index 0). private val freeformTasksInZOrder = mutableListOf<Int>() private val activeTasksListeners = ArraySet<ActiveTasksListener>() @@ -200,6 +203,15 @@ class DesktopModeTaskRepository { } /** + * Check if a task with the given [taskId] is the only active task on its display + */ + fun isOnlyActiveTask(taskId: Int): Boolean { + return displayData.valueIterator().asSequence().any { data -> + data.activeTasks.singleOrNull() == taskId + } + } + + /** * Get a set of the active tasks for given [displayId] */ fun getActiveTasks(displayId: Int): ArraySet<Int> { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index ec2e67007c4b..068661a6a666 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -40,6 +40,7 @@ import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CHANGE import android.view.WindowManager.TRANSIT_NONE import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_TO_BACK import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.RemoteTransition import android.window.TransitionInfo @@ -381,7 +382,6 @@ class DesktopTasksController( ) val wct = WindowContainerTransaction() exitSplitIfApplicable(wct, taskInfo) - moveHomeTaskToFront(wct) bringDesktopAppsToFront(taskInfo.displayId, wct) addMoveToDesktopChanges(wct, taskInfo) wct.setBounds(taskInfo.token, freeformBounds) @@ -401,6 +401,22 @@ class DesktopTasksController( shellTaskOrganizer.applyTransaction(wct) } + /** + * Perform clean up of the desktop wallpaper activity if the closed window task is + * the last active task. + * + * @param wct transaction to modify if the last active task is closed + * @param taskId task id of the window that's being closed + */ + fun onDesktopWindowClose( + wct: WindowContainerTransaction, + taskId: Int + ) { + if (desktopModeTaskRepository.isOnlyActiveTask(taskId)) { + removeWallpaperActivity(wct) + } + } + /** Move a task with given `taskId` to fullscreen */ fun moveToFullscreen(taskId: Int) { shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> @@ -676,9 +692,15 @@ class DesktopTasksController( KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: bringDesktopAppsToFront") val activeTasks = desktopModeTaskRepository.getActiveTasks(displayId) - // First move home to front and then other tasks on top of it - moveHomeTaskToFront(wct) + if (Flags.enableDesktopWindowingWallpaperActivity()) { + // Add translucent wallpaper activity to show the wallpaper underneath + addWallpaperActivity(wct) + } else { + // Move home to front + moveHomeTaskToFront(wct) + } + // Then move other tasks on top of it val allTasksInZOrder = desktopModeTaskRepository.getFreeformTasksInZOrder() activeTasks // Sort descending as the top task is at index 0. It should be ordered to top last @@ -694,6 +716,26 @@ class DesktopTasksController( ?.let { homeTask -> wct.reorder(homeTask.getToken(), true /* onTop */) } } + private fun addWallpaperActivity(wct: WindowContainerTransaction) { + KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: addWallpaper") + val intent = Intent(context, DesktopWallpaperActivity::class.java) + val options = ActivityOptions.makeBasic().apply { + isPendingIntentBackgroundActivityLaunchAllowedByPermission = true + pendingIntentBackgroundActivityStartMode = + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED + } + val pendingIntent = PendingIntent.getActivity(context, /* requestCode = */ 0, intent, + PendingIntent.FLAG_IMMUTABLE) + wct.sendPendingIntent(pendingIntent, intent, options.toBundle()) + } + + private fun removeWallpaperActivity(wct: WindowContainerTransaction) { + desktopModeTaskRepository.wallpaperActivityToken?.let { token -> + KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: removeWallpaper") + wct.removeTask(token) + } + } + fun releaseVisualIndicator() { val t = SurfaceControl.Transaction() visualIndicator?.releaseVisualIndicator(t) @@ -741,6 +783,9 @@ class DesktopTasksController( reason = "recents animation is running" false } + // Handle back navigation for the last window if wallpaper available + shouldRemoveWallpaper(request) -> + true // Only handle open or to front transitions request.type != TRANSIT_OPEN && request.type != TRANSIT_TO_FRONT -> { reason = "transition type not handled (${request.type})" @@ -777,6 +822,7 @@ class DesktopTasksController( val result = triggerTask?.let { task -> when { + request.type == TRANSIT_TO_BACK -> handleBackNavigation(task) // If display has tasks stashed, handle as stashed launch task.isStashed -> handleStashedTaskLaunch(task) // Check if the task has a top transparent activity @@ -824,6 +870,14 @@ class DesktopTasksController( return Flags.enableDesktopWindowingModalsPolicy() && isSingleTopActivityTranslucent(task) } + private fun shouldRemoveWallpaper(request: TransitionRequestInfo): Boolean { + return Flags.enableDesktopWindowingWallpaperActivity() && + request.type == TRANSIT_TO_BACK && + request.triggerTask?.let { task -> + desktopModeTaskRepository.isOnlyActiveTask(task.taskId) + } ?: false + } + private fun handleFreeformTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? { KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFreeformTaskLaunch") val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId) @@ -881,12 +935,26 @@ class DesktopTasksController( } } + /** Handle back navigation by removing wallpaper activity if it's the last active task */ + private fun handleBackNavigation(task: RunningTaskInfo): WindowContainerTransaction? { + if (desktopModeTaskRepository.isOnlyActiveTask(task.taskId) && + desktopModeTaskRepository.wallpaperActivityToken != null) { + // Remove wallpaper activity when the last active task is removed + return WindowContainerTransaction().also { wct -> + removeWallpaperActivity(wct) + } + } else { + return null + } + } + private fun addMoveToDesktopChanges( wct: WindowContainerTransaction, taskInfo: RunningTaskInfo ) { - val displayWindowingMode = taskInfo.configuration.windowConfiguration.displayWindowingMode - val targetWindowingMode = if (displayWindowingMode == WINDOWING_MODE_FREEFORM) { + val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!! + val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode + val targetWindowingMode = if (tdaWindowingMode == WINDOWING_MODE_FREEFORM) { // Display windowing is freeform, set to undefined and inherit it WINDOWING_MODE_UNDEFINED } else { @@ -903,8 +971,9 @@ class DesktopTasksController( wct: WindowContainerTransaction, taskInfo: RunningTaskInfo ) { - val displayWindowingMode = taskInfo.configuration.windowConfiguration.displayWindowingMode - val targetWindowingMode = if (displayWindowingMode == WINDOWING_MODE_FULLSCREEN) { + val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!! + val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode + val targetWindowingMode = if (tdaWindowingMode == WINDOWING_MODE_FULLSCREEN) { // Display windowing is fullscreen, set to undefined and inherit it WINDOWING_MODE_UNDEFINED } else { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt new file mode 100644 index 000000000000..20df26428649 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2024 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.desktopmode + +import android.os.IBinder +import android.view.SurfaceControl +import android.view.WindowManager +import android.window.TransitionInfo +import com.android.window.flags.Flags.enableDesktopWindowingWallpaperActivity +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.util.KtProtoLog + +/** + * A [Transitions.TransitionObserver] that observes shell transitions and updates + * the [DesktopModeTaskRepository] state TODO: b/332682201 + * This observes transitions related to desktop mode + * and other transitions that originate both within and outside shell. + */ +class DesktopTasksTransitionObserver( + private val desktopModeTaskRepository: DesktopModeTaskRepository, + private val transitions: Transitions, + shellInit: ShellInit +) : Transitions.TransitionObserver { + + init { + if (Transitions.ENABLE_SHELL_TRANSITIONS && DesktopModeStatus.isEnabled()) { + shellInit.addInitCallback(::onInit, this) + } + } + + fun onInit() { + KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "DesktopTasksTransitionObserver: onInit") + transitions.registerObserver(this) + } + + override fun onTransitionReady( + transition: IBinder, + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction + ) { + // TODO: b/332682201 Update repository state + updateWallpaperToken(info) + } + + override fun onTransitionStarting(transition: IBinder) { + // TODO: b/332682201 Update repository state + } + + override fun onTransitionMerged(merged: IBinder, playing: IBinder) { + // TODO: b/332682201 Update repository state + } + + override fun onTransitionFinished(transition: IBinder, aborted: Boolean) { + // TODO: b/332682201 Update repository state + } + + private fun updateWallpaperToken(info: TransitionInfo) { + if (!enableDesktopWindowingWallpaperActivity()) { + return + } + info.changes.forEach { change -> + change.taskInfo?.let { taskInfo -> + if (DesktopWallpaperActivity.isWallpaperTask(taskInfo)) { + when (change.mode) { + WindowManager.TRANSIT_OPEN -> + desktopModeTaskRepository.wallpaperActivityToken = taskInfo.token + WindowManager.TRANSIT_CLOSE -> + desktopModeTaskRepository.wallpaperActivityToken = null + else -> {} + } + } + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopWallpaperActivity.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopWallpaperActivity.kt new file mode 100644 index 000000000000..c4a4474689fa --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopWallpaperActivity.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 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.desktopmode + +import android.app.Activity +import android.app.ActivityManager +import android.content.ComponentName +import android.os.Bundle +import android.view.WindowManager +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.util.KtProtoLog + +/** + * A transparent activity used in the desktop mode to show the wallpaper under the freeform windows. + * This activity will be running in `FULLSCREEN` windowing mode, which ensures it hides Launcher. + * When entering desktop, we would ensure that it's added behind desktop apps and removed when + * leaving the desktop mode. + * + * Note! This activity should NOT interact directly with any other code in the Shell without calling + * onto the shell main thread. Activities are always started on the main thread. + */ +class DesktopWallpaperActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle?) { + KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "DesktopWallpaperActivity: onCreate") + super.onCreate(savedInstanceState) + window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) + } + + companion object { + private const val SYSTEM_UI_PACKAGE_NAME = "com.android.systemui" + private val wallpaperActivityComponent = + ComponentName(SYSTEM_UI_PACKAGE_NAME, DesktopWallpaperActivity::class.java.name) + + @JvmStatic + fun isWallpaperTask(taskInfo: ActivityManager.RunningTaskInfo) = + taskInfo.baseIntent.component?.let(::isWallpaperComponent) ?: false + + @JvmStatic + fun isWallpaperComponent(component: ComponentName) = + component == wallpaperActivityComponent + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java index c9185ae39114..b1a1e5999aa9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java @@ -20,6 +20,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.view.Display.DEFAULT_DISPLAY; import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; +import static com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP; import static com.android.wm.shell.transition.Transitions.TransitionObserver; import android.annotation.NonNull; @@ -60,7 +61,8 @@ public class HomeTransitionObserver implements TransitionObserver, @NonNull SurfaceControl.Transaction finishTransaction) { for (TransitionInfo.Change change : info.getChanges()) { final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); - if (taskInfo == null + if (info.getType() == TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP + || taskInfo == null || taskInfo.displayId != DEFAULT_DISPLAY || taskInfo.taskId == -1 || !taskInfo.isRunning) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index 32f271b0b896..87dc3915082f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -19,14 +19,19 @@ package com.android.wm.shell.windowdecor; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.content.pm.PackageManager.FEATURE_PC; +import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS; import static android.view.WindowManager.TRANSIT_CHANGE; import android.app.ActivityManager.RunningTaskInfo; +import android.content.ContentResolver; import android.content.Context; import android.graphics.Rect; import android.os.Handler; +import android.provider.Settings; import android.util.SparseArray; import android.view.Choreographer; +import android.view.Display; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.View; @@ -163,10 +168,33 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { } private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) { - return taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM - || (taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD - && taskInfo.configuration.windowConfiguration.getDisplayWindowingMode() - == WINDOWING_MODE_FREEFORM); + if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { + return true; + } + if (taskInfo.getActivityType() != ACTIVITY_TYPE_STANDARD) { + return false; + } + final DisplayAreaInfo rootDisplayAreaInfo = + mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId); + if (rootDisplayAreaInfo != null) { + return rootDisplayAreaInfo.configuration.windowConfiguration.getWindowingMode() + == WINDOWING_MODE_FREEFORM; + } + + // It is possible that the rootDisplayAreaInfo is null when a task appears soon enough after + // a new display shows up, because TDA may appear after task appears in WM shell. Instead of + // fixing the synchronization issues, let's use other signals to "guess" the answer. It is + // OK in this context because no other captions other than the legacy developer option + // freeform and Kingyo/CF PC may use this class. WM shell should have full control over the + // condition where captions should show up in all new cases such as desktop mode, for which + // we should use different window decor view models. Ultimately Kingyo/CF PC may need to + // spin up their own window decor view model when they start to care about multiple + // displays. + if (isPc()) { + return true; + } + return taskInfo.displayId != Display.DEFAULT_DISPLAY + && forcesDesktopModeOnExternalDisplays(); } private void createWindowDecoration( @@ -313,4 +341,17 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { return true; } } + + /** + * Returns if this device is a PC. + */ + private boolean isPc() { + return mContext.getPackageManager().hasSystemFeature(FEATURE_PC); + } + + private boolean forcesDesktopModeOnExternalDisplays() { + final ContentResolver resolver = mContext.getContentResolver(); + return Settings.Global.getInt(resolver, + DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, 0) != 0; + } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 7649a782818a..777ab9c17218 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -84,6 +84,7 @@ import com.android.wm.shell.desktopmode.DesktopModeStatus; import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition; +import com.android.wm.shell.desktopmode.DesktopWallpaperActivity; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.splitscreen.SplitScreen.StageType; @@ -407,7 +408,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mSplitScreenController.moveTaskToFullscreen(getOtherSplitTask(mTaskId).taskId, SplitScreenController.EXIT_REASON_DESKTOP_MODE); } else { - mTaskOperations.closeTask(mTaskToken); + WindowContainerTransaction wct = new WindowContainerTransaction(); + mDesktopTasksController.onDesktopWindowClose(wct, mTaskId); + mTaskOperations.closeTask(mTaskToken, wct); } } else if (id == R.id.back_button) { mTaskOperations.injectBackKey(); @@ -1005,6 +1008,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { return false; } return DesktopModeStatus.isEnabled() + && !DesktopWallpaperActivity.isWallpaperTask(taskInfo) && taskInfo.getWindowingMode() != WINDOWING_MODE_PINNED && taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD && !taskInfo.configuration.windowConfiguration.isAlwaysOnTop() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java index 1763e4f9533c..53d4e2701849 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java @@ -72,7 +72,10 @@ class TaskOperations { } void closeTask(WindowContainerToken taskToken) { - WindowContainerTransaction wct = new WindowContainerTransaction(); + closeTask(taskToken, new WindowContainerTransaction()); + } + + void closeTask(WindowContainerToken taskToken, WindowContainerTransaction wct) { wct.removeTask(taskToken); if (Transitions.ENABLE_SHELL_TRANSITIONS) { mTransitionStarter.startRemoveTransition(wct); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java index 3672ae386dc4..24f4d92af9d7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TestRunningTaskInfoBuilder.java @@ -23,8 +23,10 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import android.annotation.NonNull; import android.app.ActivityManager; import android.app.WindowConfiguration; +import android.content.Intent; import android.graphics.Point; import android.graphics.Rect; import android.os.IBinder; @@ -38,6 +40,7 @@ public final class TestRunningTaskInfoBuilder { private WindowContainerToken mToken = createMockWCToken(); private int mParentTaskId = INVALID_TASK_ID; + private Intent mBaseIntent = new Intent(); private @WindowConfiguration.ActivityType int mActivityType = ACTIVITY_TYPE_STANDARD; private @WindowConfiguration.WindowingMode int mWindowingMode = WINDOWING_MODE_UNDEFINED; private int mDisplayId = Display.DEFAULT_DISPLAY; @@ -68,6 +71,15 @@ public final class TestRunningTaskInfoBuilder { return this; } + /** + * Set {@link ActivityManager.RunningTaskInfo#baseIntent} for the task info, by default + * an empty intent is assigned + */ + public TestRunningTaskInfoBuilder setBaseIntent(@NonNull Intent intent) { + mBaseIntent = intent; + return this; + } + public TestRunningTaskInfoBuilder setActivityType( @WindowConfiguration.ActivityType int activityType) { mActivityType = activityType; @@ -109,6 +121,7 @@ public final class TestRunningTaskInfoBuilder { public ActivityManager.RunningTaskInfo build() { final ActivityManager.RunningTaskInfo info = new ActivityManager.RunningTaskInfo(); info.taskId = sNextTaskId++; + info.baseIntent = mBaseIntent; info.parentTaskId = mParentTaskId; info.displayId = mDisplayId; info.configuration.windowConfiguration.setBounds(mBounds); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt index 0c45d52d5320..b2b54acf4585 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt @@ -119,6 +119,57 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } @Test + fun isOnlyActiveTask_noActiveTasks() { + // Not an active task + assertThat(repo.isOnlyActiveTask(1)).isFalse() + } + + @Test + fun isOnlyActiveTask_singleActiveTask() { + repo.addActiveTask(DEFAULT_DISPLAY, 1) + // The only active task + assertThat(repo.isActiveTask(1)).isTrue() + assertThat(repo.isOnlyActiveTask(1)).isTrue() + // Not an active task + assertThat(repo.isActiveTask(99)).isFalse() + assertThat(repo.isOnlyActiveTask(99)).isFalse() + } + + @Test + fun isOnlyActiveTask_multipleActiveTasks() { + repo.addActiveTask(DEFAULT_DISPLAY, 1) + repo.addActiveTask(DEFAULT_DISPLAY, 2) + // Not the only task + assertThat(repo.isActiveTask(1)).isTrue() + assertThat(repo.isOnlyActiveTask(1)).isFalse() + // Not the only task + assertThat(repo.isActiveTask(2)).isTrue() + assertThat(repo.isOnlyActiveTask(2)).isFalse() + // Not an active task + assertThat(repo.isActiveTask(99)).isFalse() + assertThat(repo.isOnlyActiveTask(99)).isFalse() + } + + @Test + fun isOnlyActiveTask_multipleDisplays() { + repo.addActiveTask(DEFAULT_DISPLAY, 1) + repo.addActiveTask(DEFAULT_DISPLAY, 2) + repo.addActiveTask(SECOND_DISPLAY, 3) + // Not the only task on DEFAULT_DISPLAY + assertThat(repo.isActiveTask(1)).isTrue() + assertThat(repo.isOnlyActiveTask(1)).isFalse() + // Not the only task on DEFAULT_DISPLAY + assertThat(repo.isActiveTask(2)).isTrue() + assertThat(repo.isOnlyActiveTask(2)).isFalse() + // The only active task on SECOND_DISPLAY + assertThat(repo.isActiveTask(3)).isTrue() + assertThat(repo.isOnlyActiveTask(3)).isTrue() + // Not an active task + assertThat(repo.isActiveTask(99)).isFalse() + assertThat(repo.isOnlyActiveTask(99)).isFalse() + } + + @Test fun addListener_notifiesVisibleFreeformTask() { repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true) val listener = TestVisibilityListener() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 93a967e9bfc9..64f604119a8b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -23,10 +23,13 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED +import android.content.Intent import android.graphics.Point import android.graphics.PointF import android.graphics.Rect import android.os.Binder +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY @@ -34,11 +37,15 @@ import android.view.SurfaceControl import android.view.WindowManager import android.view.WindowManager.TRANSIT_CHANGE import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_TO_BACK import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.DisplayAreaInfo import android.window.RemoteTransition import android.window.TransitionRequestInfo +import android.window.WindowContainerToken import android.window.WindowContainerTransaction +import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_PENDING_INTENT +import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_TASK import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn @@ -161,6 +168,10 @@ class DesktopTasksControllerTest : ShellTestCase() { (i.arguments.first() as Rect).set(STABLE_BOUNDS) } + val tda = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda) + controller = createController() controller.setSplitScreenController(splitScreenController) @@ -219,7 +230,8 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun showDesktopApps_allAppsInvisible_bringsToFront() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperDisabled() { val homeTask = setUpHomeTask() val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() @@ -238,7 +250,27 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun showDesktopApps_appsAlreadyVisible_bringsToFront() { + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperEnabled() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskHidden(task1) + markTaskHidden(task2) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: wallpaper intent, task1, task2 + wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperDisabled() { val homeTask = setUpHomeTask() val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() @@ -257,7 +289,27 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun showDesktopApps_someAppsInvisible_reordersAll() { + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperEnabled() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskVisible(task1) + markTaskVisible(task2) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: wallpaper intent, task1, task2 + wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperDisabled() { val homeTask = setUpHomeTask() val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() @@ -276,7 +328,27 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun showDesktopApps_noActiveTasks_reorderHomeToTop() { + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperEnabled() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskHidden(task1) + markTaskVisible(task2) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: wallpaper intent, task1, task2 + wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_noActiveTasks_reorderHomeToTop_desktopWallpaperDisabled() { val homeTask = setUpHomeTask() controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) @@ -288,7 +360,18 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay() { + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_noActiveTasks_addDesktopWallpaper_desktopWallpaperEnabled() { + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperDisabled() { val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY) setUpHomeTask(SECOND_DISPLAY) @@ -307,6 +390,25 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperEnabled() { + val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY) + setUpHomeTask(SECOND_DISPLAY) + val taskSecondDisplay = setUpFreeformTask(SECOND_DISPLAY) + markTaskHidden(taskDefaultDisplay) + markTaskHidden(taskSecondDisplay) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(2) + // Expect order to be from bottom: wallpaper intent, task + wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + wct.assertReorderAt(index = 1, taskDefaultDisplay) + } + + @Test fun getVisibleTaskCount_noTasks_returnsZero() { assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) } @@ -336,9 +438,10 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun moveToDesktop_displayFullscreen_windowingModeSetToFreeform() { + fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() { val task = setUpFullscreenTask() - task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FULLSCREEN + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN controller.moveToDesktop(task) val wct = getLatestMoveToDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) @@ -346,9 +449,10 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun moveToDesktop_displayFreeform_windowingModeSetToUndefined() { + fun moveToDesktop_tdaFreeform_windowingModeSetToUndefined() { val task = setUpFullscreenTask() - task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FREEFORM + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM controller.moveToDesktop(task) val wct = getLatestMoveToDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) @@ -413,7 +517,8 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun moveToDesktop_otherFreeformTasksBroughtToFront() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun moveToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperDisabled() { val homeTask = setUpHomeTask() val freeformTask = setUpFreeformTask() val fullscreenTask = setUpFullscreenTask() @@ -431,6 +536,26 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun moveToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperEnabled() { + val freeformTask = setUpFreeformTask() + val fullscreenTask = setUpFullscreenTask() + markTaskHidden(freeformTask) + + controller.moveToDesktop(fullscreenTask) + + with(getLatestMoveToDesktopWct()) { + // Operations should include wallpaper intent, freeform task, fullscreen task + assertThat(hierarchyOps).hasSize(3) + assertPendingIntentAt(index = 0, desktopWallpaperIntent) + assertReorderAt(index = 1, freeformTask) + assertReorderAt(index = 2, fullscreenTask) + assertThat(changes[fullscreenTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + } + + @Test fun moveToDesktop_onlyFreeformTasksFromCurrentDisplayBroughtToFront() { setUpHomeTask(displayId = DEFAULT_DISPLAY) val freeformTaskDefault = setUpFreeformTask(displayId = DEFAULT_DISPLAY) @@ -481,9 +606,10 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun moveToFullscreen_displayFullscreen_windowingModeSetToUndefined() { + fun moveToFullscreen_tdaFullscreen_windowingModeSetToUndefined() { val task = setUpFreeformTask() - task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FULLSCREEN + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN controller.moveToFullscreen(task.taskId) val wct = getLatestExitDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) @@ -491,9 +617,10 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun moveToFullscreen_displayFreeform_windowingModeSetToFullscreen() { + fun moveToFullscreen_tdaFreeform_windowingModeSetToFullscreen() { val task = setUpFreeformTask() - task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FREEFORM + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM controller.moveToFullscreen(task.taskId) val wct = getLatestExitDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) @@ -595,6 +722,48 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun onDesktopWindowClose_noActiveTasks() { + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, 1 /* taskId */) + // Doesn't modify transaction + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test + fun onDesktopWindowClose_singleActiveTask_noWallpaperActivityToken() { + val task = setUpFreeformTask() + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, task.taskId) + // Doesn't modify transaction + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test + fun onDesktopWindowClose_singleActiveTask_hasWallpaperActivityToken() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, task.taskId) + // Adds remove wallpaper operation + wct.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + fun onDesktopWindowClose_multipleActiveTasks() { + val task1 = setUpFreeformTask() + setUpFreeformTask() + val wallpaperToken = MockToken().token() + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, task1.taskId) + // Doesn't modify transaction + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test fun handleRequest_fullscreenTask_freeformVisible_returnSwitchToFreeformWCT() { assumeTrue(ENABLE_SHELL_TRANSITIONS) @@ -638,6 +807,7 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_fullscreenTask_desktopStashed_returnWCTWithAllAppsBroughtToFront() { assumeTrue(ENABLE_SHELL_TRANSITIONS) whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true) @@ -684,7 +854,7 @@ class DesktopTasksControllerTest : ShellTestCase() { createTransition(freeformTask2, type = TRANSIT_TO_FRONT) ) assertThat(result?.changes?.get(freeformTask2.token.asBinder())?.windowingMode) - .isEqualTo(WINDOWING_MODE_FULLSCREEN) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN } @Test @@ -694,7 +864,7 @@ class DesktopTasksControllerTest : ShellTestCase() { val task = createFreeformTask() val result = controller.handleRequest(Binder(), createTransition(task)) assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) - .isEqualTo(WINDOWING_MODE_FULLSCREEN) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN } @Test @@ -706,10 +876,11 @@ class DesktopTasksControllerTest : ShellTestCase() { val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay)) assertThat(result?.changes?.get(taskDefaultDisplay.token.asBinder())?.windowingMode) - .isEqualTo(WINDOWING_MODE_FULLSCREEN) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN } @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_desktopStashed_returnWCTWithAllAppsBroughtToFront() { assumeTrue(ENABLE_SHELL_TRANSITIONS) whenever(DesktopModeStatus.isStashingEnabled()).thenReturn(true) @@ -792,7 +963,56 @@ class DesktopTasksControllerTest : ShellTestCase() { val result = controller.handleRequest(Binder(), createTransition(task)) assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) - .isEqualTo(WINDOWING_MODE_FULLSCREEN) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_singleActiveTask_noToken() { + val task = setUpFreeformTask() + val result = + controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) + // Doesn't handle request + assertThat(result).isNull() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_singleActiveTask_hasToken_desktopWallpaperDisabled() { + desktopModeTaskRepository.wallpaperActivityToken = MockToken().token() + + val task = setUpFreeformTask() + val result = + controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) + // Doesn't handle request + assertThat(result).isNull() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_singleActiveTask_hasToken_desktopWallpaperEnabled() { + val wallpaperToken = MockToken().token() + desktopModeTaskRepository.wallpaperActivityToken = wallpaperToken + + val task = setUpFreeformTask() + val result = + controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) + assertThat(result).isNotNull() + // Creates remove wallpaper transaction + result!!.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_multipleActiveTasks() { + desktopModeTaskRepository.wallpaperActivityToken = MockToken().token() + + val task1 = setUpFreeformTask() + setUpFreeformTask() + val result = + controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) + // Doesn't handle request + assertThat(result).isNull() } @Test @@ -895,7 +1115,7 @@ class DesktopTasksControllerTest : ShellTestCase() { val wct = getLatestExitDesktopWct() assertThat(wct.changes[task2.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FULLSCREEN) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN } @Test @@ -998,6 +1218,9 @@ class DesktopTasksControllerTest : ShellTestCase() { assertThat(desktopModeTaskRepository.removeBoundsBeforeMaximize(task.taskId)).isNull() } + private val desktopWallpaperIntent: Intent + get() = Intent(context, DesktopWallpaperActivity::class.java) + private fun setUpFreeformTask( displayId: Int = DEFAULT_DISPLAY, bounds: Rect? = null @@ -1123,10 +1346,14 @@ class DesktopTasksControllerTest : ShellTestCase() { } } -private fun WindowContainerTransaction.assertReorderAt(index: Int, task: RunningTaskInfo) { +private fun WindowContainerTransaction.assertIndexInBounds(index: Int) { assertWithMessage("WCT does not have a hierarchy operation at index $index") .that(hierarchyOps.size) .isGreaterThan(index) +} + +private fun WindowContainerTransaction.assertReorderAt(index: Int, task: RunningTaskInfo) { + assertIndexInBounds(index) val op = hierarchyOps[index] assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REORDER) assertThat(op.container).isEqualTo(task.token.asBinder()) @@ -1137,3 +1364,17 @@ private fun WindowContainerTransaction.assertReorderSequence(vararg tasks: Runni assertReorderAt(i, tasks[i]) } } + +private fun WindowContainerTransaction.assertRemoveAt(index: Int, token: WindowContainerToken) { + assertIndexInBounds(index) + val op = hierarchyOps[index] + assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK) + assertThat(op.container).isEqualTo(token.asBinder()) +} + +private fun WindowContainerTransaction.assertPendingIntentAt(index: Int, intent: Intent) { + assertIndexInBounds(index) + val op = hierarchyOps[index] + assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_PENDING_INTENT) + assertThat(op.pendingIntent?.intent?.component).isEqualTo(intent.component) +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java index e7d37addb368..0db10ef65a74 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java @@ -24,6 +24,7 @@ import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; +import static com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.mock; @@ -167,6 +168,25 @@ public class HomeTransitionObserverTest extends ShellTestCase { } @Test + public void testStartDragToDesktopDoesNotTriggerCallback() throws RemoteException { + TransitionInfo info = mock(TransitionInfo.class); + TransitionInfo.Change change = mock(TransitionInfo.Change.class); + ActivityManager.RunningTaskInfo taskInfo = mock(ActivityManager.RunningTaskInfo.class); + when(change.getTaskInfo()).thenReturn(taskInfo); + when(info.getChanges()).thenReturn(new ArrayList<>(List.of(change))); + when(info.getType()).thenReturn(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP); + + setupTransitionInfo(taskInfo, change, ACTIVITY_TYPE_HOME, TRANSIT_OPEN, true); + + mHomeTransitionObserver.onTransitionReady(mock(IBinder.class), + info, + mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class)); + + verify(mListener, times(0)).onHomeVisibilityChanged(anyBoolean()); + } + + @Test public void testHomeActivityWithBackGestureNotifiesHomeIsVisible() throws RemoteException { TransitionInfo info = mock(TransitionInfo.class); TransitionInfo.Change change = mock(TransitionInfo.Change.class); diff --git a/libs/androidfw/ZipFileRO.cpp b/libs/androidfw/ZipFileRO.cpp index 34a6bc27b93f..839c7b6fef37 100644 --- a/libs/androidfw/ZipFileRO.cpp +++ b/libs/androidfw/ZipFileRO.cpp @@ -119,30 +119,41 @@ ZipEntryRO ZipFileRO::findEntryByName(const char* entryName) const * appear to be bogus. */ bool ZipFileRO::getEntryInfo(ZipEntryRO entry, uint16_t* pMethod, + uint32_t* pUncompLen, uint32_t* pCompLen, off64_t* pOffset, + uint32_t* pModWhen, uint32_t* pCrc32) const +{ + return getEntryInfo(entry, pMethod, pUncompLen, pCompLen, pOffset, pModWhen, + pCrc32, nullptr); +} + +bool ZipFileRO::getEntryInfo(ZipEntryRO entry, uint16_t* pMethod, uint32_t* pUncompLen, uint32_t* pCompLen, off64_t* pOffset, - uint32_t* pModWhen, uint32_t* pCrc32) const + uint32_t* pModWhen, uint32_t* pCrc32, uint16_t* pExtraFieldSize) const { const _ZipEntryRO* zipEntry = reinterpret_cast<_ZipEntryRO*>(entry); const ZipEntry& ze = zipEntry->entry; - if (pMethod != NULL) { + if (pMethod != nullptr) { *pMethod = ze.method; } - if (pUncompLen != NULL) { + if (pUncompLen != nullptr) { *pUncompLen = ze.uncompressed_length; } - if (pCompLen != NULL) { + if (pCompLen != nullptr) { *pCompLen = ze.compressed_length; } - if (pOffset != NULL) { + if (pOffset != nullptr) { *pOffset = ze.offset; } - if (pModWhen != NULL) { + if (pModWhen != nullptr) { *pModWhen = ze.mod_time; } - if (pCrc32 != NULL) { + if (pCrc32 != nullptr) { *pCrc32 = ze.crc32; } + if (pExtraFieldSize != nullptr) { + *pExtraFieldSize = ze.extra_field_size; + } return true; } diff --git a/libs/androidfw/include/androidfw/ZipFileRO.h b/libs/androidfw/include/androidfw/ZipFileRO.h index 031d2e8fd48f..f7c5007c80d2 100644 --- a/libs/androidfw/include/androidfw/ZipFileRO.h +++ b/libs/androidfw/include/androidfw/ZipFileRO.h @@ -151,6 +151,10 @@ public: uint32_t* pCompLen, off64_t* pOffset, uint32_t* pModWhen, uint32_t* pCrc32) const; + bool getEntryInfo(ZipEntryRO entry, uint16_t* pMethod, + uint32_t* pUncompLen, uint32_t* pCompLen, off64_t* pOffset, + uint32_t* pModWhen, uint32_t* pCrc32, uint16_t* pExtraFieldSize) const; + /* * Create a new FileMap object that maps a subset of the archive. For * an uncompressed entry this effectively provides a pointer to the diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp index 34932b1b1e25..dc669a5eca73 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp @@ -24,7 +24,6 @@ #include <SkImageAndroid.h> #include <SkImageInfo.h> #include <SkMatrix.h> -#include <SkMultiPictureDocument.h> #include <SkOverdrawCanvas.h> #include <SkOverdrawColorFilter.h> #include <SkPicture.h> @@ -38,6 +37,7 @@ #include <android-base/properties.h> #include <gui/TraceUtils.h> #include <include/android/SkSurfaceAndroid.h> +#include <include/docs/SkMultiPictureDocument.h> #include <include/encode/SkPngEncoder.h> #include <include/gpu/ganesh/SkSurfaceGanesh.h> #include <unistd.h> @@ -185,7 +185,7 @@ bool SkiaPipeline::setupMultiFrameCapture() { // we need to keep it until after mMultiPic.close() // procs is passed as a pointer, but just as a method of having an optional default. // procs doesn't need to outlive this Make call. - mMultiPic = SkMakeMultiPictureDocument(mOpenMultiPicStream.get(), &procs, + mMultiPic = SkMultiPictureDocument::Make(mOpenMultiPicStream.get(), &procs, [sharingCtx = mSerialContext.get()](const SkPicture* pic) { SkSharingSerialContext::collectNonTextureImagesFromPicture(pic, sharingCtx); }); diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.h b/libs/hwui/pipeline/skia/SkiaPipeline.h index cf14b1f9ebe3..823b209017a5 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.h +++ b/libs/hwui/pipeline/skia/SkiaPipeline.h @@ -18,7 +18,6 @@ #include <SkColorSpace.h> #include <SkDocument.h> -#include <SkMultiPictureDocument.h> #include <SkSurface.h> #include "Lighting.h" diff --git a/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp b/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp index b62711f50c94..21fe6ff14f56 100644 --- a/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp +++ b/libs/hwui/pipeline/skia/VkFunctorDrawable.cpp @@ -16,10 +16,10 @@ #include "VkFunctorDrawable.h" -#include <GrBackendDrawableInfo.h> #include <SkAndroidFrameworkUtils.h> #include <SkImage.h> #include <SkM44.h> +#include <include/gpu/ganesh/vk/GrBackendDrawableInfo.h> #include <gui/TraceUtils.h> #include <private/hwui/DrawVkInfo.h> #include <utils/Color.h> |