/*
 * 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();
    }
}
