Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 40a67faa authored by Andrii Kulian's avatar Andrii Kulian
Browse files

Synchronize SplitController

Adding synchronization to the public methods in SplitController,
as they might be called on different threads and cause a
ConcurrentModificationException.

Bug: 228201646
Test: atest CtsWindowManagerJetpackTestCases --iterations 100
Change-Id: I2db9ccb5e9829487eae7aa85eff5f6153721cc04
parent 248de47f
Loading
Loading
Loading
Loading
+196 −159
Original line number Diff line number Diff line
@@ -47,6 +47,7 @@ import android.util.SparseArray;
import android.window.TaskFragmentInfo;
import android.window.WindowContainerTransaction;

import androidx.annotation.GuardedBy;
import androidx.window.common.EmptyLifecycleCallbacksAdapter;

import com.android.internal.annotations.VisibleForTesting;
@@ -65,9 +66,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
    private static final String TAG = "SplitController";

    @VisibleForTesting
    @GuardedBy("mLock")
    final SplitPresenter mPresenter;

    // Currently applied split configuration.
    @GuardedBy("mLock")
    private final List<EmbeddingRule> mSplitRules = new ArrayList<>();
    /**
     * Map from Task id to {@link TaskContainer} which contains all TaskFragment and split pair info
@@ -76,6 +79,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
     * organizer.
     */
    @VisibleForTesting
    @GuardedBy("mLock")
    final SparseArray<TaskContainer> mTaskContainers = new SparseArray<>();

    // Callback to Jetpack to notify about changes to split states.
@@ -83,6 +87,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
    private Consumer<List<SplitInfo>> mEmbeddingCallback;
    private final List<SplitInfo> mLastReportedSplitStates = new ArrayList<>();
    private final Handler mHandler;
    private final Object mLock = new Object();

    public SplitController() {
        final MainThreadExecutor executor = new MainThreadExecutor();
@@ -100,45 +105,34 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
    /** Updates the embedding rules applied to future activity launches. */
    @Override
    public void setEmbeddingRules(@NonNull Set<EmbeddingRule> rules) {
        synchronized (mLock) {
            mSplitRules.clear();
            mSplitRules.addAll(rules);
            for (int i = mTaskContainers.size() - 1; i >= 0; i--) {
                updateAnimationOverride(mTaskContainers.valueAt(i));
            }
        }
    }

    @NonNull
    public List<EmbeddingRule> getSplitRules() {
    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, boolean isPlaceholder) {
        try {
            mPresenter.startActivityToSide(launchingActivity, intent, options, sideRule,
                    isPlaceholder);
        } 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) {
        synchronized (mLock) {
            mEmbeddingCallback = callback;
            updateCallbackIfNecessary();
        }
    }

    @Override
    public void onTaskFragmentAppeared(@NonNull TaskFragmentInfo taskFragmentInfo) {
        synchronized (mLock) {
            TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken());
            if (container == null) {
                return;
@@ -150,9 +144,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
            }
            updateCallbackIfNecessary();
        }
    }

    @Override
    public void onTaskFragmentInfoChanged(@NonNull TaskFragmentInfo taskFragmentInfo) {
        synchronized (mLock) {
            TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken());
            if (container == null) {
                return;
@@ -162,21 +158,22 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
            final boolean wasInPip = isInPictureInPicture(container);
            container.setInfo(taskFragmentInfo);
            final boolean isInPip = isInPictureInPicture(container);
        // Check if there are no running activities - consider the container empty if there are no
        // non-finishing activities left.
            // Check if there are no running activities - consider the container empty if there are
            // no non-finishing activities left.
            if (!taskFragmentInfo.hasRunningActivity()) {
                if (taskFragmentInfo.isTaskFragmentClearedForPip()) {
                    // Do not finish the dependents if the last activity is reparented to PiP.
                // Instead, the original split should be cleanup, and the dependent may be expanded
                // to fullscreen.
                    // Instead, the original split should be cleanup, and the dependent may be
                    // expanded to fullscreen.
                    cleanupForEnterPip(wct, container);
                    mPresenter.cleanupContainer(container, false /* shouldFinishDependent */, wct);
                } else if (taskFragmentInfo.isTaskClearedForReuse()) {
                // Do not finish the dependents if this TaskFragment was cleared due to launching
                // activity in the Task.
                    // Do not finish the dependents if this TaskFragment was cleared due to
                    // launching activity in the Task.
                    mPresenter.cleanupContainer(container, false /* shouldFinishDependent */, wct);
                } else if (!container.isWaitingActivityAppear()) {
                // Do not finish the container before the expected activity appear until timeout.
                    // Do not finish the container before the expected activity appear until
                    // timeout.
                    mPresenter.cleanupContainer(container, true /* shouldFinishDependent */, wct);
                }
            } else if (wasInPip && isInPip) {
@@ -190,16 +187,20 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
                cleanupForEnterPip(wct, container);
            } else if (wasInPip) {
                // Exit PIP.
            // Updates the presentation of the container. Expand or launch placeholder if needed.
                // Updates the presentation of the container. Expand or launch placeholder if
                // needed.
                updateContainer(wct, container);
            }
            mPresenter.applyTransaction(wct);
            updateCallbackIfNecessary();
        }
    }

    @Override
    public void onTaskFragmentVanished(@NonNull TaskFragmentInfo taskFragmentInfo) {
        final TaskFragmentContainer container = getContainer(taskFragmentInfo.getFragmentToken());
        synchronized (mLock) {
            final TaskFragmentContainer container = getContainer(
                    taskFragmentInfo.getFragmentToken());
            if (container != null) {
                // Cleanup if the TaskFragment vanished is not requested by the organizer.
                removeContainer(container);
@@ -215,10 +216,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
            }
            cleanupTaskFragment(taskFragmentInfo.getFragmentToken());
        }
    }

    @Override
    public void onTaskFragmentParentInfoChanged(@NonNull IBinder fragmentToken,
            @NonNull Configuration parentConfig) {
        synchronized (mLock) {
            final TaskFragmentContainer container = getContainer(fragmentToken);
            if (container != null) {
                onTaskConfigurationChanged(container.getTaskId(), parentConfig);
@@ -230,17 +233,20 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
                updateCallbackIfNecessary();
            }
        }
    }

    @Override
    public void onActivityReparentToTask(int taskId, @NonNull Intent activityIntent,
            @NonNull IBinder activityToken) {
        // If the activity belongs to the current app process, we treat it as a new activity launch.
        synchronized (mLock) {
            // If the activity belongs to the current app process, we treat it as a new activity
            // launch.
            final Activity activity = getActivity(activityToken);
            if (activity != null) {
                // We don't allow split as primary for new launch because we currently only support
                // launching to top. We allow split as primary for activity reparent because the
            // activity may be split as primary before it is reparented out. In that case, we want
            // to show it as primary again when it is reparented back.
                // activity may be split as primary before it is reparented out. In that case, we
                // want to show it as primary again when it is reparented back.
                if (!resolveActivityToContainer(activity, true /* isOnReparent */)) {
                    // When there is no embedding rule matched, try to place it in the top container
                    // like a normal launch.
@@ -256,25 +262,27 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
                return;
            }

        // If the activity belongs to a different app process, we treat it as starting new intent,
        // since both actions might result in a new activity that should appear in an organized
        // TaskFragment.
            // If the activity belongs to a different app process, we treat it as starting new
            // intent, since both actions might result in a new activity that should appear in an
            // organized TaskFragment.
            final WindowContainerTransaction wct = new WindowContainerTransaction();
            TaskFragmentContainer targetContainer = resolveStartActivityIntent(wct, taskId,
                    activityIntent, null /* launchingActivity */);
            if (targetContainer == null) {
            // When there is no embedding rule matched, try to place it in the top container like a
            // normal launch.
                // When there is no embedding rule matched, try to place it in the top container
                // like a normal launch.
                targetContainer = taskContainer.getTopTaskFragmentContainer();
            }
            if (targetContainer == null) {
                return;
            }
        wct.reparentActivityToTaskFragment(targetContainer.getTaskFragmentToken(), activityToken);
            wct.reparentActivityToTaskFragment(targetContainer.getTaskFragmentToken(),
                    activityToken);
            mPresenter.applyTransaction(wct);
            // Because the activity does not belong to the organizer process, we wait until
            // onTaskFragmentAppeared to trigger updateCallbackIfNecessary().
        }
    }

    /** Called on receiving {@link #onTaskFragmentVanished(TaskFragmentInfo)} for cleanup. */
    private void cleanupTaskFragment(@NonNull IBinder taskFragmentToken) {
@@ -480,6 +488,22 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
        mPresenter.applyTransaction(wct);
    }

    /**
     * Starts an activity to side of the launchingActivity with the provided split config.
     */
    private void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent intent,
            @Nullable Bundle options, @NonNull SplitRule sideRule,
            @Nullable Consumer<Exception> failureCallback, boolean isPlaceholder) {
        try {
            mPresenter.startActivityToSide(launchingActivity, intent, options, sideRule,
                    isPlaceholder);
        } catch (Exception e) {
            if (failureCallback != null) {
                failureCallback.accept(e);
            }
        }
    }

    /**
     * Expands the given activity by either expanding the TaskFragment it is currently in or putting
     * it into a new expanded TaskFragment.
@@ -1382,10 +1406,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen

        @Override
        public void onActivityPreCreated(Activity activity, Bundle savedInstanceState) {
            synchronized (mLock) {
                final IBinder activityToken = activity.getActivityToken();
                final IBinder initialTaskFragmentToken = getInitialTaskFragmentToken(activity);
            // If the activity is not embedded, then it will not have an initial task fragment token
            // so no further action is needed.
                // If the activity is not embedded, then it will not have an initial task fragment
                // token so no further action is needed.
                if (initialTaskFragmentToken == null) {
                    return;
                }
@@ -1395,16 +1420,18 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
                    for (int j = containers.size() - 1; j >= 0; j--) {
                        final TaskFragmentContainer container = containers.get(j);
                        if (!container.hasActivity(activityToken)
                            && container.getTaskFragmentToken().equals(initialTaskFragmentToken)) {
                        // The onTaskFragmentInfoChanged callback containing this activity has not
                        // reached the client yet, so add the activity to the pending appeared
                        // activities.
                                && container.getTaskFragmentToken()
                                .equals(initialTaskFragmentToken)) {
                            // The onTaskFragmentInfoChanged callback containing this activity has
                            // not reached the client yet, so add the activity to the pending
                            // appeared activities.
                            container.addPendingAppearedActivity(activity);
                            return;
                        }
                    }
                }
            }
        }

        @Override
        public void onActivityPostCreated(Activity activity, Bundle savedInstanceState) {
@@ -1412,19 +1439,25 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
            // 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.
            synchronized (mLock) {
                SplitController.this.onActivityCreated(activity);
            }
        }

        @Override
        public void onActivityConfigurationChanged(Activity activity) {
            synchronized (mLock) {
                SplitController.this.onActivityConfigurationChanged(activity);
            }
        }

        @Override
        public void onActivityPostDestroyed(Activity activity) {
            synchronized (mLock) {
                SplitController.this.onActivityDestroyed(activity);
            }
        }
    }

    /** Executor that posts on the main application thread. */
    private static class MainThreadExecutor implements Executor {
@@ -1457,17 +1490,19 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
                return super.onStartActivity(who, intent, options);
            }

            synchronized (mLock) {
                final int taskId = getTaskId(launchingActivity);
                final WindowContainerTransaction wct = new WindowContainerTransaction();
                final TaskFragmentContainer launchedInTaskFragment = resolveStartActivityIntent(wct,
                        taskId, intent, launchingActivity);
                if (launchedInTaskFragment != null) {
                    mPresenter.applyTransaction(wct);
                // Amend the request to let the WM know that the activity should be placed in the
                // dedicated container.
                    // 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,
                            launchedInTaskFragment.getTaskFragmentToken());
                }
            }

            return super.onStartActivity(who, intent, options);
        }
@@ -1479,8 +1514,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
     */
    @Override
    public boolean isActivityEmbedded(@NonNull Activity activity) {
        synchronized (mLock) {
            return mPresenter.isActivityEmbedded(activity.getActivityToken());
        }
    }

    /**
     * If the two rules have the same presentation, we can reuse the same {@link SplitContainer} if