Loading libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +39 −34 Original line number Diff line number Diff line Loading @@ -1520,12 +1520,16 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") TaskFragmentContainer resolveStartActivityIntent(@NonNull WindowContainerTransaction wct, int taskId, @NonNull Intent intent, @Nullable Activity launchingActivity) { // Skip resolving if started from an isolated navigated TaskFragmentContainer. if (launchingActivity != null) { final TaskFragmentContainer taskFragmentContainer = getContainerWithActivity( launchingActivity); if (taskFragmentContainer != null && taskFragmentContainer.isIsolatedNavigationEnabled()) { // Skip resolving if started from an isolated navigated TaskFragmentContainer. return null; } if (isAssociatedWithOverlay(launchingActivity)) { // Skip resolving if the launching activity associated with an overlay. return null; } } Loading Loading @@ -1659,9 +1663,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // is the first embedded TF in the task. final TaskContainer taskContainer = container.getTaskContainer(); // TODO(b/265271880): remove redundant logic after all TF operations take fragmentToken. final Rect taskBounds = taskContainer.getBounds(); final Rect sanitizedBounds = sanitizeBounds(activityStackAttributes.getRelativeBounds(), getMinDimensions(intent), taskBounds); getMinDimensions(intent), container); final int windowingMode = taskContainer .getWindowingModeForTaskFragment(sanitizedBounds); mPresenter.createTaskFragment(wct, taskFragmentToken, activityInTask.getActivityToken(), Loading Loading @@ -1722,17 +1725,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") @Nullable TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) { // Check pending appeared activity first because there can be a delay for the server // update. TaskFragmentContainer taskFragmentContainer = getContainer(container -> container.hasPendingAppearedActivity(activityToken)); if (taskFragmentContainer != null) { return taskFragmentContainer; for (int i = mTaskContainers.size() - 1; i >= 0; --i) { final TaskFragmentContainer container = mTaskContainers.valueAt(i) .getContainerWithActivity(activityToken); if (container != null) { return container; } // Check appeared activity if there is no such pending appeared activity. return getContainer(container -> container.hasAppearedActivity(activityToken)); } return null; } @GuardedBy("mLock") Loading Loading @@ -2096,19 +2096,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (container == null) { return null; } final List<SplitContainer> splitContainers = container.getTaskContainer().getSplitContainers(); if (splitContainers.isEmpty()) { return null; } for (int i = splitContainers.size() - 1; i >= 0; i--) { final SplitContainer splitContainer = splitContainers.get(i); if (container.equals(splitContainer.getSecondaryContainer()) || container.equals(splitContainer.getPrimaryContainer())) { return splitContainer; } } return null; return container.getTaskContainer().getActiveSplitForContainer(container); } /** Loading Loading @@ -2158,6 +2146,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return false; } if (isAssociatedWithOverlay(activity)) { // Can't launch the placeholder if the activity associates an overlay. return false; } final TaskFragmentContainer container = getContainerWithActivity(activity); if (container != null && !allowLaunchPlaceholder(container)) { // We don't allow activity in this TaskFragment to launch placeholder. Loading Loading @@ -2197,6 +2190,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen */ @GuardedBy("mLock") private boolean allowLaunchPlaceholder(@NonNull TaskFragmentContainer container) { if (container.isOverlay()) { // Don't launch placeholder if the container is an overlay. return false; } final TaskFragmentContainer topContainer = container.getTaskContainer() .getTopNonFinishingTaskFragmentContainer(); if (container != topContainer) { Loading Loading @@ -2470,15 +2468,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") TaskFragmentContainer getContainer(@NonNull Predicate<TaskFragmentContainer> predicate) { for (int i = mTaskContainers.size() - 1; i >= 0; i--) { final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i) .getTaskFragmentContainers(); for (int j = containers.size() - 1; j >= 0; j--) { final TaskFragmentContainer container = containers.get(j); if (predicate.test(container)) { final TaskFragmentContainer container = mTaskContainers.valueAt(i) .getContainer(predicate); if (container != null) { return container; } } } return null; } Loading Loading @@ -2641,6 +2636,16 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return overlayContainers; } @GuardedBy("mLock") private boolean isAssociatedWithOverlay(@NonNull Activity activity) { final TaskContainer taskContainer = getTaskContainer(getTaskId(activity)); if (taskContainer == null) { return false; } return taskContainer.getContainer(c -> c.isOverlay() && !c.isFinished() && c.getAssociatedActivityToken() == activity.getActivityToken()) != null; } /** * Creates an overlay container or updates a visible overlay container if its * {@link TaskFragmentContainer#getTaskId()}, {@link TaskFragmentContainer#getOverlayTag()} Loading Loading @@ -2734,7 +2739,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } // Requesting an always-on-top overlay. if (!associateLaunchingActivity) { if (overlayContainer.isAssociatedWithActivity()) { 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" Loading libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +26 −5 Original line number Diff line number Diff line Loading @@ -591,16 +591,14 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { @NonNull TaskFragmentContainer container, @NonNull ActivityStackAttributes attributes, @Nullable Size minDimensions) { final Rect taskBounds = container.getTaskContainer().getBounds(); final Rect relativeBounds = sanitizeBounds(attributes.getRelativeBounds(), minDimensions, taskBounds); container); final boolean isFillParent = relativeBounds.isEmpty(); // Note that we only set isolated navigation for overlay container without activity // association. Activity will be launched to an expanded container on top of the overlay // if the overlay is associated with an activity. Thus, an overlay with activity association // will never be isolated navigated. final boolean isIsolatedNavigated = container.isOverlay() && !container.isAssociatedWithActivity() && !isFillParent; final boolean isIsolatedNavigated = container.isAlwaysOnTopOverlay() && !isFillParent; final boolean dimOnTask = !isFillParent && attributes.getWindowAttributes().getDimAreaBehavior() == DIM_AREA_ON_TASK && Flags.fullscreenDimFlag(); Loading @@ -624,7 +622,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { */ @NonNull static Rect sanitizeBounds(@NonNull Rect bounds, @Nullable Size minDimension, @NonNull Rect taskBounds) { @NonNull TaskFragmentContainer container) { if (bounds.isEmpty()) { // Don't need to check if the bounds follows the task bounds. return bounds; Loading @@ -633,10 +631,33 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { // Expand the bounds if the bounds are smaller than minimum dimensions. return new Rect(); } final TaskContainer taskContainer = container.getTaskContainer(); final Rect taskBounds = taskContainer.getBounds(); if (!taskBounds.contains(bounds)) { // Expand the bounds if the bounds exceed the task bounds. return new Rect(); } if (!container.isOverlay()) { // Stop here if the container is not an overlay. return bounds; } final IBinder associatedActivityToken = container.getAssociatedActivityToken(); if (associatedActivityToken == null) { // Stop here if the container is an always-on-top overlay. return bounds; } // Expand the overlay with activity association if the associated activity is part of a // split, or we may need to handle three change transition together. final TaskFragmentContainer associatedContainer = taskContainer .getContainerWithActivity(associatedActivityToken); if (taskContainer.getActiveSplitForContainer(associatedContainer) != null) { return new Rect(); } return bounds; } Loading libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +33 −0 Original line number Diff line number Diff line Loading @@ -43,6 +43,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.window.extensions.core.util.function.Predicate; import androidx.window.extensions.embedding.SplitAttributes.SplitType; import androidx.window.extensions.embedding.SplitAttributes.SplitType.ExpandContainersSplitType; import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType; Loading Loading @@ -318,6 +319,38 @@ class TaskContainer { return null; } @Nullable TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) { return getContainer(container -> container.hasAppearedActivity(activityToken) || container.hasPendingAppearedActivity(activityToken)); } @Nullable TaskFragmentContainer getContainer(@NonNull Predicate<TaskFragmentContainer> predicate) { for (int i = mContainers.size() - 1; i >= 0; i--) { final TaskFragmentContainer container = mContainers.get(i); if (predicate.test(container)) { return container; } } return null; } @Nullable SplitContainer getActiveSplitForContainer(@Nullable TaskFragmentContainer container) { if (container == null) { return null; } for (int i = mSplitContainers.size() - 1; i >= 0; i--) { final SplitContainer splitContainer = mSplitContainers.get(i); if (container.equals(splitContainer.getSecondaryContainer()) || container.equals(splitContainer.getPrimaryContainer())) { return splitContainer; } } return null; } /** * Returns the always-on-top overlay container in the task, or {@code null} if it doesn't exist. */ Loading libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +8 −8 Original line number Diff line number Diff line Loading @@ -465,7 +465,7 @@ class TaskFragmentContainer { return; } // Early return if this container is not an overlay with activity association. if (!isOverlay() || !isAssociatedWithActivity()) { if (!isOverlayWithActivityAssociation()) { return; } if (mAssociatedActivityToken == activityToken) { Loading Loading @@ -500,7 +500,7 @@ class TaskFragmentContainer { // sure the controller considers this container as the one containing the activity. // This is needed when the activity is added as pending appeared activity to one // TaskFragment while it is also an appeared activity in another. return mController.getContainerWithActivity(activityToken) == this; return mTaskContainer.getContainerWithActivity(activityToken) == this; } /** Whether this activity has appeared in the TaskFragment on the server side. */ Loading Loading @@ -1019,16 +1019,16 @@ class TaskFragmentContainer { return mAssociatedActivityToken; } boolean isAssociatedWithActivity() { 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(); return isOverlay() && mAssociatedActivityToken == null; } boolean isOverlayWithActivityAssociation() { return isOverlay() && mAssociatedActivityToken != null; } @Override Loading @@ -1050,7 +1050,7 @@ class TaskFragmentContainer { + " runningActivityCount=" + getRunningActivityCount() + " isFinished=" + mIsFinished + " overlayTag=" + mOverlayTag + " associatedActivity" + mAssociatedActivityToken + " associatedActivityToken=" + mAssociatedActivityToken + " lastRequestedBounds=" + mLastRequestedBounds + " pendingAppearedActivities=" + mPendingAppearedActivities + (includeContainersToFinishOnExit ? " containersToFinishOnExit=" Loading libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java +132 −11 Original line number Diff line number Diff line Loading @@ -21,11 +21,15 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; import static androidx.window.extensions.embedding.ActivityEmbeddingOptionsProperties.KEY_OVERLAY_TAG; import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_ATTRIBUTES; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TEST_TAG; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPairRuleBuilder; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPlaceholderRuleBuilder; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule; import static androidx.window.extensions.embedding.SplitPresenter.sanitizeBounds; import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; Loading Loading @@ -85,6 +89,7 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** Loading @@ -102,6 +107,9 @@ public class OverlayPresentationTest { @Rule public MockitoRule rule = MockitoJUnit.rule(); private static final Intent PLACEHOLDER_INTENT = new Intent().setComponent( new ComponentName("test", "placeholder")); @Rule public final SetFlagsRule mSetFlagRule = new SetFlagsRule(); Loading Loading @@ -259,8 +267,7 @@ public class OverlayPresentationTest { mSplitController.setActivityStackAttributesCalculator(params -> new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build()); final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded( mOverlayContainer1.getOverlayTag(), mOverlayContainer1.getTopNonFinishingActivity()); mOverlayContainer1.getOverlayTag()); assertWithMessage("overlayContainer1 must be updated since the new overlay container" + " is launched with the same tag and task") Loading @@ -280,12 +287,13 @@ public class OverlayPresentationTest { mSplitController.setActivityStackAttributesCalculator(params -> new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build()); final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded( mOverlayContainer1.getOverlayTag(), mActivity); mOverlayContainer1.getOverlayTag(), createMockActivity()); assertWithMessage("overlayContainer1 must be dismissed since the new overlay container" + " is associated with different launching activity") .that(mSplitController.getAllNonFinishingOverlayContainers()) .containsExactly(mOverlayContainer2, overlayContainer); assertThat(overlayContainer).isNotEqualTo(mOverlayContainer1); } @Test Loading Loading @@ -323,7 +331,8 @@ public class OverlayPresentationTest { } private void createExistingOverlayContainers(boolean visible) { mOverlayContainer1 = createTestOverlayContainer(TASK_ID, "test1", visible); mOverlayContainer1 = createTestOverlayContainer(TASK_ID, "test1", visible, true /* associatedLaunchingActivity */, mActivity); mOverlayContainer2 = createTestOverlayContainer(TASK_ID + 1, "test2", visible); List<TaskFragmentContainer> overlayContainers = mSplitController .getAllNonFinishingOverlayContainers(); Loading @@ -335,17 +344,49 @@ public class OverlayPresentationTest { mIntent.setComponent(new ComponentName(ApplicationProvider.getApplicationContext(), MinimumDimensionActivity.class)); final Rect bounds = new Rect(0, 0, 100, 100); final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test1"); SplitPresenter.sanitizeBounds(bounds, SplitPresenter.getMinDimensions(mIntent), TASK_BOUNDS); assertThat(sanitizeBounds(bounds, SplitPresenter.getMinDimensions(mIntent), overlayContainer).isEmpty()).isTrue(); } @Test public void testSanitizeBounds_notInTaskBounds_expandOverlay() { final Rect bounds = new Rect(TASK_BOUNDS); bounds.offset(10, 10); final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test1"); assertThat(sanitizeBounds(bounds, null, overlayContainer) .isEmpty()).isTrue(); } @Test public void testSanitizeBounds_visibleSplit_expandOverlay() { // Launch a visible split final Activity primaryActivity = createMockActivity(); final Activity secondaryActivity = createMockActivity(); final TaskFragmentContainer primaryContainer = createMockTaskFragmentContainer(primaryActivity, true /* isVisible */); final TaskFragmentContainer secondaryContainer = createMockTaskFragmentContainer(secondaryActivity, true /* isVisible */); final SplitPairRule splitPairRule = createSplitPairRuleBuilder( activityActivityPair -> true /* activityPairPredicate */, activityIntentPair -> true /* activityIntentPairPredicate */, parentWindowMetrics -> true /* parentWindowMetricsPredicate */) .build(); mSplitController.registerSplit(mTransaction, primaryContainer, primaryActivity, secondaryContainer, splitPairRule, splitPairRule.getDefaultSplitAttributes()); final Rect bounds = new Rect(0, 0, 100, 100); final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test1", true /* isVisible */, true /* associatedLaunchingActivity */, secondaryActivity); SplitPresenter.sanitizeBounds(bounds, null, TASK_BOUNDS); assertThat(sanitizeBounds(bounds, null, overlayContainer) .isEmpty()).isTrue(); } @Test Loading Loading @@ -701,6 +742,70 @@ public class OverlayPresentationTest { .doesNotContain(overlayWithAssociation); } @Test public void testLaunchPlaceholderIfNecessary_skipIfActivityAssociateOverlay() { setupPlaceholderRule(mActivity); createTestOverlayContainer(TASK_ID, "test", true /* isVisible */, true /* associateLaunchingActivity */, mActivity); mSplitController.mTransactionManager.startNewTransaction(); assertThat(mSplitController.launchPlaceholderIfNecessary(mTransaction, mActivity, false /* isOnCreated */)).isFalse(); verify(mTransaction, never()).startActivityInTaskFragment(any(), any(), any(), any()); } @Test public void testLaunchPlaceholderIfNecessary_skipIfActivityInOverlay() { setupPlaceholderRule(mActivity); createOrUpdateOverlayTaskFragmentIfNeeded("test1", mActivity); mSplitController.mTransactionManager.startNewTransaction(); assertThat(mSplitController.launchPlaceholderIfNecessary(mTransaction, mActivity, false /* isOnCreated */)).isFalse(); verify(mTransaction, never()).startActivityInTaskFragment(any(), any(), any(), any()); } /** Setups a rule to launch placeholder for the given activity. */ private void setupPlaceholderRule(@NonNull Activity primaryActivity) { final SplitRule placeholderRule = createSplitPlaceholderRuleBuilder(PLACEHOLDER_INTENT, primaryActivity::equals, i -> false, w -> true) .setDefaultSplitAttributes(SPLIT_ATTRIBUTES) .build(); mSplitController.setEmbeddingRules(Collections.singleton(placeholderRule)); } @Test public void testResolveStartActivityIntent_skipIfAssociateOverlay() { final Intent intent = new Intent(); mSplitController.setEmbeddingRules(Collections.singleton( createSplitRule(mActivity, intent))); createTestOverlayContainer(TASK_ID, "test", true /* isVisible */, true /* associateLaunchingActivity */, mActivity); final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( mTransaction, TASK_ID, intent, mActivity); assertThat(container).isNull(); verify(mSplitController, never()).resolveStartActivityIntentByRule(any(), anyInt(), any(), any()); } @Test public void testResolveStartActivityIntent_skipIfLaunchingActivityInOverlay() { final Intent intent = new Intent(); mSplitController.setEmbeddingRules(Collections.singleton( createSplitRule(mActivity, intent))); createOrUpdateOverlayTaskFragmentIfNeeded("test1", mActivity); final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( mTransaction, TASK_ID, intent, mActivity); assertThat(container).isNull(); verify(mSplitController, never()).resolveStartActivityIntentByRule(any(), anyInt(), any(), any()); } /** * A simplified version of {@link SplitController#createOrUpdateOverlayTaskFragmentIfNeeded} */ Loading @@ -726,9 +831,16 @@ public class OverlayPresentationTest { /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */ @NonNull private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) { return createMockTaskFragmentContainer(activity, false /* isVisible */); } /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */ @NonNull private TaskFragmentContainer createMockTaskFragmentContainer( @NonNull Activity activity, boolean isVisible) { final TaskFragmentContainer container = mSplitController.newContainer(activity, activity.getTaskId()); setupTaskFragmentInfo(container, activity, false /* isVisible */); setupTaskFragmentInfo(container, activity, isVisible); return container; } Loading @@ -745,17 +857,26 @@ public class OverlayPresentationTest { true /* associateLaunchingActivity */); } @NonNull private TaskFragmentContainer createTestOverlayContainer(int taskId, @NonNull String tag, boolean isVisible, boolean associateLaunchingActivity) { return createTestOverlayContainer(taskId, tag, isVisible, associateLaunchingActivity, 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) { Activity activity = createMockActivity(); boolean isVisible, boolean associateLaunchingActivity, @Nullable Activity launchingActivity) { final Activity activity = launchingActivity != null ? launchingActivity : createMockActivity(); TaskFragmentContainer overlayContainer = mSplitController.newContainer( null /* pendingAppearedActivity */, mIntent, activity, taskId, null /* pairedPrimaryContainer */, tag, Bundle.EMPTY, associateLaunchingActivity); setupTaskFragmentInfo(overlayContainer, activity, isVisible); setupTaskFragmentInfo(overlayContainer, createMockActivity(), isVisible); return overlayContainer; } Loading Loading
libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +39 −34 Original line number Diff line number Diff line Loading @@ -1520,12 +1520,16 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") TaskFragmentContainer resolveStartActivityIntent(@NonNull WindowContainerTransaction wct, int taskId, @NonNull Intent intent, @Nullable Activity launchingActivity) { // Skip resolving if started from an isolated navigated TaskFragmentContainer. if (launchingActivity != null) { final TaskFragmentContainer taskFragmentContainer = getContainerWithActivity( launchingActivity); if (taskFragmentContainer != null && taskFragmentContainer.isIsolatedNavigationEnabled()) { // Skip resolving if started from an isolated navigated TaskFragmentContainer. return null; } if (isAssociatedWithOverlay(launchingActivity)) { // Skip resolving if the launching activity associated with an overlay. return null; } } Loading Loading @@ -1659,9 +1663,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // is the first embedded TF in the task. final TaskContainer taskContainer = container.getTaskContainer(); // TODO(b/265271880): remove redundant logic after all TF operations take fragmentToken. final Rect taskBounds = taskContainer.getBounds(); final Rect sanitizedBounds = sanitizeBounds(activityStackAttributes.getRelativeBounds(), getMinDimensions(intent), taskBounds); getMinDimensions(intent), container); final int windowingMode = taskContainer .getWindowingModeForTaskFragment(sanitizedBounds); mPresenter.createTaskFragment(wct, taskFragmentToken, activityInTask.getActivityToken(), Loading Loading @@ -1722,17 +1725,14 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") @Nullable TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) { // Check pending appeared activity first because there can be a delay for the server // update. TaskFragmentContainer taskFragmentContainer = getContainer(container -> container.hasPendingAppearedActivity(activityToken)); if (taskFragmentContainer != null) { return taskFragmentContainer; for (int i = mTaskContainers.size() - 1; i >= 0; --i) { final TaskFragmentContainer container = mTaskContainers.valueAt(i) .getContainerWithActivity(activityToken); if (container != null) { return container; } // Check appeared activity if there is no such pending appeared activity. return getContainer(container -> container.hasAppearedActivity(activityToken)); } return null; } @GuardedBy("mLock") Loading Loading @@ -2096,19 +2096,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (container == null) { return null; } final List<SplitContainer> splitContainers = container.getTaskContainer().getSplitContainers(); if (splitContainers.isEmpty()) { return null; } for (int i = splitContainers.size() - 1; i >= 0; i--) { final SplitContainer splitContainer = splitContainers.get(i); if (container.equals(splitContainer.getSecondaryContainer()) || container.equals(splitContainer.getPrimaryContainer())) { return splitContainer; } } return null; return container.getTaskContainer().getActiveSplitForContainer(container); } /** Loading Loading @@ -2158,6 +2146,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return false; } if (isAssociatedWithOverlay(activity)) { // Can't launch the placeholder if the activity associates an overlay. return false; } final TaskFragmentContainer container = getContainerWithActivity(activity); if (container != null && !allowLaunchPlaceholder(container)) { // We don't allow activity in this TaskFragment to launch placeholder. Loading Loading @@ -2197,6 +2190,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen */ @GuardedBy("mLock") private boolean allowLaunchPlaceholder(@NonNull TaskFragmentContainer container) { if (container.isOverlay()) { // Don't launch placeholder if the container is an overlay. return false; } final TaskFragmentContainer topContainer = container.getTaskContainer() .getTopNonFinishingTaskFragmentContainer(); if (container != topContainer) { Loading Loading @@ -2470,15 +2468,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") TaskFragmentContainer getContainer(@NonNull Predicate<TaskFragmentContainer> predicate) { for (int i = mTaskContainers.size() - 1; i >= 0; i--) { final List<TaskFragmentContainer> containers = mTaskContainers.valueAt(i) .getTaskFragmentContainers(); for (int j = containers.size() - 1; j >= 0; j--) { final TaskFragmentContainer container = containers.get(j); if (predicate.test(container)) { final TaskFragmentContainer container = mTaskContainers.valueAt(i) .getContainer(predicate); if (container != null) { return container; } } } return null; } Loading Loading @@ -2641,6 +2636,16 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return overlayContainers; } @GuardedBy("mLock") private boolean isAssociatedWithOverlay(@NonNull Activity activity) { final TaskContainer taskContainer = getTaskContainer(getTaskId(activity)); if (taskContainer == null) { return false; } return taskContainer.getContainer(c -> c.isOverlay() && !c.isFinished() && c.getAssociatedActivityToken() == activity.getActivityToken()) != null; } /** * Creates an overlay container or updates a visible overlay container if its * {@link TaskFragmentContainer#getTaskId()}, {@link TaskFragmentContainer#getOverlayTag()} Loading Loading @@ -2734,7 +2739,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } // Requesting an always-on-top overlay. if (!associateLaunchingActivity) { if (overlayContainer.isAssociatedWithActivity()) { 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" Loading
libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +26 −5 Original line number Diff line number Diff line Loading @@ -591,16 +591,14 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { @NonNull TaskFragmentContainer container, @NonNull ActivityStackAttributes attributes, @Nullable Size minDimensions) { final Rect taskBounds = container.getTaskContainer().getBounds(); final Rect relativeBounds = sanitizeBounds(attributes.getRelativeBounds(), minDimensions, taskBounds); container); final boolean isFillParent = relativeBounds.isEmpty(); // Note that we only set isolated navigation for overlay container without activity // association. Activity will be launched to an expanded container on top of the overlay // if the overlay is associated with an activity. Thus, an overlay with activity association // will never be isolated navigated. final boolean isIsolatedNavigated = container.isOverlay() && !container.isAssociatedWithActivity() && !isFillParent; final boolean isIsolatedNavigated = container.isAlwaysOnTopOverlay() && !isFillParent; final boolean dimOnTask = !isFillParent && attributes.getWindowAttributes().getDimAreaBehavior() == DIM_AREA_ON_TASK && Flags.fullscreenDimFlag(); Loading @@ -624,7 +622,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { */ @NonNull static Rect sanitizeBounds(@NonNull Rect bounds, @Nullable Size minDimension, @NonNull Rect taskBounds) { @NonNull TaskFragmentContainer container) { if (bounds.isEmpty()) { // Don't need to check if the bounds follows the task bounds. return bounds; Loading @@ -633,10 +631,33 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { // Expand the bounds if the bounds are smaller than minimum dimensions. return new Rect(); } final TaskContainer taskContainer = container.getTaskContainer(); final Rect taskBounds = taskContainer.getBounds(); if (!taskBounds.contains(bounds)) { // Expand the bounds if the bounds exceed the task bounds. return new Rect(); } if (!container.isOverlay()) { // Stop here if the container is not an overlay. return bounds; } final IBinder associatedActivityToken = container.getAssociatedActivityToken(); if (associatedActivityToken == null) { // Stop here if the container is an always-on-top overlay. return bounds; } // Expand the overlay with activity association if the associated activity is part of a // split, or we may need to handle three change transition together. final TaskFragmentContainer associatedContainer = taskContainer .getContainerWithActivity(associatedActivityToken); if (taskContainer.getActiveSplitForContainer(associatedContainer) != null) { return new Rect(); } return bounds; } Loading
libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +33 −0 Original line number Diff line number Diff line Loading @@ -43,6 +43,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.window.extensions.core.util.function.Predicate; import androidx.window.extensions.embedding.SplitAttributes.SplitType; import androidx.window.extensions.embedding.SplitAttributes.SplitType.ExpandContainersSplitType; import androidx.window.extensions.embedding.SplitAttributes.SplitType.RatioSplitType; Loading Loading @@ -318,6 +319,38 @@ class TaskContainer { return null; } @Nullable TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) { return getContainer(container -> container.hasAppearedActivity(activityToken) || container.hasPendingAppearedActivity(activityToken)); } @Nullable TaskFragmentContainer getContainer(@NonNull Predicate<TaskFragmentContainer> predicate) { for (int i = mContainers.size() - 1; i >= 0; i--) { final TaskFragmentContainer container = mContainers.get(i); if (predicate.test(container)) { return container; } } return null; } @Nullable SplitContainer getActiveSplitForContainer(@Nullable TaskFragmentContainer container) { if (container == null) { return null; } for (int i = mSplitContainers.size() - 1; i >= 0; i--) { final SplitContainer splitContainer = mSplitContainers.get(i); if (container.equals(splitContainer.getSecondaryContainer()) || container.equals(splitContainer.getPrimaryContainer())) { return splitContainer; } } return null; } /** * Returns the always-on-top overlay container in the task, or {@code null} if it doesn't exist. */ Loading
libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +8 −8 Original line number Diff line number Diff line Loading @@ -465,7 +465,7 @@ class TaskFragmentContainer { return; } // Early return if this container is not an overlay with activity association. if (!isOverlay() || !isAssociatedWithActivity()) { if (!isOverlayWithActivityAssociation()) { return; } if (mAssociatedActivityToken == activityToken) { Loading Loading @@ -500,7 +500,7 @@ class TaskFragmentContainer { // sure the controller considers this container as the one containing the activity. // This is needed when the activity is added as pending appeared activity to one // TaskFragment while it is also an appeared activity in another. return mController.getContainerWithActivity(activityToken) == this; return mTaskContainer.getContainerWithActivity(activityToken) == this; } /** Whether this activity has appeared in the TaskFragment on the server side. */ Loading Loading @@ -1019,16 +1019,16 @@ class TaskFragmentContainer { return mAssociatedActivityToken; } boolean isAssociatedWithActivity() { 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(); return isOverlay() && mAssociatedActivityToken == null; } boolean isOverlayWithActivityAssociation() { return isOverlay() && mAssociatedActivityToken != null; } @Override Loading @@ -1050,7 +1050,7 @@ class TaskFragmentContainer { + " runningActivityCount=" + getRunningActivityCount() + " isFinished=" + mIsFinished + " overlayTag=" + mOverlayTag + " associatedActivity" + mAssociatedActivityToken + " associatedActivityToken=" + mAssociatedActivityToken + " lastRequestedBounds=" + mLastRequestedBounds + " pendingAppearedActivities=" + mPendingAppearedActivities + (includeContainersToFinishOnExit ? " containersToFinishOnExit=" Loading
libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java +132 −11 Original line number Diff line number Diff line Loading @@ -21,11 +21,15 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; import static androidx.window.extensions.embedding.ActivityEmbeddingOptionsProperties.KEY_OVERLAY_TAG; import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_ATTRIBUTES; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TEST_TAG; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPairRuleBuilder; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPlaceholderRuleBuilder; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule; import static androidx.window.extensions.embedding.SplitPresenter.sanitizeBounds; import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; Loading Loading @@ -85,6 +89,7 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** Loading @@ -102,6 +107,9 @@ public class OverlayPresentationTest { @Rule public MockitoRule rule = MockitoJUnit.rule(); private static final Intent PLACEHOLDER_INTENT = new Intent().setComponent( new ComponentName("test", "placeholder")); @Rule public final SetFlagsRule mSetFlagRule = new SetFlagsRule(); Loading Loading @@ -259,8 +267,7 @@ public class OverlayPresentationTest { mSplitController.setActivityStackAttributesCalculator(params -> new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build()); final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded( mOverlayContainer1.getOverlayTag(), mOverlayContainer1.getTopNonFinishingActivity()); mOverlayContainer1.getOverlayTag()); assertWithMessage("overlayContainer1 must be updated since the new overlay container" + " is launched with the same tag and task") Loading @@ -280,12 +287,13 @@ public class OverlayPresentationTest { mSplitController.setActivityStackAttributesCalculator(params -> new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build()); final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded( mOverlayContainer1.getOverlayTag(), mActivity); mOverlayContainer1.getOverlayTag(), createMockActivity()); assertWithMessage("overlayContainer1 must be dismissed since the new overlay container" + " is associated with different launching activity") .that(mSplitController.getAllNonFinishingOverlayContainers()) .containsExactly(mOverlayContainer2, overlayContainer); assertThat(overlayContainer).isNotEqualTo(mOverlayContainer1); } @Test Loading Loading @@ -323,7 +331,8 @@ public class OverlayPresentationTest { } private void createExistingOverlayContainers(boolean visible) { mOverlayContainer1 = createTestOverlayContainer(TASK_ID, "test1", visible); mOverlayContainer1 = createTestOverlayContainer(TASK_ID, "test1", visible, true /* associatedLaunchingActivity */, mActivity); mOverlayContainer2 = createTestOverlayContainer(TASK_ID + 1, "test2", visible); List<TaskFragmentContainer> overlayContainers = mSplitController .getAllNonFinishingOverlayContainers(); Loading @@ -335,17 +344,49 @@ public class OverlayPresentationTest { mIntent.setComponent(new ComponentName(ApplicationProvider.getApplicationContext(), MinimumDimensionActivity.class)); final Rect bounds = new Rect(0, 0, 100, 100); final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test1"); SplitPresenter.sanitizeBounds(bounds, SplitPresenter.getMinDimensions(mIntent), TASK_BOUNDS); assertThat(sanitizeBounds(bounds, SplitPresenter.getMinDimensions(mIntent), overlayContainer).isEmpty()).isTrue(); } @Test public void testSanitizeBounds_notInTaskBounds_expandOverlay() { final Rect bounds = new Rect(TASK_BOUNDS); bounds.offset(10, 10); final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test1"); assertThat(sanitizeBounds(bounds, null, overlayContainer) .isEmpty()).isTrue(); } @Test public void testSanitizeBounds_visibleSplit_expandOverlay() { // Launch a visible split final Activity primaryActivity = createMockActivity(); final Activity secondaryActivity = createMockActivity(); final TaskFragmentContainer primaryContainer = createMockTaskFragmentContainer(primaryActivity, true /* isVisible */); final TaskFragmentContainer secondaryContainer = createMockTaskFragmentContainer(secondaryActivity, true /* isVisible */); final SplitPairRule splitPairRule = createSplitPairRuleBuilder( activityActivityPair -> true /* activityPairPredicate */, activityIntentPair -> true /* activityIntentPairPredicate */, parentWindowMetrics -> true /* parentWindowMetricsPredicate */) .build(); mSplitController.registerSplit(mTransaction, primaryContainer, primaryActivity, secondaryContainer, splitPairRule, splitPairRule.getDefaultSplitAttributes()); final Rect bounds = new Rect(0, 0, 100, 100); final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test1", true /* isVisible */, true /* associatedLaunchingActivity */, secondaryActivity); SplitPresenter.sanitizeBounds(bounds, null, TASK_BOUNDS); assertThat(sanitizeBounds(bounds, null, overlayContainer) .isEmpty()).isTrue(); } @Test Loading Loading @@ -701,6 +742,70 @@ public class OverlayPresentationTest { .doesNotContain(overlayWithAssociation); } @Test public void testLaunchPlaceholderIfNecessary_skipIfActivityAssociateOverlay() { setupPlaceholderRule(mActivity); createTestOverlayContainer(TASK_ID, "test", true /* isVisible */, true /* associateLaunchingActivity */, mActivity); mSplitController.mTransactionManager.startNewTransaction(); assertThat(mSplitController.launchPlaceholderIfNecessary(mTransaction, mActivity, false /* isOnCreated */)).isFalse(); verify(mTransaction, never()).startActivityInTaskFragment(any(), any(), any(), any()); } @Test public void testLaunchPlaceholderIfNecessary_skipIfActivityInOverlay() { setupPlaceholderRule(mActivity); createOrUpdateOverlayTaskFragmentIfNeeded("test1", mActivity); mSplitController.mTransactionManager.startNewTransaction(); assertThat(mSplitController.launchPlaceholderIfNecessary(mTransaction, mActivity, false /* isOnCreated */)).isFalse(); verify(mTransaction, never()).startActivityInTaskFragment(any(), any(), any(), any()); } /** Setups a rule to launch placeholder for the given activity. */ private void setupPlaceholderRule(@NonNull Activity primaryActivity) { final SplitRule placeholderRule = createSplitPlaceholderRuleBuilder(PLACEHOLDER_INTENT, primaryActivity::equals, i -> false, w -> true) .setDefaultSplitAttributes(SPLIT_ATTRIBUTES) .build(); mSplitController.setEmbeddingRules(Collections.singleton(placeholderRule)); } @Test public void testResolveStartActivityIntent_skipIfAssociateOverlay() { final Intent intent = new Intent(); mSplitController.setEmbeddingRules(Collections.singleton( createSplitRule(mActivity, intent))); createTestOverlayContainer(TASK_ID, "test", true /* isVisible */, true /* associateLaunchingActivity */, mActivity); final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( mTransaction, TASK_ID, intent, mActivity); assertThat(container).isNull(); verify(mSplitController, never()).resolveStartActivityIntentByRule(any(), anyInt(), any(), any()); } @Test public void testResolveStartActivityIntent_skipIfLaunchingActivityInOverlay() { final Intent intent = new Intent(); mSplitController.setEmbeddingRules(Collections.singleton( createSplitRule(mActivity, intent))); createOrUpdateOverlayTaskFragmentIfNeeded("test1", mActivity); final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( mTransaction, TASK_ID, intent, mActivity); assertThat(container).isNull(); verify(mSplitController, never()).resolveStartActivityIntentByRule(any(), anyInt(), any(), any()); } /** * A simplified version of {@link SplitController#createOrUpdateOverlayTaskFragmentIfNeeded} */ Loading @@ -726,9 +831,16 @@ public class OverlayPresentationTest { /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */ @NonNull private TaskFragmentContainer createMockTaskFragmentContainer(@NonNull Activity activity) { return createMockTaskFragmentContainer(activity, false /* isVisible */); } /** Creates a mock TaskFragment that has been registered and appeared in the organizer. */ @NonNull private TaskFragmentContainer createMockTaskFragmentContainer( @NonNull Activity activity, boolean isVisible) { final TaskFragmentContainer container = mSplitController.newContainer(activity, activity.getTaskId()); setupTaskFragmentInfo(container, activity, false /* isVisible */); setupTaskFragmentInfo(container, activity, isVisible); return container; } Loading @@ -745,17 +857,26 @@ public class OverlayPresentationTest { true /* associateLaunchingActivity */); } @NonNull private TaskFragmentContainer createTestOverlayContainer(int taskId, @NonNull String tag, boolean isVisible, boolean associateLaunchingActivity) { return createTestOverlayContainer(taskId, tag, isVisible, associateLaunchingActivity, 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) { Activity activity = createMockActivity(); boolean isVisible, boolean associateLaunchingActivity, @Nullable Activity launchingActivity) { final Activity activity = launchingActivity != null ? launchingActivity : createMockActivity(); TaskFragmentContainer overlayContainer = mSplitController.newContainer( null /* pendingAppearedActivity */, mIntent, activity, taskId, null /* pairedPrimaryContainer */, tag, Bundle.EMPTY, associateLaunchingActivity); setupTaskFragmentInfo(overlayContainer, activity, isVisible); setupTaskFragmentInfo(overlayContainer, createMockActivity(), isVisible); return overlayContainer; } Loading