Merge "[3/n] Pin ActivityStack" into udc-qpr-dev

This commit is contained in:
Louis Chang
2023-07-13 00:23:31 +00:00
committed by Android (Google) Code Review
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