[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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
+ "}";
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user