diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index f6bcfd6f6dd3f..e3abe4d3ebad8 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -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; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/SampleExtensionImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/SampleExtensionImpl.java index a0d5b004ff1c0..cafc2337a022c 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/SampleExtensionImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/SampleExtensionImpl.java @@ -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> 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 splitRules) { + mSplitController.setSplitRules(splitRules); + } + + @Override + @NonNull + public List 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); + } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitContainer.java new file mode 100644 index 0000000000000..ade8573589877 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitContainer.java @@ -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; + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitController.java new file mode 100644 index 0000000000000..7298d34977704 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitController.java @@ -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 mSplitRules = new ArrayList<>(); + private final List mContainers = new ArrayList<>(); + private final List 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 splitRules) { + mSplitRules.clear(); + mSplitRules.addAll(splitRules); + } + + @NonNull + public List 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 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 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 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 getActiveSplitStates() { + List 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 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 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) { + } + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitPresenter.java new file mode 100644 index 0000000000000..381d6d7c0eed2 --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitPresenter.java @@ -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(); + } +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentContainer.java new file mode 100644 index 0000000000000..3cf37a6e4e3bf --- /dev/null +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentContainer.java @@ -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 mContainersToFinishOnExit = + new ArrayList<>(); + + /** Individual associated activities in different containers that should be finished on exit. */ + private final List 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 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 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 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; + } +} diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar index be6652d43fb21..fdbc5f61c4513 100644 Binary files a/libs/WindowManager/Jetpack/window-extensions-release.aar and b/libs/WindowManager/Jetpack/window-extensions-release.aar differ