Split activity organizer in WM Extensions
Reference implementation for the client-side organizer for side-by-side split activity UI. Bug: 190433398 Bug: 190432728 Bug: 190433500 Bug: 190433972 Bug: 191163577 Bug: 191163638 Test: Manual, using the sample in WM Jetpack Change-Id: I27335d47f4f544e2b9f63c5daaac9bd1c272a89f
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user