From 7ae2cae674c4b3bbdc930527e116db033f305cff Mon Sep 17 00:00:00 2001 From: Andrii Kulian Date: Thu, 15 Jul 2021 13:11:40 -0700 Subject: [PATCH 1/2] Add base container classes for reference Jetpack organizer - TaskFragmentContainer corresponds to a TaskFragment record. - SplitContainer has a record of two containers combined in a split with a given rule. Bug: 190433398 Test: Manual, using the reference implementation. Change-Id: I54cd86d2047aac2fa392189ed355760b1a32b6c4 --- .../extensions/organizer/SplitContainer.java | 62 ++++++ .../organizer/TaskFragmentContainer.java | 202 ++++++++++++++++++ .../Jetpack/window-extensions-release.aar | Bin 7613 -> 11688 bytes 3 files changed, 264 insertions(+) create mode 100644 libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitContainer.java create mode 100644 libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/TaskFragmentContainer.java 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/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 be6652d43fb21c954cb0d361ae07cf5f38df1455..fdbc5f61c45134289bc43ede508807cbd9d697b5 100644 GIT binary patch delta 9794 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 NYytoPI~xE1003Z*4ITgh From 85986bf974ceba929de1bfda6a7e43330e17595e Mon Sep 17 00:00:00 2001 From: Andrii Kulian Date: Thu, 15 Jul 2021 13:13:50 -0700 Subject: [PATCH 2/2] 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 --- core/java/android/app/ActivityThread.java | 14 +- .../extensions/SampleExtensionImpl.java | 32 + .../extensions/organizer/SplitController.java | 580 ++++++++++++++++++ .../extensions/organizer/SplitPresenter.java | 302 +++++++++ 4 files changed, 921 insertions(+), 7 deletions(-) create mode 100644 libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitController.java create mode 100644 libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitPresenter.java 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/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(); + } +}