Merge changes I27335d47,I54cd86d2 into sc-v2-dev

* changes:
  Split activity organizer in WM Extensions
  Add base container classes for reference Jetpack organizer
This commit is contained in:
TreeHugger Robot
2021-07-21 07:22:35 +00:00
committed by Android (Google) Code Review
7 changed files with 1185 additions and 7 deletions

View File

@@ -3552,6 +3552,13 @@ public final class ActivityThread extends ClientTransactionHandler
+ ", comp=" + r.intent.getComponent().toShortString()
+ ", dir=" + r.packageInfo.getAppDir());
// updatePendingActivityConfiguration() reads from mActivities to update
// ActivityClientRecord which runs in a different thread. Protect modifications to
// mActivities to avoid race.
synchronized (mResourcesManager) {
mActivities.put(r.token, r);
}
if (activity != null) {
CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
Configuration config =
@@ -3613,13 +3620,6 @@ public final class ActivityThread extends ClientTransactionHandler
}
r.setState(ON_CREATE);
// updatePendingActivityConfiguration() reads from mActivities to update
// ActivityClientRecord which runs in a different thread. Protect modifications to
// mActivities to avoid race.
synchronized (mResourcesManager) {
mActivities.put(r.token, r);
}
} catch (SuperNotCalledException e) {
throw e;

View File

@@ -23,15 +23,19 @@ import static androidx.window.util.ExtensionHelper.transformToWindowSpaceRect;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Rect;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.window.common.DeviceStateManagerPostureProducer;
import androidx.window.common.DisplayFeature;
import androidx.window.common.ResourceConfigDisplayFeatureProducer;
import androidx.window.common.SettingsDevicePostureProducer;
import androidx.window.common.SettingsDisplayFeatureProducer;
import androidx.window.extensions.organizer.SplitController;
import androidx.window.util.DataProducer;
import androidx.window.util.PriorityDataProducer;
@@ -56,6 +60,8 @@ class SampleExtensionImpl extends StubExtension {
private final SettingsDisplayFeatureProducer mSettingsDisplayFeatureProducer;
private final DataProducer<List<DisplayFeature>> mDisplayFeatureProducer;
private final SplitController mSplitController;
SampleExtensionImpl(Context context) {
mSettingsDevicePostureProducer = new SettingsDevicePostureProducer(context);
mDevicePostureProducer = new PriorityDataProducer<>(List.of(
@@ -71,6 +77,8 @@ class SampleExtensionImpl extends StubExtension {
mDevicePostureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged);
mDisplayFeatureProducer.addDataChangedCallback(this::onDisplayFeaturesChanged);
mSplitController = new SplitController();
}
private int getFeatureState(DisplayFeature feature) {
@@ -134,4 +142,28 @@ class SampleExtensionImpl extends StubExtension {
onDisplayFeaturesChanged();
}
@Override
public void setSplitRules(@NonNull List<ExtensionSplitRule> splitRules) {
mSplitController.setSplitRules(splitRules);
}
@Override
@NonNull
public List<ExtensionSplitRule> getSplitRules() {
return new ArrayList<>(mSplitController.getSplitRules());
}
@Override
public void setSplitOrganizerCallback(@Nullable SplitOrganizerCallback callback) {
mSplitController.setSplitOrganizerCallback(callback);
}
@Override
public void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent intent,
@Nullable Bundle options, @NonNull ExtensionSplitPairRule splitPairRule,
int startRequestId) {
mSplitController.startActivityToSide(launchingActivity, intent, options, splitPairRule,
startRequestId);
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright (C) 2021 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.organizer;
import android.annotation.NonNull;
import android.app.Activity;
import androidx.window.extensions.ExtensionSplitPairRule;
/**
* Client-side descriptor of a split that holds two containers.
*/
class SplitContainer {
private final TaskFragmentContainer mPrimaryContainer;
private final TaskFragmentContainer mSecondaryContainer;
private final ExtensionSplitPairRule mSplitPairRule;
SplitContainer(@NonNull TaskFragmentContainer primaryContainer,
@NonNull Activity primaryActivity,
@NonNull TaskFragmentContainer secondaryContainer,
@NonNull ExtensionSplitPairRule splitPairRule) {
mPrimaryContainer = primaryContainer;
mSecondaryContainer = secondaryContainer;
mSplitPairRule = splitPairRule;
if (mSplitPairRule.finishPrimaryWithSecondary || mSplitPairRule.useAsPlaceholder) {
mSecondaryContainer.addActivityToFinishOnExit(primaryActivity);
}
if (mSplitPairRule.finishSecondaryWithPrimary || mSplitPairRule.useAsPlaceholder) {
mPrimaryContainer.addContainerToFinishOnExit(mSecondaryContainer);
}
}
@NonNull
TaskFragmentContainer getPrimaryContainer() {
return mPrimaryContainer;
}
@NonNull
TaskFragmentContainer getSecondaryContainer() {
return mSecondaryContainer;
}
@NonNull
ExtensionSplitPairRule getSplitPairRule() {
return mSplitPairRule;
}
}

View File

@@ -0,0 +1,580 @@
/*
* Copyright (C) 2021 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.organizer;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityClient;
import android.app.ActivityThread;
import android.app.Application.ActivityLifecycleCallbacks;
import android.content.ComponentName;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.IBinder;
import android.window.TaskFragmentAppearedInfo;
import android.window.TaskFragmentInfo;
import android.window.WindowContainerTransaction;
import androidx.window.extensions.ExtensionInterface.SplitOrganizerCallback;
import androidx.window.extensions.ExtensionSplitActivityRule;
import androidx.window.extensions.ExtensionSplitInfo;
import androidx.window.extensions.ExtensionSplitPairRule;
import androidx.window.extensions.ExtensionSplitRule;
import androidx.window.extensions.ExtensionTaskFragment;
import java.util.ArrayList;
import java.util.List;
/**
* Main controller class that manages split states and presentation.
*/
public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback {
private final SplitPresenter mPresenter;
// Currently applied split configuration.
private final List<ExtensionSplitRule> mSplitRules = new ArrayList<>();
private final List<TaskFragmentContainer> mContainers = new ArrayList<>();
private final List<SplitContainer> mSplitContainers = new ArrayList<>();
// Callback to Jetpack to notify about changes to split states.
private SplitOrganizerCallback mSplitOrganizerCallback;
public SplitController() {
mPresenter = new SplitPresenter(ActivityThread.currentActivityThread().getExecutor(),
this);
// Register a callback to be notified about activities being created.
ActivityThread.currentActivityThread().getApplication().registerActivityLifecycleCallbacks(
new LifecycleCallbacks());
}
public void setSplitRules(@NonNull List<ExtensionSplitRule> splitRules) {
mSplitRules.clear();
mSplitRules.addAll(splitRules);
}
@NonNull
public List<ExtensionSplitRule> getSplitRules() {
return mSplitRules;
}
/**
* Starts an activity to side of the launchingActivity with the provided split config.
*/
public void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent intent,
@Nullable Bundle options, @NonNull ExtensionSplitPairRule splitPairRule,
int startRequestId) {
try {
mPresenter.startActivityToSide(launchingActivity, intent, options, splitPairRule);
} catch (Exception e) {
if (mSplitOrganizerCallback != null && startRequestId != -1) {
mSplitOrganizerCallback.onActivityFailedToStartInContainer(startRequestId, e);
}
}
}
/**
* Registers the split organizer callback to notify about changes to active splits.
*/
public void setSplitOrganizerCallback(@NonNull SplitOrganizerCallback callback) {
mSplitOrganizerCallback = callback;
updateCallbackIfNecessary();
}
@Override
public void onTaskFragmentAppeared(@NonNull TaskFragmentAppearedInfo taskFragmentAppearedInfo) {
for (TaskFragmentContainer container : mContainers) {
if (container.getTaskFragmentToken().equals(
taskFragmentAppearedInfo.getTaskFragmentInfo().getFragmentToken())) {
container.setInfo(taskFragmentAppearedInfo.getTaskFragmentInfo());
return;
}
}
}
@Override
public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) {
for (TaskFragmentContainer container : mContainers) {
if (container.getTaskFragmentToken().equals(taskFragmentInfo.getFragmentToken())) {
container.setInfo(taskFragmentInfo);
if (taskFragmentInfo.isEmpty()) {
cleanupContainer(container, true /* shouldFinishDependent */);
updateCallbackIfNecessary();
}
return;
}
}
}
@Override
public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) {
for (TaskFragmentContainer container : mContainers) {
if (container.getTaskFragmentToken().equals(taskFragmentInfo.getFragmentToken())) {
cleanupContainer(container, true /* shouldFinishDependent */);
updateCallbackIfNecessary();
return;
}
}
}
@Override
public void onTaskFragmentParentInfoChanged(@NonNull IBinder fragmentToken,
@NonNull Configuration parentConfig) {
TaskFragmentContainer container = getContainer(fragmentToken);
if (container != null) {
mPresenter.updateContainer(container);
updateCallbackIfNecessary();
}
}
/**
* Checks if the activity start should be routed to a particular container. It can create a new
* container for the activity and a new split container if necessary.
*/
void onActivityCreated(@NonNull Activity launchedActivity) {
final ComponentName componentName = launchedActivity.getComponentName();
final List<ExtensionSplitRule> splitRules = getSplitRules();
final TaskFragmentContainer currentContainer = getContainerWithActivity(
launchedActivity.getActivityToken());
// Check if the activity is configured to always be expanded.
if (shouldExpand(componentName, splitRules)) {
if (shouldContainerBeExpanded(currentContainer)) {
// Make sure that the existing container is expanded
mPresenter.expandTaskFragment(currentContainer.getTaskFragmentToken());
} else {
// Put activity into a new expanded container
final TaskFragmentContainer newContainer = newContainer(launchedActivity);
mPresenter.expandActivity(newContainer.getTaskFragmentToken(),
launchedActivity);
}
return;
}
// Check if activity requires a placeholder
if (launchPlaceholderIfNecessary(launchedActivity)) {
return;
}
// TODO(b/190433398): Check if it is a placeholder and there is already another split
// created by the primary activity. This is necessary for the case when the primary activity
// launched another secondary in the split, but the placeholder was still launched by the
// logic above. We didn't prevent the placeholder launcher because we didn't know that
// another secondary activity is coming up.
// Check if the activity should form a split with the activity below in the same task
// fragment.
Activity activityBelow = null;
if (currentContainer != null) {
final List<Activity> containerActivities = currentContainer.collectActivities();
final int index = containerActivities.indexOf(launchedActivity);
if (index > 0) {
activityBelow = containerActivities.get(index - 1);
}
}
if (activityBelow == null) {
IBinder belowToken = ActivityClient.getInstance().getActivityTokenBelow(
launchedActivity.getActivityToken());
if (belowToken != null) {
activityBelow = ActivityThread.currentActivityThread().getActivity(belowToken);
}
}
if (activityBelow == null) {
return;
}
final ExtensionSplitPairRule splitPairRule = getSplitRule(
activityBelow.getComponentName(), componentName, splitRules);
if (splitPairRule == null) {
return;
}
mPresenter.createNewSplitContainer(activityBelow, launchedActivity,
splitPairRule);
updateCallbackIfNecessary();
}
/**
* Returns a container that this activity is registered with. An activity can only belong to one
* container, or no container at all.
*/
@Nullable
TaskFragmentContainer getContainerWithActivity(@NonNull IBinder activityToken) {
for (TaskFragmentContainer container : mContainers) {
if (container.hasActivity(activityToken)) {
return container;
}
}
return null;
}
/**
* Creates and registers a new organized container with an optional activity that will be
* re-parented to it in a WCT.
*/
TaskFragmentContainer newContainer(@Nullable Activity activity) {
TaskFragmentContainer container = new TaskFragmentContainer(activity);
mContainers.add(container);
return container;
}
/**
* Creates and registers a new split with the provided containers and configuration.
*/
void registerSplit(@NonNull TaskFragmentContainer primaryContainer,
@NonNull Activity primaryActivity,
@NonNull TaskFragmentContainer secondaryContainer,
@NonNull ExtensionSplitPairRule splitPairRule) {
SplitContainer splitContainer = new SplitContainer(primaryContainer, primaryActivity,
secondaryContainer, splitPairRule);
mSplitContainers.add(splitContainer);
}
void cleanupContainer(@NonNull TaskFragmentContainer container, boolean shouldFinishDependent) {
if (container.isFinished()) {
return;
}
container.finish(shouldFinishDependent);
// Remove all split containers that included this one
mContainers.remove(container);
List<SplitContainer> containersToRemove = new ArrayList<>();
for (SplitContainer splitContainer : mSplitContainers) {
if (container.equals(splitContainer.getSecondaryContainer())
|| container.equals(splitContainer.getPrimaryContainer())) {
containersToRemove.add(splitContainer);
}
}
mSplitContainers.removeAll(containersToRemove);
mPresenter.deleteContainer(container);
}
/**
* Returns the topmost not finished container.
*/
@Nullable
TaskFragmentContainer getTopActiveContainer() {
for (int i = mContainers.size() - 1; i >= 0; i--) {
TaskFragmentContainer container = mContainers.get(i);
if (!container.isFinished()) {
return container;
}
}
return null;
}
/**
* Updates the presentation of the container. If the container is part of the split or should
* have a placeholder, it will also update the other part of the split.
*/
void updateContainer(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentContainer container) {
if (launchPlaceholderIfNecessary(container)) {
// Placeholder was launched, the positions will be updated when the activity is added
// to the secondary container.
return;
}
if (shouldContainerBeExpanded(container)) {
if (container.getInfo() != null) {
mPresenter.expandTaskFragment(wct, container.getTaskFragmentToken());
}
// If the info is not available yet the task fragment will be expanded when it's ready
return;
}
SplitContainer splitContainer = getActiveSplitForContainer(container);
if (splitContainer == null) {
return;
}
if (splitContainer != mSplitContainers.get(mSplitContainers.size() - 1)) {
// Skip position update - it isn't the topmost split.
return;
}
if (splitContainer.getPrimaryContainer().isEmpty()
|| splitContainer.getSecondaryContainer().isEmpty()) {
// Skip position update - one or both containers are empty.
return;
}
if (dismissPlaceholderIfNecessary(splitContainer)) {
// Placeholder was finished, the positions will be updated when its container is emptied
return;
}
mPresenter.updateSplitContainer(splitContainer, container, wct);
}
/**
* Returns the top active split container that has the provided container.
*/
@Nullable
private SplitContainer getActiveSplitForContainer(@NonNull TaskFragmentContainer container) {
for (int i = mSplitContainers.size() - 1; i >= 0; i--) {
SplitContainer splitContainer = mSplitContainers.get(i);
if (container.equals(splitContainer.getSecondaryContainer())
|| container.equals(splitContainer.getPrimaryContainer())) {
return splitContainer;
}
}
return null;
}
/**
* Checks if the container requires a placeholder and launches it if necessary.
*/
private boolean launchPlaceholderIfNecessary(@NonNull TaskFragmentContainer container) {
final Activity topActivity = container.getTopNonFinishingActivity();
if (topActivity == null) {
return false;
}
return launchPlaceholderIfNecessary(topActivity);
}
boolean launchPlaceholderIfNecessary(@NonNull Activity activity) {
final TaskFragmentContainer container = getContainerWithActivity(
activity.getActivityToken());
SplitContainer splitContainer = container != null ? getActiveSplitForContainer(container)
: null;
if (splitContainer != null && container.equals(splitContainer.getPrimaryContainer())) {
// Don't launch placeholder in primary split container
return false;
}
// Check if there is enough space for launch
final ExtensionSplitPairRule placeholderRule = getPlaceholderRule(
activity.getComponentName());
if (placeholderRule == null || !mPresenter.shouldShowSideBySide(
mPresenter.getParentContainerBounds(activity), placeholderRule)) {
return false;
}
Intent placeholderIntent = new Intent();
placeholderIntent.setComponent(placeholderRule.secondaryActivityName);
// TODO(b/190433398): Handle failed request
startActivityToSide(activity, placeholderIntent, null, placeholderRule, -1);
return true;
}
private boolean dismissPlaceholderIfNecessary(@NonNull SplitContainer splitContainer) {
if (!splitContainer.getSplitPairRule().useAsPlaceholder) {
return false;
}
if (mPresenter.shouldShowSideBySide(splitContainer)) {
return false;
}
cleanupContainer(splitContainer.getSecondaryContainer(),
false /* shouldFinishDependent */);
return true;
}
/**
* Returns the rule to launch a placeholder for the activity with the provided component name
* if it is configured in the split config.
*/
private ExtensionSplitPairRule getPlaceholderRule(@NonNull ComponentName componentName) {
for (ExtensionSplitRule rule : mSplitRules) {
if (!(rule instanceof ExtensionSplitPairRule)) {
continue;
}
ExtensionSplitPairRule pairRule = (ExtensionSplitPairRule) rule;
if (componentName.equals(pairRule.primaryActivityName)
&& pairRule.useAsPlaceholder) {
return pairRule;
}
}
return null;
}
/**
* Notifies listeners about changes to split states if necessary.
*/
private void updateCallbackIfNecessary() {
if (mSplitOrganizerCallback == null) {
return;
}
// TODO(b/190433398): Check if something actually changed
mSplitOrganizerCallback.onSplitInfoChanged(getActiveSplitStates());
}
/**
* Returns a list of descriptors for currently active split states.
*/
private List<ExtensionSplitInfo> getActiveSplitStates() {
List<ExtensionSplitInfo> splitStates = new ArrayList<>();
for (SplitContainer container : mSplitContainers) {
ExtensionTaskFragment primaryContainer =
new ExtensionTaskFragment(
container.getPrimaryContainer().collectActivities());
ExtensionTaskFragment secondaryContainer =
new ExtensionTaskFragment(
container.getSecondaryContainer().collectActivities());
ExtensionSplitInfo splitState = new ExtensionSplitInfo(primaryContainer,
secondaryContainer, container.getSplitPairRule().splitRatio);
splitStates.add(splitState);
}
return splitStates;
}
/**
* Returns {@code true} if the container is expanded to occupy full task size.
* Returns {@code false} if the container is included in an active split.
*/
boolean shouldContainerBeExpanded(@Nullable TaskFragmentContainer container) {
if (container == null) {
return false;
}
for (SplitContainer splitContainer : mSplitContainers) {
if (container.equals(splitContainer.getPrimaryContainer())
|| container.equals(splitContainer.getSecondaryContainer())) {
return false;
}
}
return true;
}
/**
* Returns a split rule for the provided pair of component names if available.
*/
@Nullable
private static ExtensionSplitPairRule getSplitRule(@NonNull ComponentName primaryActivityName,
@NonNull ComponentName secondaryActivityName,
@NonNull List<ExtensionSplitRule> splitRules) {
if (splitRules == null || primaryActivityName == null || secondaryActivityName == null) {
return null;
}
for (ExtensionSplitRule rule : splitRules) {
if (!(rule instanceof ExtensionSplitPairRule)) {
continue;
}
ExtensionSplitPairRule pairRule = (ExtensionSplitPairRule) rule;
if (match(secondaryActivityName, pairRule.secondaryActivityName)
&& match(primaryActivityName, pairRule.primaryActivityName)) {
return pairRule;
}
}
return null;
}
@Nullable
private TaskFragmentContainer getContainer(@NonNull IBinder fragmentToken) {
for (TaskFragmentContainer container : mContainers) {
if (container.getTaskFragmentToken().equals(fragmentToken)) {
return container;
}
}
return null;
}
/**
* Returns {@code true} if an Activity with the provided component name should always be
* expanded to occupy full task bounds. Such activity must not be put in a split.
*/
private static boolean shouldExpand(@NonNull ComponentName componentName,
List<ExtensionSplitRule> splitRules) {
if (splitRules == null) {
return false;
}
for (ExtensionSplitRule rule : splitRules) {
if (!(rule instanceof ExtensionSplitActivityRule)) {
continue;
}
ExtensionSplitActivityRule activityRule = (ExtensionSplitActivityRule) rule;
if (match(componentName, activityRule.activityName)
&& activityRule.alwaysExpand) {
return true;
}
}
return false;
}
/** Match check allowing wildcards for activity class name but not package name. */
private static boolean match(@NonNull ComponentName activityComponent,
@NonNull ComponentName ruleComponent) {
if (activityComponent.toString().contains("*")) {
throw new IllegalArgumentException("Wildcard can only be part of the rule.");
}
final boolean packagesMatch =
activityComponent.getPackageName().equals(ruleComponent.getPackageName());
final boolean classesMatch =
activityComponent.getClassName().equals(ruleComponent.getClassName());
return packagesMatch && (classesMatch
|| wildcardMatch(activityComponent.getClassName(), ruleComponent.getClassName()));
}
/**
* Checks if the provided name matches the pattern.
*/
private static boolean wildcardMatch(@NonNull String name, @NonNull String pattern) {
if (!pattern.contains("*")) {
return false;
}
if (pattern.equals("*")) {
return true;
}
if (pattern.indexOf("*") != pattern.lastIndexOf("*") || !pattern.endsWith("*")) {
throw new IllegalArgumentException(
"Name pattern with a wildcard must only contain a single * in the end");
}
return name.startsWith(pattern.substring(0, pattern.length() - 1));
}
private final class LifecycleCallbacks implements ActivityLifecycleCallbacks {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
// Calling after Activity#onCreate is complete to allow the app launch something
// first. In case of a configured placeholder activity we want to make sure
// that we don't launch it if an activity itself already requested something to be
// launched to side.
SplitController.this.onActivityCreated(activity);
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
}
}

View File

@@ -0,0 +1,302 @@
/*
* Copyright (C) 2021 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.organizer;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Bundle;
import android.window.TaskFragmentCreationParams;
import android.window.WindowContainerTransaction;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.window.extensions.ExtensionSplitPairRule;
import java.util.concurrent.Executor;
/**
* Controls the visual presentation of the splits according to the containers formed by
* {@link SplitController}.
*/
class SplitPresenter extends JetpackTaskFragmentOrganizer {
private static final int POSITION_LEFT = 0;
private static final int POSITION_RIGHT = 1;
private static final int POSITION_FILL = 2;
@IntDef(value = {
POSITION_LEFT,
POSITION_RIGHT,
POSITION_FILL,
})
private @interface Position {}
private final SplitController mController;
SplitPresenter(@NonNull Executor executor, SplitController controller) {
super(executor, controller);
mController = controller;
registerOrganizer();
}
/**
* Updates the presentation of the provided container.
*/
void updateContainer(TaskFragmentContainer container) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
mController.updateContainer(wct, container);
applyTransaction(wct);
}
/**
* Deletes the provided container and updates the presentation if necessary.
*/
void deleteContainer(TaskFragmentContainer container) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
deleteTaskFragment(wct, container.getTaskFragmentToken());
final TaskFragmentContainer newTopContainer = mController.getTopActiveContainer();
if (newTopContainer != null) {
mController.updateContainer(wct, newTopContainer);
}
applyTransaction(wct);
}
/**
* Creates a new split container with the two provided activities.
* @param primaryActivity An activity that should be in the primary container. If it is not
* currently in an existing container, a new one will be created and the
* activity will be re-parented to it.
* @param secondaryActivity An activity that should be in the secondary container. If it is not
* currently in an existing container, or if it is currently in the
* same container as the primary activity, a new container will be
* created and the activity will be re-parented to it.
* @param rule The split rule to be applied to the container.
*/
void createNewSplitContainer(@NonNull Activity primaryActivity,
@NonNull Activity secondaryActivity, @NonNull ExtensionSplitPairRule rule) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
final Rect parentBounds = getParentContainerBounds(primaryActivity);
final Rect primaryRectBounds = getBoundsForPosition(POSITION_LEFT, parentBounds, rule);
final Rect secondaryRectBounds = getBoundsForPosition(POSITION_RIGHT, parentBounds, rule);
TaskFragmentContainer primaryContainer = mController.getContainerWithActivity(
primaryActivity.getActivityToken());
if (primaryContainer == null) {
primaryContainer = mController.newContainer(primaryActivity);
final TaskFragmentCreationParams fragmentOptions =
createFragmentOptions(
primaryContainer.getTaskFragmentToken(),
primaryActivity.getActivityToken(),
primaryRectBounds,
WINDOWING_MODE_MULTI_WINDOW);
wct.createTaskFragment(fragmentOptions);
wct.reparentActivityToTaskFragment(primaryContainer.getTaskFragmentToken(),
primaryActivity.getActivityToken());
} else {
resizeTaskFragmentIfRegistered(wct, primaryContainer, primaryRectBounds);
}
TaskFragmentContainer secondaryContainer = mController.getContainerWithActivity(
secondaryActivity.getActivityToken());
if (secondaryContainer == null || secondaryContainer == primaryContainer) {
secondaryContainer = mController.newContainer(secondaryActivity);
final TaskFragmentCreationParams fragmentOptions =
createFragmentOptions(
secondaryContainer.getTaskFragmentToken(),
secondaryActivity.getActivityToken(),
secondaryRectBounds,
WINDOWING_MODE_MULTI_WINDOW);
wct.createTaskFragment(fragmentOptions);
wct.reparentActivityToTaskFragment(secondaryContainer.getTaskFragmentToken(),
secondaryActivity.getActivityToken());
} else {
resizeTaskFragmentIfRegistered(wct, secondaryContainer, secondaryRectBounds);
}
// TODO(b/190433398): The primary container and the secondary container should also be set
// as adjacent (WCT#setAdjacentRoots) to make activities behind invisible.
applyTransaction(wct);
mController.registerSplit(primaryContainer, primaryActivity, secondaryContainer, rule);
}
/**
* Starts a new activity to the side, creating a new split container. A new container will be
* created for the activity that will be started.
* @param launchingActivity An activity that should be in the primary container. If it is not
* currently in an existing container, a new one will be created and
* the activity will be re-parented to it.
* @param activityIntent The intent to start the new activity.
* @param activityOptions The options to apply to new activity start.
* @param rule The split rule to be applied to the container.
*/
void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent activityIntent,
@Nullable Bundle activityOptions, @NonNull ExtensionSplitPairRule rule) {
final Rect parentBounds = getParentContainerBounds(launchingActivity);
final Rect primaryRectBounds = getBoundsForPosition(POSITION_LEFT, parentBounds, rule);
final Rect secondaryRectBounds = getBoundsForPosition(POSITION_RIGHT, parentBounds, rule);
TaskFragmentContainer primaryContainer = mController.getContainerWithActivity(
launchingActivity.getActivityToken());
if (primaryContainer == null) {
primaryContainer = mController.newContainer(launchingActivity);
}
TaskFragmentContainer secondaryContainer = mController.newContainer(null);
startActivityToSide(
primaryContainer.getTaskFragmentToken(),
primaryRectBounds,
launchingActivity,
secondaryContainer.getTaskFragmentToken(),
secondaryRectBounds,
activityIntent,
activityOptions);
// TODO(b/190433398): The primary container and the secondary container should also be set
// as adjacent (WCT#setAdjacentRoots) to make activities behind invisible.
mController.registerSplit(primaryContainer, launchingActivity, secondaryContainer,
rule);
}
/**
* Updates the positions of containers in an existing split.
* @param splitContainer The split container to be updated.
* @param updatedContainer The task fragment that was updated and caused this split update.
* @param wct WindowContainerTransaction that this update should be performed with.
*/
void updateSplitContainer(@NonNull SplitContainer splitContainer,
@NonNull TaskFragmentContainer updatedContainer,
@NonNull WindowContainerTransaction wct) {
// Getting the parent bounds using the updated container - it will have the recent value.
final Rect parentBounds = getParentContainerBounds(updatedContainer);
final ExtensionSplitPairRule rule = splitContainer.getSplitPairRule();
final Rect primaryRectBounds = getBoundsForPosition(POSITION_LEFT, parentBounds, rule);
final Rect secondaryRectBounds = getBoundsForPosition(POSITION_RIGHT, parentBounds, rule);
// TODO(b/190433398): Check if the bounds actually changed.
// If the task fragments are not registered yet, the positions will be updated after they
// are created again.
resizeTaskFragmentIfRegistered(wct, splitContainer.getPrimaryContainer(),
primaryRectBounds);
resizeTaskFragmentIfRegistered(wct, splitContainer.getSecondaryContainer(),
secondaryRectBounds);
}
/**
* Resizes the task fragment if it was already registered. Skips the operation if the container
* creation has not been reported from the server yet.
*/
// TODO(b/190433398): Handle resize if the fragment hasn't appeared yet.
void resizeTaskFragmentIfRegistered(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentContainer container,
@Nullable Rect bounds) {
if (container.getInfo() == null) {
return;
}
// TODO(b/190433398): Check if the bounds actually changed.
resizeTaskFragment(wct, container.getTaskFragmentToken(), bounds);
}
boolean shouldShowSideBySide(@NonNull SplitContainer splitContainer) {
final Rect parentBounds = getParentContainerBounds(splitContainer.getPrimaryContainer());
return shouldShowSideBySide(parentBounds, splitContainer.getSplitPairRule());
}
boolean shouldShowSideBySide(@Nullable Rect parentBounds,
@NonNull ExtensionSplitPairRule rule) {
return parentBounds != null && parentBounds.width() >= rule.minWidth
// TODO(b/190433398): Consider proper smallest width computation.
&& Math.min(parentBounds.width(), parentBounds.height()) >= rule.minSmallestWidth;
}
@NonNull
private Rect getBoundsForPosition(@Position int position, @NonNull Rect parentBounds,
@NonNull ExtensionSplitPairRule rule) {
if (!shouldShowSideBySide(parentBounds, rule)) {
return new Rect();
}
float splitRatio = rule.splitRatio;
switch (position) {
case POSITION_LEFT:
return new Rect(
parentBounds.left,
parentBounds.top,
(int) (parentBounds.left + parentBounds.width() * splitRatio),
parentBounds.bottom);
case POSITION_RIGHT:
return new Rect(
(int) (parentBounds.left + parentBounds.width() * splitRatio),
parentBounds.top,
parentBounds.right,
parentBounds.bottom);
case POSITION_FILL:
return parentBounds;
}
return parentBounds;
}
@NonNull
Rect getParentContainerBounds(@NonNull TaskFragmentContainer container) {
final Configuration parentConfig = mFragmentParentConfigs.get(
container.getTaskFragmentToken());
if (parentConfig != null) {
return parentConfig.windowConfiguration.getBounds();
}
// If there is no parent yet - then assuming that activities are running in full task bounds
final Activity topActivity = container.getTopNonFinishingActivity();
final Rect bounds = topActivity != null ? getParentContainerBounds(topActivity) : null;
if (bounds == null) {
throw new IllegalStateException("Unknown parent bounds");
}
return bounds;
}
@NonNull
Rect getParentContainerBounds(@NonNull Activity activity) {
final TaskFragmentContainer container = mController.getContainerWithActivity(
activity.getActivityToken());
if (container != null) {
final Configuration parentConfig = mFragmentParentConfigs.get(
container.getTaskFragmentToken());
if (parentConfig != null) {
return parentConfig.windowConfiguration.getBounds();
}
}
// TODO(b/190433398): Check if the client-side available info about parent bounds is enough.
if (!activity.isInMultiWindowMode()) {
// In fullscreen mode the max bounds should correspond to the task bounds.
return activity.getResources().getConfiguration().windowConfiguration.getMaxBounds();
}
return activity.getResources().getConfiguration().windowConfiguration.getBounds();
}
}

View File

@@ -0,0 +1,202 @@
/*
* Copyright (C) 2021 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.organizer;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityThread;
import android.os.Binder;
import android.os.IBinder;
import android.window.TaskFragmentInfo;
import java.util.ArrayList;
import java.util.List;
/**
* Client-side container for a stack of activities. Corresponds to an instance of TaskFragment
* on the server side.
*/
class TaskFragmentContainer {
/**
* Client-created token that uniquely identifies the task fragment container instance.
*/
@NonNull
private final IBinder mToken;
/**
* Server-provided task fragment information.
*/
private TaskFragmentInfo mInfo;
/**
* Activity that is being reparented to this container, but haven't been added to {@link #mInfo}
* yet.
*/
private Activity mReparentingActivity;
/** Containers that are dependent on this one and should be completely destroyed on exit. */
private final List<TaskFragmentContainer> mContainersToFinishOnExit =
new ArrayList<>();
/** Individual associated activities in different containers that should be finished on exit. */
private final List<Activity> mActivitiesToFinishOnExit = new ArrayList<>();
/** Indicates whether the container was cleaned up after the last activity was removed. */
private boolean mIsFinished;
/**
* Creates a container with an existing activity that will be re-parented to it in a window
* container transaction.
*/
TaskFragmentContainer(@Nullable Activity activity) {
mToken = new Binder("TaskFragmentContainer");
mReparentingActivity = activity;
}
/**
* Returns the client-created token that uniquely identifies this container.
*/
@NonNull
IBinder getTaskFragmentToken() {
return mToken;
}
/** List of activities that belong to this container and live in this process. */
@NonNull
List<Activity> collectActivities() {
// Add the re-parenting activity, in case the server has not yet reported the task
// fragment info update with it placed in this container. We still want to apply rules
// in this intermediate state.
List<Activity> allActivities = new ArrayList<>();
if (mReparentingActivity != null) {
allActivities.add(mReparentingActivity);
}
// Add activities reported from the server.
if (mInfo == null) {
return allActivities;
}
ActivityThread activityThread = ActivityThread.currentActivityThread();
for (IBinder token : mInfo.getActivities()) {
Activity activity = activityThread.getActivity(token);
if (activity != null && !allActivities.contains(activity)) {
allActivities.add(activity);
}
}
return allActivities;
}
boolean hasActivity(@NonNull IBinder token) {
if (mInfo != null && mInfo.getActivities().contains(token)) {
return true;
}
return mReparentingActivity != null
&& mReparentingActivity.getActivityToken().equals(token);
}
@Nullable
TaskFragmentInfo getInfo() {
return mInfo;
}
void setInfo(@Nullable TaskFragmentInfo info) {
mInfo = info;
if (mInfo == null || mReparentingActivity == null) {
return;
}
// Cleanup activities that were being re-parented
for (IBinder activityToken : mInfo.getActivities()) {
if (mReparentingActivity.getActivityToken().equals(activityToken)) {
mReparentingActivity = null;
break;
}
}
}
@Nullable
Activity getTopNonFinishingActivity() {
List<Activity> activities = collectActivities();
if (activities.isEmpty()) {
return null;
}
int i = activities.size() - 1;
while (i >= 0 && activities.get(i).isFinishing()) {
i--;
}
return i >= 0 ? activities.get(i) : null;
}
boolean isEmpty() {
return mReparentingActivity == null && (mInfo == null || mInfo.isEmpty());
}
/**
* Adds a container that should be finished when this container is finished.
*/
void addContainerToFinishOnExit(@NonNull TaskFragmentContainer containerToFinish) {
mContainersToFinishOnExit.add(containerToFinish);
}
/**
* Adds an activity that should be finished when this container is finished.
*/
void addActivityToFinishOnExit(@NonNull Activity activityToFinish) {
mActivitiesToFinishOnExit.add(activityToFinish);
}
/**
* Removes all activities that belong to this process and finishes other containers/activities
* configured to finish together.
*/
void finish(boolean shouldFinishDependent) {
if (mIsFinished) {
return;
}
mIsFinished = true;
// Finish own activities
for (Activity activity : collectActivities()) {
activity.finish();
}
if (!shouldFinishDependent) {
return;
}
// Finish dependent containers
for (TaskFragmentContainer container : mContainersToFinishOnExit) {
container.finish(true /* shouldFinishDependent */);
}
mContainersToFinishOnExit.clear();
// Finish associated activities
for (Activity activity : mActivitiesToFinishOnExit) {
activity.finish();
}
mActivitiesToFinishOnExit.clear();
// Finish activities that were being re-parented to this container.
if (mReparentingActivity != null) {
mReparentingActivity.finish();
mReparentingActivity = null;
}
}
boolean isFinished() {
return mIsFinished;
}
}