| /* |
| * 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.embedding; |
| |
| import static androidx.window.extensions.embedding.SplitContainer.getFinishPrimaryWithSecondaryBehavior; |
| import static androidx.window.extensions.embedding.SplitContainer.getFinishSecondaryWithPrimaryBehavior; |
| import static androidx.window.extensions.embedding.SplitContainer.isStickyPlaceholderRule; |
| import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenAdjacent; |
| import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenStacked; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.app.Activity; |
| import android.app.ActivityClient; |
| import android.app.ActivityOptions; |
| import android.app.ActivityThread; |
| import android.app.Instrumentation; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.res.Configuration; |
| import android.graphics.Rect; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Looper; |
| import android.window.TaskFragmentInfo; |
| import android.window.WindowContainerTransaction; |
| |
| import androidx.window.common.EmptyLifecycleCallbacksAdapter; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.concurrent.Executor; |
| import java.util.function.Consumer; |
| |
| /** |
| * Main controller class that manages split states and presentation. |
| */ |
| public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmentCallback, |
| ActivityEmbeddingComponent { |
| |
| private final SplitPresenter mPresenter; |
| |
| // Currently applied split configuration. |
| private final List<EmbeddingRule> 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 @NonNull Consumer<List<SplitInfo>> mEmbeddingCallback; |
| private final List<SplitInfo> mLastReportedSplitStates = new ArrayList<>(); |
| |
| // We currently only support split activity embedding within the one root Task. |
| private final Rect mParentBounds = new Rect(); |
| |
| public SplitController() { |
| mPresenter = new SplitPresenter(new MainThreadExecutor(), this); |
| ActivityThread activityThread = ActivityThread.currentActivityThread(); |
| // Register a callback to be notified about activities being created. |
| activityThread.getApplication().registerActivityLifecycleCallbacks( |
| new LifecycleCallbacks()); |
| // Intercept activity starts to route activities to new containers if necessary. |
| Instrumentation instrumentation = activityThread.getInstrumentation(); |
| instrumentation.addMonitor(new ActivityStartMonitor()); |
| } |
| |
| /** Updates the embedding rules applied to future activity launches. */ |
| @Override |
| public void setEmbeddingRules(@NonNull Set<EmbeddingRule> rules) { |
| mSplitRules.clear(); |
| mSplitRules.addAll(rules); |
| updateAnimationOverride(); |
| } |
| |
| @NonNull |
| public List<EmbeddingRule> 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 SplitRule sideRule, |
| @Nullable Consumer<Exception> failureCallback) { |
| try { |
| mPresenter.startActivityToSide(launchingActivity, intent, options, sideRule); |
| } catch (Exception e) { |
| if (failureCallback != null) { |
| failureCallback.accept(e); |
| } |
| } |
| } |
| |
| /** |
| * Registers the split organizer callback to notify about changes to active splits. |
| */ |
| @Override |
| public void setSplitInfoCallback(@NonNull Consumer<List<SplitInfo>> callback) { |
| mEmbeddingCallback = callback; |
| updateCallbackIfNecessary(); |
| } |
| |
| @Override |
| public void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo) { |
| TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); |
| if (container == null) { |
| return; |
| } |
| |
| container.setInfo(taskFragmentInfo); |
| if (container.isFinished()) { |
| mPresenter.cleanupContainer(container, false /* shouldFinishDependent */); |
| } |
| updateCallbackIfNecessary(); |
| } |
| |
| @Override |
| public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) { |
| TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); |
| if (container == null) { |
| return; |
| } |
| |
| container.setInfo(taskFragmentInfo); |
| // Check if there are no running activities - consider the container empty if there are no |
| // non-finishing activities left. |
| if (!taskFragmentInfo.hasRunningActivity()) { |
| // Do not finish the dependents if this TaskFragment was cleared due to launching |
| // activity in the Task. |
| final boolean shouldFinishDependent = |
| !taskFragmentInfo.isTaskClearedForReuse(); |
| mPresenter.cleanupContainer(container, shouldFinishDependent); |
| } |
| updateCallbackIfNecessary(); |
| } |
| |
| @Override |
| public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) { |
| TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken()); |
| if (container == null) { |
| return; |
| } |
| |
| mPresenter.cleanupContainer(container, true /* shouldFinishDependent */); |
| updateCallbackIfNecessary(); |
| } |
| |
| @Override |
| public void onTaskFragmentParentInfoChanged(@NonNull IBinder fragmentToken, |
| @NonNull Configuration parentConfig) { |
| onParentBoundsMayChange(parentConfig.windowConfiguration.getBounds()); |
| TaskFragmentContainer container = getContainer(fragmentToken); |
| if (container != null) { |
| mPresenter.updateContainer(container); |
| updateCallbackIfNecessary(); |
| } |
| } |
| |
| private void onParentBoundsMayChange(Activity activity) { |
| if (activity.isFinishing()) { |
| return; |
| } |
| |
| onParentBoundsMayChange(mPresenter.getParentContainerBounds(activity)); |
| } |
| |
| private void onParentBoundsMayChange(Rect parentBounds) { |
| if (!parentBounds.isEmpty() && !mParentBounds.equals(parentBounds)) { |
| mParentBounds.set(parentBounds); |
| updateAnimationOverride(); |
| } |
| } |
| |
| /** |
| * Updates if we should override transition animation. We only want to override if the Task |
| * bounds is large enough for at least one split rule. |
| */ |
| private void updateAnimationOverride() { |
| if (mParentBounds.isEmpty()) { |
| // We don't know about the parent bounds yet. |
| return; |
| } |
| |
| // Check if the parent container bounds can support any split rule. |
| boolean supportSplit = false; |
| for (EmbeddingRule rule : mSplitRules) { |
| if (!(rule instanceof SplitRule)) { |
| continue; |
| } |
| if (mPresenter.shouldShowSideBySide(mParentBounds, (SplitRule) rule)) { |
| supportSplit = true; |
| break; |
| } |
| } |
| |
| // We only want to override if it supports split. |
| if (supportSplit) { |
| mPresenter.startOverrideSplitAnimation(); |
| } else { |
| mPresenter.stopOverrideSplitAnimation(); |
| } |
| } |
| |
| void onActivityCreated(@NonNull Activity launchedActivity) { |
| handleActivityCreated(launchedActivity); |
| 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. |
| */ |
| // TODO(b/190433398): Break down into smaller functions. |
| void handleActivityCreated(@NonNull Activity launchedActivity) { |
| final List<EmbeddingRule> splitRules = getSplitRules(); |
| final TaskFragmentContainer currentContainer = getContainerWithActivity( |
| launchedActivity.getActivityToken()); |
| |
| if (currentContainer == null) { |
| // Initial check before any TaskFragment is created. |
| onParentBoundsMayChange(launchedActivity); |
| } |
| |
| // Check if the activity is configured to always be expanded. |
| if (shouldExpand(launchedActivity, null, 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; |
| } |
| |
| // Check if the split is already set. |
| final TaskFragmentContainer activityBelowContainer = getContainerWithActivity( |
| activityBelow.getActivityToken()); |
| if (currentContainer != null && activityBelowContainer != null) { |
| final SplitContainer existingSplit = getActiveSplitForContainers(currentContainer, |
| activityBelowContainer); |
| if (existingSplit != null) { |
| // There is already an active split with the activity below. |
| return; |
| } |
| } |
| |
| final SplitPairRule splitPairRule = getSplitRule(activityBelow, launchedActivity, |
| splitRules); |
| if (splitPairRule == null) { |
| return; |
| } |
| |
| mPresenter.createNewSplitContainer(activityBelow, launchedActivity, |
| splitPairRule); |
| } |
| |
| private void onActivityConfigurationChanged(@NonNull Activity activity) { |
| final TaskFragmentContainer currentContainer = getContainerWithActivity( |
| activity.getActivityToken()); |
| |
| if (currentContainer != null) { |
| // Changes to activities in controllers are handled in |
| // onTaskFragmentParentInfoChanged |
| return; |
| } |
| // The bounds of the container may have been changed. |
| onParentBoundsMayChange(activity); |
| |
| // Check if activity requires a placeholder |
| launchPlaceholderIfNecessary(activity); |
| } |
| |
| /** |
| * 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. Finishes |
| * existing secondary containers if found for the given primary container. |
| */ |
| void registerSplit(@NonNull WindowContainerTransaction wct, |
| @NonNull TaskFragmentContainer primaryContainer, @NonNull Activity primaryActivity, |
| @NonNull TaskFragmentContainer secondaryContainer, |
| @NonNull SplitRule splitRule) { |
| SplitContainer splitContainer = new SplitContainer(primaryContainer, primaryActivity, |
| secondaryContainer, splitRule); |
| // Remove container later to prevent pinning escaping toast showing in lock task mode. |
| if (splitRule instanceof SplitPairRule && ((SplitPairRule) splitRule).shouldClearTop()) { |
| removeExistingSecondaryContainers(wct, primaryContainer); |
| } |
| mSplitContainers.add(splitContainer); |
| } |
| |
| /** |
| * Removes the container from bookkeeping records. |
| */ |
| void removeContainer(@NonNull TaskFragmentContainer container) { |
| // 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); |
| } |
| |
| /** |
| * Removes a secondary container for the given primary container if an existing split is |
| * already registered. |
| */ |
| void removeExistingSecondaryContainers(@NonNull WindowContainerTransaction wct, |
| @NonNull TaskFragmentContainer primaryContainer) { |
| // If the primary container was already in a split - remove the secondary container that |
| // is now covered by the new one that replaced it. |
| final SplitContainer existingSplitContainer = getActiveSplitForContainer( |
| primaryContainer); |
| if (existingSplitContainer == null |
| || primaryContainer == existingSplitContainer.getSecondaryContainer()) { |
| return; |
| } |
| |
| existingSplitContainer.getSecondaryContainer().finish( |
| false /* shouldFinishDependent */, mPresenter, wct, this); |
| } |
| |
| /** |
| * 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() && container.getTopNonFinishingActivity() != null) { |
| 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, if available. |
| */ |
| @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; |
| } |
| |
| /** |
| * Returns the active split that has the provided containers as primary and secondary or as |
| * secondary and primary, if available. |
| */ |
| @Nullable |
| private SplitContainer getActiveSplitForContainers( |
| @NonNull TaskFragmentContainer firstContainer, |
| @NonNull TaskFragmentContainer secondContainer) { |
| for (int i = mSplitContainers.size() - 1; i >= 0; i--) { |
| SplitContainer splitContainer = mSplitContainers.get(i); |
| final TaskFragmentContainer primary = splitContainer.getPrimaryContainer(); |
| final TaskFragmentContainer secondary = splitContainer.getSecondaryContainer(); |
| if ((firstContainer == secondary && secondContainer == primary) |
| || (firstContainer == primary && secondContainer == secondary)) { |
| 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 SplitPlaceholderRule placeholderRule = getPlaceholderRule(activity); |
| if (placeholderRule == null || !mPresenter.shouldShowSideBySide( |
| mPresenter.getParentContainerBounds(activity), placeholderRule)) { |
| return false; |
| } |
| |
| // TODO(b/190433398): Handle failed request |
| startActivityToSide(activity, placeholderRule.getPlaceholderIntent(), null, |
| placeholderRule, null); |
| return true; |
| } |
| |
| private boolean dismissPlaceholderIfNecessary(@NonNull SplitContainer splitContainer) { |
| if (!splitContainer.isPlaceholderContainer()) { |
| return false; |
| } |
| |
| if (isStickyPlaceholderRule(splitContainer.getSplitRule())) { |
| // The placeholder should remain after it was first shown. |
| return false; |
| } |
| |
| if (mPresenter.shouldShowSideBySide(splitContainer)) { |
| return false; |
| } |
| |
| mPresenter.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 SplitPlaceholderRule getPlaceholderRule(@NonNull Activity activity) { |
| for (EmbeddingRule rule : mSplitRules) { |
| if (!(rule instanceof SplitPlaceholderRule)) { |
| continue; |
| } |
| SplitPlaceholderRule placeholderRule = (SplitPlaceholderRule) rule; |
| if (placeholderRule.matchesActivity(activity)) { |
| return placeholderRule; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Notifies listeners about changes to split states if necessary. |
| */ |
| private void updateCallbackIfNecessary() { |
| if (mEmbeddingCallback == null) { |
| return; |
| } |
| if (!allActivitiesCreated()) { |
| return; |
| } |
| List<SplitInfo> currentSplitStates = getActiveSplitStates(); |
| if (currentSplitStates == null || mLastReportedSplitStates.equals(currentSplitStates)) { |
| return; |
| } |
| mLastReportedSplitStates.clear(); |
| mLastReportedSplitStates.addAll(currentSplitStates); |
| mEmbeddingCallback.accept(currentSplitStates); |
| } |
| |
| /** |
| * @return a list of descriptors for currently active split states. If the value returned is |
| * null, that indicates that the active split states are in an intermediate state and should |
| * not be reported. |
| */ |
| @Nullable |
| private List<SplitInfo> getActiveSplitStates() { |
| List<SplitInfo> splitStates = new ArrayList<>(); |
| for (SplitContainer container : mSplitContainers) { |
| if (container.getPrimaryContainer().isEmpty() |
| || container.getSecondaryContainer().isEmpty()) { |
| // We are in an intermediate state because either the split container is about to be |
| // removed or the primary or secondary container are about to receive an activity. |
| return null; |
| } |
| ActivityStack primaryContainer = container.getPrimaryContainer().toActivityStack(); |
| ActivityStack secondaryContainer = container.getSecondaryContainer().toActivityStack(); |
| SplitInfo splitState = new SplitInfo(primaryContainer, |
| secondaryContainer, |
| // Splits that are not showing side-by-side are reported as having 0 split |
| // ratio, since by definition in the API the primary container occupies no |
| // width of the split when covered by the secondary. |
| mPresenter.shouldShowSideBySide(container) |
| ? container.getSplitRule().getSplitRatio() |
| : 0.0f); |
| splitStates.add(splitState); |
| } |
| return splitStates; |
| } |
| |
| /** |
| * Checks if all activities that are registered with the containers have already appeared in |
| * the client. |
| */ |
| private boolean allActivitiesCreated() { |
| for (TaskFragmentContainer container : mContainers) { |
| if (container.getInfo() == null |
| || container.getInfo().getActivities().size() |
| != container.collectActivities().size()) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * 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 primary activity and secondary activity intent |
| * if available. |
| */ |
| @Nullable |
| private static SplitPairRule getSplitRule(@NonNull Activity primaryActivity, |
| @NonNull Intent secondaryActivityIntent, @NonNull List<EmbeddingRule> splitRules) { |
| for (EmbeddingRule rule : splitRules) { |
| if (!(rule instanceof SplitPairRule)) { |
| continue; |
| } |
| SplitPairRule pairRule = (SplitPairRule) rule; |
| if (pairRule.matchesActivityIntentPair(primaryActivity, secondaryActivityIntent)) { |
| return pairRule; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns a split rule for the provided pair of primary and secondary activities if available. |
| */ |
| @Nullable |
| private static SplitPairRule getSplitRule(@NonNull Activity primaryActivity, |
| @NonNull Activity secondaryActivity, @NonNull List<EmbeddingRule> splitRules) { |
| for (EmbeddingRule rule : splitRules) { |
| if (!(rule instanceof SplitPairRule)) { |
| continue; |
| } |
| SplitPairRule pairRule = (SplitPairRule) rule; |
| final Intent intent = secondaryActivity.getIntent(); |
| if (pairRule.matchesActivityPair(primaryActivity, secondaryActivity) |
| && (intent == null |
| || pairRule.matchesActivityIntentPair(primaryActivity, intent))) { |
| return pairRule; |
| } |
| } |
| return null; |
| } |
| |
| @Nullable |
| 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(@Nullable Activity activity, @Nullable Intent intent, |
| List<EmbeddingRule> splitRules) { |
| if (splitRules == null) { |
| return false; |
| } |
| for (EmbeddingRule rule : splitRules) { |
| if (!(rule instanceof ActivityRule)) { |
| continue; |
| } |
| ActivityRule activityRule = (ActivityRule) rule; |
| if (!activityRule.shouldAlwaysExpand()) { |
| continue; |
| } |
| if (activity != null && activityRule.matchesActivity(activity)) { |
| return true; |
| } else if (intent != null && activityRule.matchesIntent(intent)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Checks whether the associated container should be destroyed together with a finishing |
| * container. There is a case when primary containers for placeholders should be retained |
| * despite the rule configuration to finish primary with secondary - if they are marked as |
| * 'sticky' and the placeholder was finished when fully overlapping the primary container. |
| * @return {@code true} if the associated container should be retained (and not be finished). |
| */ |
| boolean shouldRetainAssociatedContainer(@NonNull TaskFragmentContainer finishingContainer, |
| @NonNull TaskFragmentContainer associatedContainer) { |
| SplitContainer splitContainer = getActiveSplitForContainers(associatedContainer, |
| finishingContainer); |
| if (splitContainer == null) { |
| // Containers are not in the same split, no need to retain. |
| return false; |
| } |
| // Find the finish behavior for the associated container |
| int finishBehavior; |
| SplitRule splitRule = splitContainer.getSplitRule(); |
| if (finishingContainer == splitContainer.getPrimaryContainer()) { |
| finishBehavior = getFinishSecondaryWithPrimaryBehavior(splitRule); |
| } else { |
| finishBehavior = getFinishPrimaryWithSecondaryBehavior(splitRule); |
| } |
| // Decide whether the associated container should be retained based on the current |
| // presentation mode. |
| if (mPresenter.shouldShowSideBySide(splitContainer)) { |
| return !shouldFinishAssociatedContainerWhenAdjacent(finishBehavior); |
| } else { |
| return !shouldFinishAssociatedContainerWhenStacked(finishBehavior); |
| } |
| } |
| |
| /** |
| * @see #shouldRetainAssociatedContainer(TaskFragmentContainer, TaskFragmentContainer) |
| */ |
| boolean shouldRetainAssociatedActivity(@NonNull TaskFragmentContainer finishingContainer, |
| @NonNull Activity associatedActivity) { |
| TaskFragmentContainer associatedContainer = getContainerWithActivity( |
| associatedActivity.getActivityToken()); |
| if (associatedContainer == null) { |
| return false; |
| } |
| |
| return shouldRetainAssociatedContainer(finishingContainer, associatedContainer); |
| } |
| |
| private final class LifecycleCallbacks extends EmptyLifecycleCallbacksAdapter { |
| |
| @Override |
| public void onActivityPostCreated(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 onActivityConfigurationChanged(Activity activity) { |
| SplitController.this.onActivityConfigurationChanged(activity); |
| } |
| } |
| |
| /** Executor that posts on the main application thread. */ |
| private static class MainThreadExecutor implements Executor { |
| private final Handler mHandler = new Handler(Looper.getMainLooper()); |
| |
| @Override |
| public void execute(Runnable r) { |
| mHandler.post(r); |
| } |
| } |
| |
| /** |
| * A monitor that intercepts all activity start requests originating in the client process and |
| * can amend them to target a specific task fragment to form a split. |
| */ |
| private class ActivityStartMonitor extends Instrumentation.ActivityMonitor { |
| |
| @Override |
| public Instrumentation.ActivityResult onStartActivity(@NonNull Context who, |
| @NonNull Intent intent, @NonNull Bundle options) { |
| // TODO(b/190433398): Check if the activity is configured to always be expanded. |
| |
| // Check if activity should be put in a split with the activity that launched it. |
| if (!(who instanceof Activity)) { |
| return super.onStartActivity(who, intent, options); |
| } |
| final Activity launchingActivity = (Activity) who; |
| |
| if (shouldExpand(null, intent, getSplitRules())) { |
| setLaunchingInExpandedContainer(launchingActivity, options); |
| } else if (!splitWithLaunchingActivity(launchingActivity, intent, options)) { |
| setLaunchingInSameSideContainer(launchingActivity, intent, options); |
| } |
| |
| return super.onStartActivity(who, intent, options); |
| } |
| |
| private void setLaunchingInExpandedContainer(Activity launchingActivity, Bundle options) { |
| TaskFragmentContainer newContainer = mPresenter.createNewExpandedContainer( |
| launchingActivity); |
| |
| // Amend the request to let the WM know that the activity should be placed in the |
| // dedicated container. |
| options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, |
| newContainer.getTaskFragmentToken()); |
| } |
| |
| /** |
| * Returns {@code true} if the activity that is going to be started via the |
| * {@code intent} should be paired with the {@code launchingActivity} and is set to be |
| * launched in the side container. |
| */ |
| private boolean splitWithLaunchingActivity(Activity launchingActivity, Intent intent, |
| Bundle options) { |
| final SplitPairRule splitPairRule = getSplitRule(launchingActivity, intent, |
| getSplitRules()); |
| if (splitPairRule == null) { |
| return false; |
| } |
| |
| // Check if there is any existing side container to launch into. |
| TaskFragmentContainer secondaryContainer = findSideContainerForNewLaunch( |
| launchingActivity, splitPairRule); |
| if (secondaryContainer == null) { |
| // Create a new split with an empty side container. |
| secondaryContainer = mPresenter |
| .createNewSplitWithEmptySideContainer(launchingActivity, splitPairRule); |
| } |
| |
| // Amend the request to let the WM know that the activity should be placed in the |
| // dedicated container. |
| options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, |
| secondaryContainer.getTaskFragmentToken()); |
| return true; |
| } |
| |
| /** |
| * Finds if there is an existing split side {@link TaskFragmentContainer} that can be used |
| * for the new rule. |
| */ |
| @Nullable |
| private TaskFragmentContainer findSideContainerForNewLaunch(Activity launchingActivity, |
| SplitPairRule splitPairRule) { |
| final TaskFragmentContainer launchingContainer = getContainerWithActivity( |
| launchingActivity.getActivityToken()); |
| if (launchingContainer == null) { |
| return null; |
| } |
| |
| // We only check if the launching activity is the primary of the split. We will check |
| // if the launching activity is the secondary in #setLaunchingInSameSideContainer. |
| final SplitContainer splitContainer = getActiveSplitForContainer(launchingContainer); |
| if (splitContainer == null |
| || splitContainer.getPrimaryContainer() != launchingContainer) { |
| return null; |
| } |
| |
| if (canReuseContainer(splitPairRule, splitContainer.getSplitRule())) { |
| return splitContainer.getSecondaryContainer(); |
| } |
| return null; |
| } |
| |
| /** |
| * Checks if the activity that is going to be started via the {@code intent} should be |
| * paired with the existing top activity which is currently paired with the |
| * {@code launchingActivity}. If so, set the activity to be launched in the same side |
| * container of the {@code launchingActivity}. |
| */ |
| private void setLaunchingInSameSideContainer(Activity launchingActivity, Intent intent, |
| Bundle options) { |
| final TaskFragmentContainer launchingContainer = getContainerWithActivity( |
| launchingActivity.getActivityToken()); |
| if (launchingContainer == null) { |
| return; |
| } |
| |
| final SplitContainer splitContainer = getActiveSplitForContainer(launchingContainer); |
| if (splitContainer == null) { |
| return; |
| } |
| |
| if (splitContainer.getSecondaryContainer() != launchingContainer) { |
| return; |
| } |
| |
| // The launching activity is on the secondary container. Retrieve the primary |
| // activity from the other container. |
| Activity primaryActivity = |
| splitContainer.getPrimaryContainer().getTopNonFinishingActivity(); |
| if (primaryActivity == null) { |
| return; |
| } |
| |
| final SplitPairRule splitPairRule = getSplitRule(primaryActivity, intent, |
| getSplitRules()); |
| if (splitPairRule == null) { |
| return; |
| } |
| |
| // Can only launch in the same container if the rules share the same presentation. |
| if (!canReuseContainer(splitPairRule, splitContainer.getSplitRule())) { |
| return; |
| } |
| |
| // Amend the request to let the WM know that the activity should be placed in the |
| // dedicated container. This is necessary for the case that the activity is started |
| // into a new Task, or new Task will be escaped from the current host Task and be |
| // displayed in fullscreen. |
| options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, |
| launchingContainer.getTaskFragmentToken()); |
| } |
| } |
| |
| /** |
| * Checks if an activity is embedded and its presentation is customized by a |
| * {@link android.window.TaskFragmentOrganizer} to only occupy a portion of Task bounds. |
| */ |
| public boolean isActivityEmbedded(@NonNull Activity activity) { |
| return mPresenter.isActivityEmbedded(activity.getActivityToken()); |
| } |
| |
| /** |
| * If the two rules have the same presentation, we can reuse the same {@link SplitContainer} if |
| * there is any. |
| */ |
| private static boolean canReuseContainer(SplitRule rule1, SplitRule rule2) { |
| if (!isContainerReusableRule(rule1) || !isContainerReusableRule(rule2)) { |
| return false; |
| } |
| return rule1.getSplitRatio() == rule2.getSplitRatio() |
| && rule1.getLayoutDirection() == rule2.getLayoutDirection(); |
| } |
| |
| /** |
| * Whether it is ok for other rule to reuse the {@link TaskFragmentContainer} of the given |
| * rule. |
| */ |
| private static boolean isContainerReusableRule(SplitRule rule) { |
| // We don't expect to reuse the placeholder rule. |
| if (!(rule instanceof SplitPairRule)) { |
| return false; |
| } |
| final SplitPairRule pairRule = (SplitPairRule) rule; |
| |
| // Not reuse if it needs to destroy the existing. |
| return !pairRule.shouldClearTop(); |
| } |
| } |