Loading libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +26 −25 Original line number Diff line number Diff line Loading @@ -24,9 +24,9 @@ import static androidx.window.extensions.embedding.SplitContainer.getFinishSecon import static androidx.window.extensions.embedding.SplitContainer.isStickyPlaceholderRule; import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenAdjacent; import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenStacked; import static androidx.window.extensions.embedding.SplitPresenter.boundsSmallerThanMinDimensions; import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPAND_FAILED_NO_TF_INFO; import static androidx.window.extensions.embedding.SplitPresenter.getActivityIntentMinDimensionsPair; import static androidx.window.extensions.embedding.SplitPresenter.getMinDimensions; import static androidx.window.extensions.embedding.SplitPresenter.getNonEmbeddedActivityBounds; import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSideBySide; import android.app.Activity; Loading Loading @@ -381,6 +381,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * in a state that the caller shouldn't handle. */ @VisibleForTesting @GuardedBy("mLock") boolean resolveActivityToContainer(@NonNull Activity activity, boolean isOnReparent) { if (isInPictureInPicture(activity) || activity.isFinishing()) { // We don't embed activity when it is in PIP, or finishing. Return true since we don't Loading Loading @@ -581,8 +582,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } /** Finds the activity below the given activity. */ @VisibleForTesting @Nullable private Activity findActivityBelow(@NonNull Activity activity) { Activity findActivityBelow(@NonNull Activity activity) { Activity activityBelow = null; final TaskFragmentContainer container = getContainerWithActivity(activity); if (container != null) { Loading @@ -606,6 +608,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Checks if there is a rule to split the two activities. If there is one, puts them into split * and returns {@code true}. Otherwise, returns {@code false}. */ @GuardedBy("mLock") private boolean putActivitiesIntoSplitIfNecessary(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) { final SplitPairRule splitRule = getSplitRule(primaryActivity, secondaryActivity); Loading @@ -616,26 +619,26 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen primaryActivity); final SplitContainer splitContainer = getActiveSplitForContainer(primaryContainer); if (splitContainer != null && primaryContainer == splitContainer.getPrimaryContainer() && canReuseContainer(splitRule, splitContainer.getSplitRule()) && !boundsSmallerThanMinDimensions(primaryContainer.getLastRequestedBounds(), getMinDimensions(primaryActivity))) { && canReuseContainer(splitRule, splitContainer.getSplitRule())) { // Can launch in the existing secondary container if the rules share the same // presentation. final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); if (secondaryContainer == getContainerWithActivity(secondaryActivity) && !boundsSmallerThanMinDimensions(secondaryContainer.getLastRequestedBounds(), getMinDimensions(secondaryActivity))) { if (secondaryContainer == getContainerWithActivity(secondaryActivity)) { // The activity is already in the target TaskFragment. return true; } secondaryContainer.addPendingAppearedActivity(secondaryActivity); final WindowContainerTransaction wct = new WindowContainerTransaction(); if (mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity, secondaryActivity, null /* secondaryIntent */) != RESULT_EXPAND_FAILED_NO_TF_INFO) { wct.reparentActivityToTaskFragment( secondaryContainer.getTaskFragmentToken(), secondaryActivity.getActivityToken()); mPresenter.applyTransaction(wct); return true; } } // Create new split pair. mPresenter.createNewSplitContainer(primaryActivity, secondaryActivity, splitRule); return true; Loading Loading @@ -792,6 +795,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns a container for the new activity intent to launch into as splitting with the primary * activity. */ @GuardedBy("mLock") @Nullable private TaskFragmentContainer getSecondaryContainerForSplitIfAny( @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, Loading @@ -805,17 +809,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (splitContainer != null && existingContainer == splitContainer.getPrimaryContainer() && (canReuseContainer(splitRule, splitContainer.getSplitRule()) // TODO(b/231845476) we should always respect clearTop. || !respectClearTop)) { final Rect secondaryBounds = splitContainer.getSecondaryContainer() .getLastRequestedBounds(); if (secondaryBounds.isEmpty() || !boundsSmallerThanMinDimensions(secondaryBounds, getMinDimensions(intent))) { || !respectClearTop) && mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity, null /* secondaryActivity */, intent) != RESULT_EXPAND_FAILED_NO_TF_INFO) { // Can launch in the existing secondary container if the rules share the same // presentation. return splitContainer.getSecondaryContainer(); } } // Create a new TaskFragment to split with the primary activity for the new activity. return mPresenter.createNewSplitWithEmptySideContainer(wct, primaryActivity, intent, splitRule); Loading Loading @@ -868,6 +868,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * if needed. * @param taskId parent Task of the new TaskFragment. */ @GuardedBy("mLock") TaskFragmentContainer newContainer(@Nullable Activity pendingAppearedActivity, @Nullable Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId) { if (activityInTask == null) { Loading @@ -881,7 +882,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen pendingAppearedIntent, taskContainer, this); if (!taskContainer.isTaskBoundsInitialized()) { // Get the initial bounds before the TaskFragment has appeared. final Rect taskBounds = SplitPresenter.getTaskBoundsFromActivity(activityInTask); final Rect taskBounds = getNonEmbeddedActivityBounds(activityInTask); if (!taskContainer.setTaskBounds(taskBounds)) { Log.w(TAG, "Can't find bounds from activity=" + activityInTask); } Loading libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +83 −2 Original line number Diff line number Diff line Loading @@ -65,6 +65,41 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { }) private @interface Position {} /** * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, * Activity, Activity, Intent)}. * No need to expand the splitContainer because screen is big enough to * {@link #shouldShowSideBySide(Rect, SplitRule, Pair)} and minimum dimensions is satisfied. */ static final int RESULT_NOT_EXPANDED = 0; /** * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, * Activity, Activity, Intent)}. * The splitContainer should be expanded. It is usually because minimum dimensions is not * satisfied. * @see #shouldShowSideBySide(Rect, SplitRule, Pair) */ static final int RESULT_EXPANDED = 1; /** * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, * Activity, Activity, Intent)}. * The splitContainer should be expanded, but the client side hasn't received * {@link android.window.TaskFragmentInfo} yet. Fallback to create new expanded SplitContainer * instead. */ static final int RESULT_EXPAND_FAILED_NO_TF_INFO = 2; /** * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, * Activity, Activity, Intent)} */ @IntDef(value = { RESULT_NOT_EXPANDED, RESULT_EXPANDED, RESULT_EXPAND_FAILED_NO_TF_INFO, }) private @interface ResultCode {} private final SplitController mController; SplitPresenter(@NonNull Executor executor, SplitController controller) { Loading Loading @@ -396,6 +431,44 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { super.updateWindowingMode(wct, fragmentToken, windowingMode); } /** * Expands the split container if the current split bounds are smaller than the Activity or * Intent that is added to the container. * * @return the {@link ResultCode} based on {@link #shouldShowSideBySide(Rect, SplitRule, Pair)} * and if {@link android.window.TaskFragmentInfo} has reported to the client side. */ @ResultCode int expandSplitContainerIfNeeded(@NonNull WindowContainerTransaction wct, @NonNull SplitContainer splitContainer, @NonNull Activity primaryActivity, @Nullable Activity secondaryActivity, @Nullable Intent secondaryIntent) { if (secondaryActivity == null && secondaryIntent == null) { throw new IllegalArgumentException("Either secondaryActivity or secondaryIntent must be" + " non-null."); } final Rect taskBounds = getParentContainerBounds(primaryActivity); final Pair<Size, Size> minDimensionsPair; if (secondaryActivity != null) { minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity, secondaryActivity); } else { minDimensionsPair = getActivityIntentMinDimensionsPair(primaryActivity, secondaryIntent); } // Expand the splitContainer if minimum dimensions are not satisfied. if (!shouldShowSideBySide(taskBounds, splitContainer.getSplitRule(), minDimensionsPair)) { // If the client side hasn't received TaskFragmentInfo yet, we can't change TaskFragment // bounds. Return failure to create a new SplitContainer which fills task bounds. if (splitContainer.getPrimaryContainer().getInfo() == null || splitContainer.getSecondaryContainer().getInfo() == null) { return RESULT_EXPAND_FAILED_NO_TF_INFO; } expandTaskFragment(wct, splitContainer.getPrimaryContainer().getTaskFragmentToken()); expandTaskFragment(wct, splitContainer.getSecondaryContainer().getTaskFragmentToken()); return RESULT_EXPANDED; } return RESULT_NOT_EXPANDED; } static boolean shouldShowSideBySide(@NonNull Rect parentBounds, @NonNull SplitRule rule) { return shouldShowSideBySide(parentBounds, rule, null /* minimumDimensionPair */); } Loading Loading @@ -565,11 +638,19 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { if (container != null) { return getParentContainerBounds(container); } return getTaskBoundsFromActivity(activity); // Obtain bounds from Activity instead because the Activity hasn't been embedded yet. return getNonEmbeddedActivityBounds(activity); } /** * Obtains the bounds from a non-embedded Activity. * <p> * Note that callers should use {@link #getParentContainerBounds(Activity)} instead for most * cases unless we want to obtain task bounds before * {@link TaskContainer#isTaskBoundsInitialized()}. */ @NonNull static Rect getTaskBoundsFromActivity(@NonNull Activity activity) { static Rect getNonEmbeddedActivityBounds(@NonNull Activity activity) { final WindowConfiguration windowConfiguration = activity.getResources().getConfiguration().windowConfiguration; if (!activity.isInMultiWindowMode()) { Loading libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java +26 −1 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import static org.mockito.Mockito.mock; import android.annotation.NonNull; import android.app.Activity; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Point; import android.graphics.Rect; Loading Loading @@ -57,13 +58,21 @@ public class EmbeddingTestUtils { /** Creates a rule to always split the given activity and the given intent. */ static SplitRule createSplitRule(@NonNull Activity primaryActivity, @NonNull Intent secondaryIntent) { return createSplitRule(primaryActivity, secondaryIntent, true /* clearTop */); } /** Creates a rule to always split the given activity and the given intent. */ static SplitRule createSplitRule(@NonNull Activity primaryActivity, @NonNull Intent secondaryIntent, boolean clearTop) { final Pair<Activity, Intent> targetPair = new Pair<>(primaryActivity, secondaryIntent); return new SplitPairRule.Builder( activityPair -> false, targetPair::equals, w -> true) .setSplitRatio(SPLIT_RATIO) .setShouldClearTop(true) .setShouldClearTop(clearTop) .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY) .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY) .build(); } Loading @@ -75,6 +84,14 @@ public class EmbeddingTestUtils { true /* clearTop */); } /** Creates a rule to always split the given activities. */ static SplitRule createSplitRule(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity, boolean clearTop) { return createSplitRule(primaryActivity, secondaryActivity, DEFAULT_FINISH_PRIMARY_WITH_SECONDARY, DEFAULT_FINISH_SECONDARY_WITH_PRIMARY, clearTop); } /** Creates a rule to always split the given activities with the given finish behaviors. */ static SplitRule createSplitRule(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity, int finishPrimaryWithSecondary, Loading Loading @@ -105,4 +122,12 @@ public class EmbeddingTestUtils { false /* isTaskFragmentClearedForPip */, new Point()); } static ActivityInfo createActivityInfoWithMinDimensions() { ActivityInfo aInfo = new ActivityInfo(); final Rect primaryBounds = getSplitBounds(true /* isPrimary */); aInfo.windowLayout = new ActivityInfo.WindowLayout(0, 0, 0, 0, 0, primaryBounds.width() + 1, primaryBounds.height() + 1); return aInfo; } } libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +98 −18 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_RATIO; 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.createActivityInfoWithMinDimensions; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule; import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds; Loading @@ -34,6 +35,7 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; Loading Loading @@ -435,6 +437,50 @@ public class SplitControllerTest { assertTrue(container.areLastRequestedBoundsEqual(null)); } @Test public void testResolveStartActivityIntent_shouldExpandSplitContainer() { final Intent intent = new Intent().setComponent( new ComponentName(ApplicationProvider.getApplicationContext(), MinimumDimensionActivity.class)); setupSplitRule(mActivity, intent, false /* clearTop */); final Activity secondaryActivity = createMockActivity(); addSplitTaskFragments(mActivity, secondaryActivity, false /* clearTop */); final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( mTransaction, TASK_ID, intent, mActivity); final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( mActivity); assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, container)); assertTrue(primaryContainer.areLastRequestedBoundsEqual(null)); assertTrue(container.areLastRequestedBoundsEqual(null)); assertEquals(container, mSplitController.getContainerWithActivity(secondaryActivity)); } @Test public void testResolveStartActivityIntent_noInfo_shouldCreateSplitContainer() { final Intent intent = new Intent().setComponent( new ComponentName(ApplicationProvider.getApplicationContext(), MinimumDimensionActivity.class)); setupSplitRule(mActivity, intent, false /* clearTop */); final Activity secondaryActivity = createMockActivity(); addSplitTaskFragments(mActivity, secondaryActivity, false /* clearTop */); final TaskFragmentContainer secondaryContainer = mSplitController .getContainerWithActivity(secondaryActivity); secondaryContainer.mInfo = null; final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( mTransaction, TASK_ID, intent, mActivity); final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( mActivity); assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, container)); assertTrue(primaryContainer.areLastRequestedBoundsEqual(null)); assertTrue(container.areLastRequestedBoundsEqual(null)); assertNotEquals(container, secondaryContainer); } @Test public void testPlaceActivityInTopContainer() { mSplitController.placeActivityInTopContainer(mActivity); Loading Loading @@ -787,11 +833,7 @@ public class SplitControllerTest { final Activity activityBelow = createMockActivity(); setupSplitRule(mActivity, activityBelow); ActivityInfo aInfo = new ActivityInfo(); final Rect primaryBounds = getSplitBounds(true /* isPrimary */); aInfo.windowLayout = new ActivityInfo.WindowLayout(0, 0, 0, 0, 0, primaryBounds.width() + 1, primaryBounds.height() + 1); doReturn(aInfo).when(mActivity).getActivityInfo(); doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo(); final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, TASK_ID); Loading @@ -810,17 +852,12 @@ public class SplitControllerTest { final Activity activityBelow = createMockActivity(); setupSplitRule(activityBelow, mActivity); ActivityInfo aInfo = new ActivityInfo(); final Rect secondaryBounds = getSplitBounds(false /* isPrimary */); aInfo.windowLayout = new ActivityInfo.WindowLayout(0, 0, 0, 0, 0, secondaryBounds.width() + 1, secondaryBounds.height() + 1); doReturn(aInfo).when(mActivity).getActivityInfo(); doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo(); final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, TASK_ID); container.addPendingAppearedActivity(mActivity); // Allow to split as primary. boolean result = mSplitController.resolveActivityToContainer(mActivity, false /* isOnReparent */); Loading @@ -828,6 +865,29 @@ public class SplitControllerTest { assertSplitPair(activityBelow, mActivity, true /* matchParentBounds */); } // Suppress GuardedBy warning on unit tests @SuppressWarnings("GuardedBy") @Test public void testResolveActivityToContainer_minDimensions_shouldExpandSplitContainer() { final Activity primaryActivity = createMockActivity(); final Activity secondaryActivity = createMockActivity(); addSplitTaskFragments(primaryActivity, secondaryActivity, false /* clearTop */); setupSplitRule(primaryActivity, mActivity, false /* clearTop */); doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo(); doReturn(secondaryActivity).when(mSplitController).findActivityBelow(eq(mActivity)); clearInvocations(mSplitPresenter); boolean result = mSplitController.resolveActivityToContainer(mActivity, false /* isOnReparent */); assertTrue(result); assertSplitPair(primaryActivity, mActivity, true /* matchParentBounds */); assertEquals(mSplitController.getContainerWithActivity(secondaryActivity), mSplitController.getContainerWithActivity(mActivity)); verify(mSplitPresenter, never()).createNewSplitContainer(any(), any(), any()); } @Test public void testResolveActivityToContainer_inUnknownTaskFragment() { doReturn(new Binder()).when(mSplitController).getInitialTaskFragmentToken(mActivity); Loading Loading @@ -944,23 +1004,41 @@ public class SplitControllerTest { /** Setups a rule to always split the given activities. */ private void setupSplitRule(@NonNull Activity primaryActivity, @NonNull Intent secondaryIntent) { final SplitRule splitRule = createSplitRule(primaryActivity, secondaryIntent); setupSplitRule(primaryActivity, secondaryIntent, true /* clearTop */); } /** Setups a rule to always split the given activities. */ private void setupSplitRule(@NonNull Activity primaryActivity, @NonNull Intent secondaryIntent, boolean clearTop) { final SplitRule splitRule = createSplitRule(primaryActivity, secondaryIntent, clearTop); mSplitController.setEmbeddingRules(Collections.singleton(splitRule)); } /** Setups a rule to always split the given activities. */ private void setupSplitRule(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) { final SplitRule splitRule = createSplitRule(primaryActivity, secondaryActivity); setupSplitRule(primaryActivity, secondaryActivity, true /* clearTop */); } /** Setups a rule to always split the given activities. */ private void setupSplitRule(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity, boolean clearTop) { final SplitRule splitRule = createSplitRule(primaryActivity, secondaryActivity, clearTop); mSplitController.setEmbeddingRules(Collections.singleton(splitRule)); } /** Adds a pair of TaskFragments as split for the given activities. */ private void addSplitTaskFragments(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) { addSplitTaskFragments(primaryActivity, secondaryActivity, true /* clearTop */); } /** Adds a pair of TaskFragments as split for the given activities. */ private void addSplitTaskFragments(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity, boolean clearTop) { registerSplitPair(createMockTaskFragmentContainer(primaryActivity), createMockTaskFragmentContainer(secondaryActivity), createSplitRule(primaryActivity, secondaryActivity)); createSplitRule(primaryActivity, secondaryActivity, clearTop)); } /** Registers the two given TaskFragments as split pair. */ Loading Loading @@ -1011,16 +1089,18 @@ public class SplitControllerTest { if (primaryContainer.mInfo != null) { final Rect primaryBounds = matchParentBounds ? new Rect() : getSplitBounds(true /* isPrimary */); final int windowingMode = matchParentBounds ? WINDOWING_MODE_UNDEFINED : WINDOWING_MODE_MULTI_WINDOW; assertTrue(primaryContainer.areLastRequestedBoundsEqual(primaryBounds)); assertTrue(primaryContainer.isLastRequestedWindowingModeEqual( WINDOWING_MODE_MULTI_WINDOW)); assertTrue(primaryContainer.isLastRequestedWindowingModeEqual(windowingMode)); } if (secondaryContainer.mInfo != null) { final Rect secondaryBounds = matchParentBounds ? new Rect() : getSplitBounds(false /* isPrimary */); final int windowingMode = matchParentBounds ? WINDOWING_MODE_UNDEFINED : WINDOWING_MODE_MULTI_WINDOW; assertTrue(secondaryContainer.areLastRequestedBoundsEqual(secondaryBounds)); assertTrue(secondaryContainer.isLastRequestedWindowingModeEqual( WINDOWING_MODE_MULTI_WINDOW)); assertTrue(secondaryContainer.isLastRequestedWindowingModeEqual(windowingMode)); } } } libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java +54 −0 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +26 −25 Original line number Diff line number Diff line Loading @@ -24,9 +24,9 @@ import static androidx.window.extensions.embedding.SplitContainer.getFinishSecon import static androidx.window.extensions.embedding.SplitContainer.isStickyPlaceholderRule; import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenAdjacent; import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenStacked; import static androidx.window.extensions.embedding.SplitPresenter.boundsSmallerThanMinDimensions; import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPAND_FAILED_NO_TF_INFO; import static androidx.window.extensions.embedding.SplitPresenter.getActivityIntentMinDimensionsPair; import static androidx.window.extensions.embedding.SplitPresenter.getMinDimensions; import static androidx.window.extensions.embedding.SplitPresenter.getNonEmbeddedActivityBounds; import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSideBySide; import android.app.Activity; Loading Loading @@ -381,6 +381,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * in a state that the caller shouldn't handle. */ @VisibleForTesting @GuardedBy("mLock") boolean resolveActivityToContainer(@NonNull Activity activity, boolean isOnReparent) { if (isInPictureInPicture(activity) || activity.isFinishing()) { // We don't embed activity when it is in PIP, or finishing. Return true since we don't Loading Loading @@ -581,8 +582,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } /** Finds the activity below the given activity. */ @VisibleForTesting @Nullable private Activity findActivityBelow(@NonNull Activity activity) { Activity findActivityBelow(@NonNull Activity activity) { Activity activityBelow = null; final TaskFragmentContainer container = getContainerWithActivity(activity); if (container != null) { Loading @@ -606,6 +608,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Checks if there is a rule to split the two activities. If there is one, puts them into split * and returns {@code true}. Otherwise, returns {@code false}. */ @GuardedBy("mLock") private boolean putActivitiesIntoSplitIfNecessary(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) { final SplitPairRule splitRule = getSplitRule(primaryActivity, secondaryActivity); Loading @@ -616,26 +619,26 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen primaryActivity); final SplitContainer splitContainer = getActiveSplitForContainer(primaryContainer); if (splitContainer != null && primaryContainer == splitContainer.getPrimaryContainer() && canReuseContainer(splitRule, splitContainer.getSplitRule()) && !boundsSmallerThanMinDimensions(primaryContainer.getLastRequestedBounds(), getMinDimensions(primaryActivity))) { && canReuseContainer(splitRule, splitContainer.getSplitRule())) { // Can launch in the existing secondary container if the rules share the same // presentation. final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); if (secondaryContainer == getContainerWithActivity(secondaryActivity) && !boundsSmallerThanMinDimensions(secondaryContainer.getLastRequestedBounds(), getMinDimensions(secondaryActivity))) { if (secondaryContainer == getContainerWithActivity(secondaryActivity)) { // The activity is already in the target TaskFragment. return true; } secondaryContainer.addPendingAppearedActivity(secondaryActivity); final WindowContainerTransaction wct = new WindowContainerTransaction(); if (mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity, secondaryActivity, null /* secondaryIntent */) != RESULT_EXPAND_FAILED_NO_TF_INFO) { wct.reparentActivityToTaskFragment( secondaryContainer.getTaskFragmentToken(), secondaryActivity.getActivityToken()); mPresenter.applyTransaction(wct); return true; } } // Create new split pair. mPresenter.createNewSplitContainer(primaryActivity, secondaryActivity, splitRule); return true; Loading Loading @@ -792,6 +795,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * Returns a container for the new activity intent to launch into as splitting with the primary * activity. */ @GuardedBy("mLock") @Nullable private TaskFragmentContainer getSecondaryContainerForSplitIfAny( @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity, Loading @@ -805,17 +809,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (splitContainer != null && existingContainer == splitContainer.getPrimaryContainer() && (canReuseContainer(splitRule, splitContainer.getSplitRule()) // TODO(b/231845476) we should always respect clearTop. || !respectClearTop)) { final Rect secondaryBounds = splitContainer.getSecondaryContainer() .getLastRequestedBounds(); if (secondaryBounds.isEmpty() || !boundsSmallerThanMinDimensions(secondaryBounds, getMinDimensions(intent))) { || !respectClearTop) && mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity, null /* secondaryActivity */, intent) != RESULT_EXPAND_FAILED_NO_TF_INFO) { // Can launch in the existing secondary container if the rules share the same // presentation. return splitContainer.getSecondaryContainer(); } } // Create a new TaskFragment to split with the primary activity for the new activity. return mPresenter.createNewSplitWithEmptySideContainer(wct, primaryActivity, intent, splitRule); Loading Loading @@ -868,6 +868,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen * if needed. * @param taskId parent Task of the new TaskFragment. */ @GuardedBy("mLock") TaskFragmentContainer newContainer(@Nullable Activity pendingAppearedActivity, @Nullable Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId) { if (activityInTask == null) { Loading @@ -881,7 +882,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen pendingAppearedIntent, taskContainer, this); if (!taskContainer.isTaskBoundsInitialized()) { // Get the initial bounds before the TaskFragment has appeared. final Rect taskBounds = SplitPresenter.getTaskBoundsFromActivity(activityInTask); final Rect taskBounds = getNonEmbeddedActivityBounds(activityInTask); if (!taskContainer.setTaskBounds(taskBounds)) { Log.w(TAG, "Can't find bounds from activity=" + activityInTask); } Loading
libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +83 −2 Original line number Diff line number Diff line Loading @@ -65,6 +65,41 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { }) private @interface Position {} /** * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, * Activity, Activity, Intent)}. * No need to expand the splitContainer because screen is big enough to * {@link #shouldShowSideBySide(Rect, SplitRule, Pair)} and minimum dimensions is satisfied. */ static final int RESULT_NOT_EXPANDED = 0; /** * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, * Activity, Activity, Intent)}. * The splitContainer should be expanded. It is usually because minimum dimensions is not * satisfied. * @see #shouldShowSideBySide(Rect, SplitRule, Pair) */ static final int RESULT_EXPANDED = 1; /** * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, * Activity, Activity, Intent)}. * The splitContainer should be expanded, but the client side hasn't received * {@link android.window.TaskFragmentInfo} yet. Fallback to create new expanded SplitContainer * instead. */ static final int RESULT_EXPAND_FAILED_NO_TF_INFO = 2; /** * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer, * Activity, Activity, Intent)} */ @IntDef(value = { RESULT_NOT_EXPANDED, RESULT_EXPANDED, RESULT_EXPAND_FAILED_NO_TF_INFO, }) private @interface ResultCode {} private final SplitController mController; SplitPresenter(@NonNull Executor executor, SplitController controller) { Loading Loading @@ -396,6 +431,44 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { super.updateWindowingMode(wct, fragmentToken, windowingMode); } /** * Expands the split container if the current split bounds are smaller than the Activity or * Intent that is added to the container. * * @return the {@link ResultCode} based on {@link #shouldShowSideBySide(Rect, SplitRule, Pair)} * and if {@link android.window.TaskFragmentInfo} has reported to the client side. */ @ResultCode int expandSplitContainerIfNeeded(@NonNull WindowContainerTransaction wct, @NonNull SplitContainer splitContainer, @NonNull Activity primaryActivity, @Nullable Activity secondaryActivity, @Nullable Intent secondaryIntent) { if (secondaryActivity == null && secondaryIntent == null) { throw new IllegalArgumentException("Either secondaryActivity or secondaryIntent must be" + " non-null."); } final Rect taskBounds = getParentContainerBounds(primaryActivity); final Pair<Size, Size> minDimensionsPair; if (secondaryActivity != null) { minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity, secondaryActivity); } else { minDimensionsPair = getActivityIntentMinDimensionsPair(primaryActivity, secondaryIntent); } // Expand the splitContainer if minimum dimensions are not satisfied. if (!shouldShowSideBySide(taskBounds, splitContainer.getSplitRule(), minDimensionsPair)) { // If the client side hasn't received TaskFragmentInfo yet, we can't change TaskFragment // bounds. Return failure to create a new SplitContainer which fills task bounds. if (splitContainer.getPrimaryContainer().getInfo() == null || splitContainer.getSecondaryContainer().getInfo() == null) { return RESULT_EXPAND_FAILED_NO_TF_INFO; } expandTaskFragment(wct, splitContainer.getPrimaryContainer().getTaskFragmentToken()); expandTaskFragment(wct, splitContainer.getSecondaryContainer().getTaskFragmentToken()); return RESULT_EXPANDED; } return RESULT_NOT_EXPANDED; } static boolean shouldShowSideBySide(@NonNull Rect parentBounds, @NonNull SplitRule rule) { return shouldShowSideBySide(parentBounds, rule, null /* minimumDimensionPair */); } Loading Loading @@ -565,11 +638,19 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { if (container != null) { return getParentContainerBounds(container); } return getTaskBoundsFromActivity(activity); // Obtain bounds from Activity instead because the Activity hasn't been embedded yet. return getNonEmbeddedActivityBounds(activity); } /** * Obtains the bounds from a non-embedded Activity. * <p> * Note that callers should use {@link #getParentContainerBounds(Activity)} instead for most * cases unless we want to obtain task bounds before * {@link TaskContainer#isTaskBoundsInitialized()}. */ @NonNull static Rect getTaskBoundsFromActivity(@NonNull Activity activity) { static Rect getNonEmbeddedActivityBounds(@NonNull Activity activity) { final WindowConfiguration windowConfiguration = activity.getResources().getConfiguration().windowConfiguration; if (!activity.isInMultiWindowMode()) { Loading
libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java +26 −1 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import static org.mockito.Mockito.mock; import android.annotation.NonNull; import android.app.Activity; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Point; import android.graphics.Rect; Loading Loading @@ -57,13 +58,21 @@ public class EmbeddingTestUtils { /** Creates a rule to always split the given activity and the given intent. */ static SplitRule createSplitRule(@NonNull Activity primaryActivity, @NonNull Intent secondaryIntent) { return createSplitRule(primaryActivity, secondaryIntent, true /* clearTop */); } /** Creates a rule to always split the given activity and the given intent. */ static SplitRule createSplitRule(@NonNull Activity primaryActivity, @NonNull Intent secondaryIntent, boolean clearTop) { final Pair<Activity, Intent> targetPair = new Pair<>(primaryActivity, secondaryIntent); return new SplitPairRule.Builder( activityPair -> false, targetPair::equals, w -> true) .setSplitRatio(SPLIT_RATIO) .setShouldClearTop(true) .setShouldClearTop(clearTop) .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY) .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY) .build(); } Loading @@ -75,6 +84,14 @@ public class EmbeddingTestUtils { true /* clearTop */); } /** Creates a rule to always split the given activities. */ static SplitRule createSplitRule(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity, boolean clearTop) { return createSplitRule(primaryActivity, secondaryActivity, DEFAULT_FINISH_PRIMARY_WITH_SECONDARY, DEFAULT_FINISH_SECONDARY_WITH_PRIMARY, clearTop); } /** Creates a rule to always split the given activities with the given finish behaviors. */ static SplitRule createSplitRule(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity, int finishPrimaryWithSecondary, Loading Loading @@ -105,4 +122,12 @@ public class EmbeddingTestUtils { false /* isTaskFragmentClearedForPip */, new Point()); } static ActivityInfo createActivityInfoWithMinDimensions() { ActivityInfo aInfo = new ActivityInfo(); final Rect primaryBounds = getSplitBounds(true /* isPrimary */); aInfo.windowLayout = new ActivityInfo.WindowLayout(0, 0, 0, 0, 0, primaryBounds.width() + 1, primaryBounds.height() + 1); return aInfo; } }
libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +98 −18 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_RATIO; 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.createActivityInfoWithMinDimensions; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule; import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds; Loading @@ -34,6 +35,7 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; Loading Loading @@ -435,6 +437,50 @@ public class SplitControllerTest { assertTrue(container.areLastRequestedBoundsEqual(null)); } @Test public void testResolveStartActivityIntent_shouldExpandSplitContainer() { final Intent intent = new Intent().setComponent( new ComponentName(ApplicationProvider.getApplicationContext(), MinimumDimensionActivity.class)); setupSplitRule(mActivity, intent, false /* clearTop */); final Activity secondaryActivity = createMockActivity(); addSplitTaskFragments(mActivity, secondaryActivity, false /* clearTop */); final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( mTransaction, TASK_ID, intent, mActivity); final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( mActivity); assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, container)); assertTrue(primaryContainer.areLastRequestedBoundsEqual(null)); assertTrue(container.areLastRequestedBoundsEqual(null)); assertEquals(container, mSplitController.getContainerWithActivity(secondaryActivity)); } @Test public void testResolveStartActivityIntent_noInfo_shouldCreateSplitContainer() { final Intent intent = new Intent().setComponent( new ComponentName(ApplicationProvider.getApplicationContext(), MinimumDimensionActivity.class)); setupSplitRule(mActivity, intent, false /* clearTop */); final Activity secondaryActivity = createMockActivity(); addSplitTaskFragments(mActivity, secondaryActivity, false /* clearTop */); final TaskFragmentContainer secondaryContainer = mSplitController .getContainerWithActivity(secondaryActivity); secondaryContainer.mInfo = null; final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent( mTransaction, TASK_ID, intent, mActivity); final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity( mActivity); assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, container)); assertTrue(primaryContainer.areLastRequestedBoundsEqual(null)); assertTrue(container.areLastRequestedBoundsEqual(null)); assertNotEquals(container, secondaryContainer); } @Test public void testPlaceActivityInTopContainer() { mSplitController.placeActivityInTopContainer(mActivity); Loading Loading @@ -787,11 +833,7 @@ public class SplitControllerTest { final Activity activityBelow = createMockActivity(); setupSplitRule(mActivity, activityBelow); ActivityInfo aInfo = new ActivityInfo(); final Rect primaryBounds = getSplitBounds(true /* isPrimary */); aInfo.windowLayout = new ActivityInfo.WindowLayout(0, 0, 0, 0, 0, primaryBounds.width() + 1, primaryBounds.height() + 1); doReturn(aInfo).when(mActivity).getActivityInfo(); doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo(); final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, TASK_ID); Loading @@ -810,17 +852,12 @@ public class SplitControllerTest { final Activity activityBelow = createMockActivity(); setupSplitRule(activityBelow, mActivity); ActivityInfo aInfo = new ActivityInfo(); final Rect secondaryBounds = getSplitBounds(false /* isPrimary */); aInfo.windowLayout = new ActivityInfo.WindowLayout(0, 0, 0, 0, 0, secondaryBounds.width() + 1, secondaryBounds.height() + 1); doReturn(aInfo).when(mActivity).getActivityInfo(); doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo(); final TaskFragmentContainer container = mSplitController.newContainer(activityBelow, TASK_ID); container.addPendingAppearedActivity(mActivity); // Allow to split as primary. boolean result = mSplitController.resolveActivityToContainer(mActivity, false /* isOnReparent */); Loading @@ -828,6 +865,29 @@ public class SplitControllerTest { assertSplitPair(activityBelow, mActivity, true /* matchParentBounds */); } // Suppress GuardedBy warning on unit tests @SuppressWarnings("GuardedBy") @Test public void testResolveActivityToContainer_minDimensions_shouldExpandSplitContainer() { final Activity primaryActivity = createMockActivity(); final Activity secondaryActivity = createMockActivity(); addSplitTaskFragments(primaryActivity, secondaryActivity, false /* clearTop */); setupSplitRule(primaryActivity, mActivity, false /* clearTop */); doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo(); doReturn(secondaryActivity).when(mSplitController).findActivityBelow(eq(mActivity)); clearInvocations(mSplitPresenter); boolean result = mSplitController.resolveActivityToContainer(mActivity, false /* isOnReparent */); assertTrue(result); assertSplitPair(primaryActivity, mActivity, true /* matchParentBounds */); assertEquals(mSplitController.getContainerWithActivity(secondaryActivity), mSplitController.getContainerWithActivity(mActivity)); verify(mSplitPresenter, never()).createNewSplitContainer(any(), any(), any()); } @Test public void testResolveActivityToContainer_inUnknownTaskFragment() { doReturn(new Binder()).when(mSplitController).getInitialTaskFragmentToken(mActivity); Loading Loading @@ -944,23 +1004,41 @@ public class SplitControllerTest { /** Setups a rule to always split the given activities. */ private void setupSplitRule(@NonNull Activity primaryActivity, @NonNull Intent secondaryIntent) { final SplitRule splitRule = createSplitRule(primaryActivity, secondaryIntent); setupSplitRule(primaryActivity, secondaryIntent, true /* clearTop */); } /** Setups a rule to always split the given activities. */ private void setupSplitRule(@NonNull Activity primaryActivity, @NonNull Intent secondaryIntent, boolean clearTop) { final SplitRule splitRule = createSplitRule(primaryActivity, secondaryIntent, clearTop); mSplitController.setEmbeddingRules(Collections.singleton(splitRule)); } /** Setups a rule to always split the given activities. */ private void setupSplitRule(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) { final SplitRule splitRule = createSplitRule(primaryActivity, secondaryActivity); setupSplitRule(primaryActivity, secondaryActivity, true /* clearTop */); } /** Setups a rule to always split the given activities. */ private void setupSplitRule(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity, boolean clearTop) { final SplitRule splitRule = createSplitRule(primaryActivity, secondaryActivity, clearTop); mSplitController.setEmbeddingRules(Collections.singleton(splitRule)); } /** Adds a pair of TaskFragments as split for the given activities. */ private void addSplitTaskFragments(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity) { addSplitTaskFragments(primaryActivity, secondaryActivity, true /* clearTop */); } /** Adds a pair of TaskFragments as split for the given activities. */ private void addSplitTaskFragments(@NonNull Activity primaryActivity, @NonNull Activity secondaryActivity, boolean clearTop) { registerSplitPair(createMockTaskFragmentContainer(primaryActivity), createMockTaskFragmentContainer(secondaryActivity), createSplitRule(primaryActivity, secondaryActivity)); createSplitRule(primaryActivity, secondaryActivity, clearTop)); } /** Registers the two given TaskFragments as split pair. */ Loading Loading @@ -1011,16 +1089,18 @@ public class SplitControllerTest { if (primaryContainer.mInfo != null) { final Rect primaryBounds = matchParentBounds ? new Rect() : getSplitBounds(true /* isPrimary */); final int windowingMode = matchParentBounds ? WINDOWING_MODE_UNDEFINED : WINDOWING_MODE_MULTI_WINDOW; assertTrue(primaryContainer.areLastRequestedBoundsEqual(primaryBounds)); assertTrue(primaryContainer.isLastRequestedWindowingModeEqual( WINDOWING_MODE_MULTI_WINDOW)); assertTrue(primaryContainer.isLastRequestedWindowingModeEqual(windowingMode)); } if (secondaryContainer.mInfo != null) { final Rect secondaryBounds = matchParentBounds ? new Rect() : getSplitBounds(false /* isPrimary */); final int windowingMode = matchParentBounds ? WINDOWING_MODE_UNDEFINED : WINDOWING_MODE_MULTI_WINDOW; assertTrue(secondaryContainer.areLastRequestedBoundsEqual(secondaryBounds)); assertTrue(secondaryContainer.isLastRequestedWindowingModeEqual( WINDOWING_MODE_MULTI_WINDOW)); assertTrue(secondaryContainer.isLastRequestedWindowingModeEqual(windowingMode)); } } }
libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java +54 −0 File changed.Preview size limit exceeded, changes collapsed. Show changes