diff options
| author | 2023-04-17 10:03:33 +0000 | |
|---|---|---|
| committer | 2023-07-07 04:12:08 +0000 | |
| commit | 4944e8e0717dfaa5d85b31bd93353d80d7f287f2 (patch) | |
| tree | 4364feeb5cf0285b97a4afa0747d1d676ce95266 | |
| parent | b5485855d9b601adbd9c10bf49d33466844224e6 (diff) | |
[3/n] Pin ActivityStack
Implementing pinTopActivityStack API, which required in extensions
vendor API level 4.
This is the initial change to make the top-most TF container to
split with another TF container below.
Also adding a SplitPinContainer to hold the two TF containers.
The primary container is set to mutable which could be changed
and updated in runtime (in latter CLs).
The pinned TF container and SplitPinContainer is ensured to be on
top on the client side. The server side change will be done in the
following CLs.
Bug: 208573140
Test: atest SplitControllerTest
Change-Id: Ic932756f0adb4997b3729f0ed53dd9542dd4b7ae
8 files changed, 260 insertions, 18 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java index 76e0e1eb7a95..55eabb039c01 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java @@ -48,7 +48,7 @@ public class WindowExtensionsImpl implements WindowExtensions { // TODO(b/241126279) Introduce constants to better version functionality @Override public int getVendorApiLevel() { - return 3; + return 4; } @NonNull diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java index 18497ad249ee..381e9d472f0f 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitContainer.java @@ -32,7 +32,7 @@ import androidx.window.extensions.core.util.function.Function; */ class SplitContainer { @NonNull - private final TaskFragmentContainer mPrimaryContainer; + private TaskFragmentContainer mPrimaryContainer; @NonNull private final TaskFragmentContainer mSecondaryContainer; @NonNull @@ -46,17 +46,35 @@ class SplitContainer { @NonNull private final IBinder mToken; + /** + * Whether the selection of which container is primary can be changed at runtime. Runtime + * updates is currently possible only for {@link SplitPinContainer} + * + * @see SplitPinContainer + */ + private final boolean mIsPrimaryContainerMutable; + SplitContainer(@NonNull TaskFragmentContainer primaryContainer, @NonNull Activity primaryActivity, @NonNull TaskFragmentContainer secondaryContainer, @NonNull SplitRule splitRule, @NonNull SplitAttributes splitAttributes) { + this(primaryContainer, primaryActivity, secondaryContainer, splitRule, splitAttributes, + false /* isPrimaryContainerMutable */); + } + + SplitContainer(@NonNull TaskFragmentContainer primaryContainer, + @NonNull Activity primaryActivity, + @NonNull TaskFragmentContainer secondaryContainer, + @NonNull SplitRule splitRule, + @NonNull SplitAttributes splitAttributes, boolean isPrimaryContainerMutable) { mPrimaryContainer = primaryContainer; mSecondaryContainer = secondaryContainer; mSplitRule = splitRule; mDefaultSplitAttributes = splitRule.getDefaultSplitAttributes(); mCurrentSplitAttributes = splitAttributes; mToken = new Binder("SplitContainer"); + mIsPrimaryContainerMutable = isPrimaryContainerMutable; if (shouldFinishPrimaryWithSecondary(splitRule)) { if (mPrimaryContainer.getRunningActivityCount() == 1 @@ -74,6 +92,13 @@ class SplitContainer { } } + void setPrimaryContainer(@NonNull TaskFragmentContainer primaryContainer) { + if (!mIsPrimaryContainerMutable) { + throw new IllegalStateException("Cannot update primary TaskFragmentContainer"); + } + mPrimaryContainer = primaryContainer; + } + @NonNull TaskFragmentContainer getPrimaryContainer() { return mPrimaryContainer; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 4cedd41e2d9a..47bd6adb11e2 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -213,6 +213,56 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } @Override + public boolean pinTopActivityStack(int taskId, @NonNull SplitPinRule splitPinRule) { + synchronized (mLock) { + final TaskContainer task = getTaskContainer(taskId); + if (task == null) { + Log.e(TAG, "Cannot find the task for id: " + taskId); + return false; + } + + final TaskFragmentContainer topContainer = + task.getTopNonFinishingTaskFragmentContainer(); + // Cannot pin the TaskFragment if no other TaskFragment behind it. + if (topContainer == null || task.indexOf(topContainer) <= 0) { + Log.w(TAG, "Cannot find an ActivityStack to pin or split"); + return false; + } + // Abort if the top container is already pinned. + if (task.getSplitPinContainer() != null) { + Log.w(TAG, "There is already a pinned ActivityStack."); + return false; + } + + // Find a valid adjacent TaskFragmentContainer + final TaskFragmentContainer primaryContainer = + task.getNonFinishingTaskFragmentContainerBelow(topContainer); + if (primaryContainer == null) { + Log.w(TAG, "Cannot find another ActivityStack to split"); + return false; + } + + // Registers a Split + final SplitPinContainer splitPinContainer = new SplitPinContainer(primaryContainer, + topContainer, splitPinRule, splitPinRule.getDefaultSplitAttributes()); + task.addSplitContainer(splitPinContainer); + + // Updates the Split + final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(); + final WindowContainerTransaction wct = transactionRecord.getTransaction(); + mPresenter.updateSplitContainer(splitPinContainer, wct); + transactionRecord.apply(false /* shouldApplyIndependently */); + updateCallbackIfNecessary(); + return true; + } + } + + @Override + public void unpinTopActivityStack(int taskId){ + // TODO + } + + @Override public void setSplitAttributesCalculator( @NonNull Function<SplitAttributesCalculatorParams, SplitAttributes> calculator) { synchronized (mLock) { @@ -672,7 +722,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (targetContainer == null) { // When there is no embedding rule matched, try to place it in the top container // like a normal launch. - targetContainer = taskContainer.getTopTaskFragmentContainer(); + targetContainer = taskContainer.getTopNonFinishingTaskFragmentContainer(); } if (targetContainer == null) { return; @@ -791,7 +841,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen final TaskFragmentContainer container = getContainerWithActivity(activity); if (!isOnReparent && container != null - && container.getTaskContainer().getTopTaskFragmentContainer() != container) { + && container.getTaskContainer().getTopNonFinishingTaskFragmentContainer() + != container) { // Do not resolve if the launched activity is not the top-most container in the Task. return true; } @@ -888,7 +939,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (taskContainer == null) { return; } - final TaskFragmentContainer targetContainer = taskContainer.getTopTaskFragmentContainer(); + final TaskFragmentContainer targetContainer = + taskContainer.getTopNonFinishingTaskFragmentContainer(); if (targetContainer == null) { return; } @@ -1213,11 +1265,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // 3. Whether the top activity (if any) should be split with the new activity intent. final TaskContainer taskContainer = getTaskContainer(taskId); - if (taskContainer == null || taskContainer.getTopTaskFragmentContainer() == null) { + if (taskContainer == null + || taskContainer.getTopNonFinishingTaskFragmentContainer() == null) { // There is no other activity in the Task to check split with. return null; } - final TaskFragmentContainer topContainer = taskContainer.getTopTaskFragmentContainer(); + final TaskFragmentContainer topContainer = + taskContainer.getTopNonFinishingTaskFragmentContainer(); final Activity topActivity = topContainer.getTopNonFinishingActivity(); if (topActivity != null && topActivity != launchingActivity) { final TaskFragmentContainer container = getSecondaryContainerForSplitIfAny(wct, diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPinContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPinContainer.java new file mode 100644 index 000000000000..03c77a089012 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPinContainer.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.window.extensions.embedding; + +import androidx.annotation.NonNull; + +/** + * Client-side descriptor of a split that holds two containers while the secondary + * container is pinned on top of the Task and the primary container is the container that is + * currently below the secondary container. The primary container could be updated to + * another container whenever the existing primary container is removed or no longer + * be the container that's right behind the secondary container. + */ +class SplitPinContainer extends SplitContainer { + + SplitPinContainer(@NonNull TaskFragmentContainer primaryContainer, + @NonNull TaskFragmentContainer secondaryContainer, + @NonNull SplitPinRule splitPinRule, + @NonNull SplitAttributes splitAttributes) { + super(primaryContainer, primaryContainer.getTopNonFinishingActivity(), secondaryContainer, + splitPinRule, splitAttributes, true /* isPrimaryContainerMutable */); + } + + @Override + public String toString() { + return "SplitPinContainer{" + + " primaryContainer=" + getPrimaryContainer() + + " secondaryContainer=" + getSecondaryContainer() + + " splitPinRule=" + getSplitRule() + + " splitAttributes" + getCurrentSplitAttributes() + + "}"; + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index 53d39d9fa28e..6630e05a8b0a 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -336,10 +336,6 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { // value. final SplitRule rule = splitContainer.getSplitRule(); final TaskFragmentContainer primaryContainer = splitContainer.getPrimaryContainer(); - final Activity activity = primaryContainer.getTopNonFinishingActivity(); - if (activity == null) { - return; - } final TaskContainer taskContainer = splitContainer.getTaskContainer(); final TaskProperties taskProperties = taskContainer.getTaskProperties(); final SplitAttributes splitAttributes = splitContainer.getCurrentSplitAttributes(); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index 4580c9836168..822adbd84454 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -57,6 +57,10 @@ class TaskContainer { @NonNull private final List<SplitContainer> mSplitContainers = new ArrayList<>(); + /** Active pin split pair in this Task. */ + @Nullable + private SplitPinContainer mSplitPinContainer; + @NonNull private final Configuration mConfiguration; @@ -174,11 +178,28 @@ class TaskContainer { } @Nullable - TaskFragmentContainer getTopTaskFragmentContainer() { - if (mContainers.isEmpty()) { - return null; + TaskFragmentContainer getTopNonFinishingTaskFragmentContainer() { + for (int i = mContainers.size() - 1; i >= 0; i--) { + final TaskFragmentContainer container = mContainers.get(i); + if (!container.isFinished()) { + return container; + } + } + return null; + } + + /** Gets a non-finishing container below the given one. */ + @Nullable + TaskFragmentContainer getNonFinishingTaskFragmentContainerBelow( + @NonNull TaskFragmentContainer current) { + final int index = mContainers.indexOf(current); + for (int i = index - 1; i >= 0; i--) { + final TaskFragmentContainer container = mContainers.get(i); + if (!container.isFinished()) { + return container; + } } - return mContainers.get(mContainers.size() - 1); + return null; } @Nullable @@ -217,31 +238,57 @@ class TaskContainer { } void addSplitContainer(@NonNull SplitContainer splitContainer) { + if (splitContainer instanceof SplitPinContainer) { + mSplitPinContainer = (SplitPinContainer) splitContainer; + mSplitContainers.add(splitContainer); + return; + } + + // Keeps the SplitPinContainer on the top of the list. + mSplitContainers.remove(mSplitPinContainer); mSplitContainers.add(splitContainer); + if (mSplitPinContainer != null) { + mSplitContainers.add(mSplitPinContainer); + } } void removeSplitContainers(@NonNull List<SplitContainer> containers) { mSplitContainers.removeAll(containers); } + void removeSplitPinContainer() { + mSplitContainers.remove(mSplitPinContainer); + mSplitPinContainer = null; + } + + @Nullable + SplitPinContainer getSplitPinContainer() { + return mSplitPinContainer; + } + void addTaskFragmentContainer(@NonNull TaskFragmentContainer taskFragmentContainer) { mContainers.add(taskFragmentContainer); + onTaskFragmentContainerUpdated(); } void addTaskFragmentContainer(int index, @NonNull TaskFragmentContainer taskFragmentContainer) { mContainers.add(index, taskFragmentContainer); + onTaskFragmentContainerUpdated(); } void removeTaskFragmentContainer(@NonNull TaskFragmentContainer taskFragmentContainer) { mContainers.remove(taskFragmentContainer); + onTaskFragmentContainerUpdated(); } void removeTaskFragmentContainers(@NonNull List<TaskFragmentContainer> taskFragmentContainer) { mContainers.removeAll(taskFragmentContainer); + onTaskFragmentContainerUpdated(); } void clearTaskFragmentContainer() { mContainers.clear(); + onTaskFragmentContainerUpdated(); } /** @@ -254,6 +301,34 @@ class TaskContainer { return mContainers; } + private void onTaskFragmentContainerUpdated() { + if (mSplitPinContainer == null) { + return; + } + + final TaskFragmentContainer pinnedContainer = mSplitPinContainer.getSecondaryContainer(); + final int index = mContainers.indexOf(pinnedContainer); + if (index <= 0) { + removeSplitPinContainer(); + return; + } + + // Ensure the pinned container is top-most. + if (index != mContainers.size() - 1) { + mContainers.remove(pinnedContainer); + mContainers.add(pinnedContainer); + } + + // Update the primary container adjacent to the pinned container if needed. + final TaskFragmentContainer adjacentContainer = + getNonFinishingTaskFragmentContainerBelow(pinnedContainer); + if (adjacentContainer == null) { + removeSplitPinContainer(); + } else if (mSplitPinContainer.getPrimaryContainer() != adjacentContainer) { + mSplitPinContainer.setPrimaryContainer(adjacentContainer); + } + } + /** Adds the descriptors of split states in this Task to {@code outSplitStates}. */ void getSplitStates(@NonNull List<SplitInfo> outSplitStates) { for (SplitContainer container : mSplitContainers) { diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index 9e264726a65a..9af1fe916279 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -1462,6 +1462,51 @@ public class SplitControllerTest { verify(testRecord).apply(eq(false)); } + @Test + public void testPinTopActivityStack() { + // Create two activities. + final Activity primaryActivity = createMockActivity(); + final Activity secondaryActivity = createMockActivity(); + + // Unable to pin if not being embedded. + SplitPinRule splitPinRule = new SplitPinRule.Builder(new SplitAttributes.Builder().build(), + parentWindowMetrics -> true /* parentWindowMetricsPredicate */).build(); + assertFalse(mSplitController.pinTopActivityStack(TASK_ID, splitPinRule)); + + // Split the two activities. + addSplitTaskFragments(primaryActivity, secondaryActivity); + final TaskFragmentContainer primaryContainer = + mSplitController.getContainerWithActivity(primaryActivity); + spyOn(primaryContainer); + + // Unable to pin if no valid TaskFragment. + doReturn(true).when(primaryContainer).isFinished(); + assertFalse(mSplitController.pinTopActivityStack(TASK_ID, splitPinRule)); + + // Otherwise, should pin successfully. + doReturn(false).when(primaryContainer).isFinished(); + assertTrue(mSplitController.pinTopActivityStack(TASK_ID, splitPinRule)); + + // Unable to pin if there is already a pinned TaskFragment + assertFalse(mSplitController.pinTopActivityStack(TASK_ID, splitPinRule)); + + // Unable to pin on an unknown Task. + assertFalse(mSplitController.pinTopActivityStack(TASK_ID + 1, splitPinRule)); + + // Gets the current size of all the SplitContainers. + final TaskContainer taskContainer = mSplitController.getTaskContainer(TASK_ID); + final int splitContainerCount = taskContainer.getSplitContainers().size(); + + // Create another activity and split with primary activity. + final Activity thirdActivity = createMockActivity(); + addSplitTaskFragments(primaryActivity, thirdActivity); + + // Ensure another SplitContainer is added and the pinned TaskFragment still on top + assertTrue(taskContainer.getSplitContainers().size() == splitContainerCount + +1); + assertTrue(mSplitController.getTopActiveContainer(TASK_ID).getTopNonFinishingActivity() + == secondaryActivity); + } + /** Creates a mock activity in the organizer process. */ private Activity createMockActivity() { return createMockActivity(TASK_ID); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java index 11af1d1f20e1..000c65a75c81 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskContainerTest.java @@ -135,15 +135,15 @@ public class TaskContainerTest { @Test public void testGetTopTaskFragmentContainer() { final TaskContainer taskContainer = createTestTaskContainer(); - assertNull(taskContainer.getTopTaskFragmentContainer()); + assertNull(taskContainer.getTopNonFinishingTaskFragmentContainer()); final TaskFragmentContainer tf0 = new TaskFragmentContainer(null /* activity */, new Intent(), taskContainer, mController, null /* pairedPrimaryContainer */); - assertEquals(tf0, taskContainer.getTopTaskFragmentContainer()); + assertEquals(tf0, taskContainer.getTopNonFinishingTaskFragmentContainer()); final TaskFragmentContainer tf1 = new TaskFragmentContainer(null /* activity */, new Intent(), taskContainer, mController, null /* pairedPrimaryContainer */); - assertEquals(tf1, taskContainer.getTopTaskFragmentContainer()); + assertEquals(tf1, taskContainer.getTopNonFinishingTaskFragmentContainer()); } @Test |