[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
This commit is contained in:
Louis Chang
2023-04-17 10:03:33 +00:00
parent b5485855d9
commit 4944e8e071
8 changed files with 260 additions and 18 deletions

View File

@@ -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

View File

@@ -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;

View File

@@ -212,6 +212,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) {
@@ -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,

View File

@@ -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()
+ "}";
}
}

View File

@@ -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();

View File

@@ -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 mContainers.get(mContainers.size() - 1);
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 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) {

View File

@@ -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);

View File

@@ -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