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

Commit a126bc9d authored by Charles Chen's avatar Charles Chen
Browse files

Always dismiss visible top associated overlay

Before, we launch the new activity to the existing overlay if its
associated activity and tag are exactly the same as the existing one.
Now we always dismiss the existing one because an overlay container
can only contain an activity.

Test: atest OverlayPresentationTest
Fixes: 343307330
Flag: EXEMPT bugfix

Change-Id: Id4d42eeb7077e48a18331cdbd3b063f089955e2d
parent 0454d096
Loading
Loading
Loading
Loading
+64 −80
Original line number Diff line number Diff line
@@ -119,7 +119,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen

    // TODO(b/243518738): Move to WM Extensions if we have requirement of overlay without
    //  association. It's not set in WM Extensions nor Wm Jetpack library currently.
    private static final String KEY_OVERLAY_ASSOCIATE_WITH_LAUNCHING_ACTIVITY =
    @VisibleForTesting
    static final String KEY_OVERLAY_ASSOCIATE_WITH_LAUNCHING_ACTIVITY =
            "androidx.window.extensions.embedding.shouldAssociateWithLaunchingActivity";

    @VisibleForTesting
@@ -2742,89 +2743,72 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
        }

        final int taskId = getTaskId(launchActivity);
        if (!overlayContainers.isEmpty()) {
        // Overlay container policy:
        // 1. Overlay tag must be unique per process.
        //   a. For associated overlay, if a new launched overlay container has the same tag as
        //      an existing one, the existing overlay will be dismissed regardless of its task
        //      and window hierarchy.
        //   b. For always-on-top overlay, if there's an overlay container has the same tag in the
        //      launched task, the overlay container will be re-used, which means the
        //      ActivityStackAttributes will be applied and the launched activity will be positioned
        //      on top of the overlay container.
        // 2. There must be at most one overlay that partially occludes a visible activity per task.
        //   a. For associated overlay, only the top visible overlay container in the launched task
        //      will be dismissed.
        //   b. Always-on-top overlay is always visible. If there's an overlay with different tags
        //      in the same task, the overlay will be dismissed in case an activity above
        //      the overlay is dismissed and the overlay is shown unexpectedly.
        for (final TaskFragmentContainer overlayContainer : overlayContainers) {
            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.
                    if (overlayTag.equals(overlayContainer.getOverlayTag())) {
            final boolean areInSameTask = taskId == overlayContainer.getTaskId();
            final boolean haveSameTag = overlayTag.equals(overlayContainer.getOverlayTag());
            if (!associateLaunchingActivity && overlayContainer.isAlwaysOnTopOverlay()
                    && haveSameTag && areInSameTask) {
                // Just launch the activity and update the existing always-on-top overlay
                // if the requested overlay is an always-on-top overlay with the same tag
                // as the existing one.
                mPresenter.applyActivityStackAttributes(wct, overlayContainer, attrs,
                        getMinDimensions(intent));
                return overlayContainer;
            }
            if (haveSameTag) {
                // For other tag match, we should clean up the existing overlay since the overlay
                // tag must be unique per process.
                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);
                        + overlayContainer.getOverlayTag() + " is dismissed with "
                        + " the launching activity=" + launchActivity
                        + " because there's an existing overlay container with the same tag.");
                mPresenter.cleanupContainer(wct, overlayContainer,
                        false /* shouldFinishDependant */);
            }
            if (!areInSameTask) {
                // Early return here because we won't clean-up or update overlay from different
                // tasks except tag collision.
                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 (associateLaunchingActivity) {
                // For associated overlay, we only dismiss the overlay if it's the top non-finishing
                // child of its parent container.
                if (isTopNonFinishingOverlay) {
                    Log.w(TAG, "The on-top overlay container with tag:"
                            + overlayContainer.getOverlayTag() + " is dismissed with "
                            + " the launching activity=" + launchActivity
                            + "because we only allow one overlay on top.");
                    mPresenter.cleanupContainer(wct, overlayContainer,
                            false /* shouldFinishDependant */);
                }
                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.isOverlayWithActivityAssociation()) {
                        // 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.");
                        mPresenter.cleanupContainer(wct, overlayContainer,
                                false /* shouldFinishDependant */);
                        continue;
                    } else {
                        // 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));
                        return overlayContainer;
                    }
                }
                if (launchActivity.getActivityToken()
                        != overlayContainer.getAssociatedActivityToken()) {
            // Otherwise, we should clean up the overlay in the task because we only allow one
            // overlay when an always-on-top overlay is launched.
            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.
                    + overlayContainer.getOverlayTag() + " is dismissed with "
                    + " the launching activity=" + launchActivity
                    + "because an always-on-top overlay is launched.");
            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.
        return createEmptyContainer(wct, intent, taskId, attrs, launchActivity, overlayTag,
+50 −23
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSpli
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPlaceholderRuleBuilder;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createTfContainer;
import static androidx.window.extensions.embedding.SplitController.KEY_OVERLAY_ASSOCIATE_WITH_LAUNCHING_ACTIVITY;
import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_BOTTOM;
import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_LEFT;
import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_RIGHT;
@@ -94,6 +95,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

@@ -267,7 +269,7 @@ public class OverlayPresentationTest {
    }

    @Test
    public void testCreateOrUpdateOverlay_visibleOverlaySameTagInTask_dismissOverlay() {
    public void testCreateOrUpdateOverlay_visibleOverlayInTask_dismissOverlay() {
        createExistingOverlayContainers();

        final TaskFragmentContainer overlayContainer =
@@ -294,26 +296,6 @@ public class OverlayPresentationTest {
                .containsExactly(mOverlayContainer2, overlayContainer);
    }

    @Test
    public void testCreateOrUpdateOverlay_sameTagTaskAndActivity_updateOverlay() {
        createExistingOverlayContainers();

        final Rect bounds = new Rect(0, 0, 100, 100);
        mSplitController.setActivityStackAttributesCalculator(params ->
                new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build());
        final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded(
                mOverlayContainer1.getOverlayTag());

        assertWithMessage("overlayContainer1 must be updated since the new overlay container"
                + " is launched with the same tag and task")
                .that(mSplitController.getAllNonFinishingOverlayContainers())
                .containsExactly(mOverlayContainer1, mOverlayContainer2);

        assertThat(overlayContainer).isEqualTo(mOverlayContainer1);
        verify(mSplitPresenter).resizeTaskFragment(eq(mTransaction),
                eq(mOverlayContainer1.getTaskFragmentToken()), eq(bounds));
    }

    @Test
    public void testCreateOrUpdateOverlay_sameTagAndTaskButNotActivity_dismissOverlay() {
        createExistingOverlayContainers();
@@ -361,6 +343,43 @@ public class OverlayPresentationTest {
                .containsExactly(overlayContainer);
    }

    @Test
    public void testCreateOrUpdateAlwaysOnTopOverlay_dismissMultipleOverlaysInTask() {
        createExistingOverlayContainers();
        // Create another overlay in task.
        final TaskFragmentContainer overlayContainer3 =
                createTestOverlayContainer(TASK_ID, "test3");
        assertThat(mSplitController.getAllNonFinishingOverlayContainers())
                .containsExactly(mOverlayContainer1, mOverlayContainer2, overlayContainer3);

        final TaskFragmentContainer overlayContainer =
                createOrUpdateAlwaysOnTopOverlay("test4");

        assertWithMessage("overlayContainer1 and overlayContainer3 must be dismissed")
                .that(mSplitController.getAllNonFinishingOverlayContainers())
                .containsExactly(mOverlayContainer2, overlayContainer);
    }

    @Test
    public void testCreateOrUpdateAlwaysOnTopOverlay_updateOverlay() {
        createExistingOverlayContainers();
        // Create another overlay in task.
        final TaskFragmentContainer alwaysOnTopOverlay = createTestOverlayContainer(TASK_ID,
                "test3", true /* isVisible */, false /* associateLaunchingActivity */);
        final ActivityStackAttributes attrs = new ActivityStackAttributes.Builder()
                .setRelativeBounds(new Rect(0, 0, 100, 100)).build();
        mSplitController.setActivityStackAttributesCalculator(params -> attrs);

        Mockito.clearInvocations(mSplitPresenter);
        final TaskFragmentContainer overlayContainer =
                createOrUpdateAlwaysOnTopOverlay(alwaysOnTopOverlay.getOverlayTag());

        assertWithMessage("overlayContainer1 and overlayContainer3 must be dismissed")
                .that(mSplitController.getAllNonFinishingOverlayContainers())
                .containsExactly(mOverlayContainer2, alwaysOnTopOverlay);
        assertThat(overlayContainer).isEqualTo(alwaysOnTopOverlay);
    }

    @Test
    public void testCreateOrUpdateOverlay_launchFromSplit_returnNull() {
        final Activity primaryActivity = createMockActivity();
@@ -966,6 +985,16 @@ public class OverlayPresentationTest {
                launchOptions, mIntent, activity);
    }

    @Nullable
    private TaskFragmentContainer createOrUpdateAlwaysOnTopOverlay(
            @NonNull String tag) {
        final Bundle launchOptions = new Bundle();
        launchOptions.putBoolean(KEY_OVERLAY_ASSOCIATE_WITH_LAUNCHING_ACTIVITY, false);
        launchOptions.putString(KEY_OVERLAY_TAG, tag);
        return mSplitController.createOrUpdateOverlayTaskFragmentIfNeeded(mTransaction,
                launchOptions, mIntent, createMockActivity());
    }

    /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */
    @NonNull
    private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) {
@@ -1002,8 +1031,6 @@ public class OverlayPresentationTest {
                null /* launchingActivity */);
    }

    // TODO(b/243518738): add more test coverage on overlay container without activity association
    //  once we have use cases.
    @NonNull
    private TaskFragmentContainer createTestOverlayContainer(int taskId, @NonNull String tag,
            boolean isVisible, boolean associateLaunchingActivity,