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

Commit 94354a9a authored by Andrii Kulian's avatar Andrii Kulian
Browse files

Intercept activity start requests in client organizer

This allows applying split rules to activity start requests for
different processes and covers APIs like 'startActivityAsUser',
not requiring app developers to use dedicated APIs to start to side
instead.

When the client organizer observes a new activity being started, it
creates a new TaskFragment first and modifies the activity start
options to target that container.

Bug: 194140227
Bug: 190433398
Test: Manual, using reference implementation and sample app.
Change-Id: Ice3de8ec725d327266ec38052129b4158608855e
parent 92b32639
Loading
Loading
Loading
Loading
+24 −0
Original line number Diff line number Diff line
@@ -214,6 +214,14 @@ public class ActivityOptions {
    public static final String KEY_LAUNCH_ROOT_TASK_TOKEN =
            "android.activity.launchRootTaskToken";

    /**
     * The {@link com.android.server.wm.TaskFragment} token the activity should be launched into.
     * @see #setLaunchTaskFragmentToken(IBinder)
     * @hide
     */
    public static final String KEY_LAUNCH_TASK_FRAGMENT_TOKEN =
            "android.activity.launchTaskFragmentToken";

    /**
     * The windowing mode the activity should be launched into.
     * @hide
@@ -396,6 +404,7 @@ public class ActivityOptions {
    private int mCallerDisplayId = INVALID_DISPLAY;
    private WindowContainerToken mLaunchTaskDisplayArea;
    private WindowContainerToken mLaunchRootTask;
    private IBinder mLaunchTaskFragmentToken;
    @WindowConfiguration.WindowingMode
    private int mLaunchWindowingMode = WINDOWING_MODE_UNDEFINED;
    @WindowConfiguration.ActivityType
@@ -1138,6 +1147,7 @@ public class ActivityOptions {
        mCallerDisplayId = opts.getInt(KEY_CALLER_DISPLAY_ID, INVALID_DISPLAY);
        mLaunchTaskDisplayArea = opts.getParcelable(KEY_LAUNCH_TASK_DISPLAY_AREA_TOKEN);
        mLaunchRootTask = opts.getParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN);
        mLaunchTaskFragmentToken = opts.getBinder(KEY_LAUNCH_TASK_FRAGMENT_TOKEN);
        mLaunchWindowingMode = opts.getInt(KEY_LAUNCH_WINDOWING_MODE, WINDOWING_MODE_UNDEFINED);
        mLaunchActivityType = opts.getInt(KEY_LAUNCH_ACTIVITY_TYPE, ACTIVITY_TYPE_UNDEFINED);
        mLaunchTaskId = opts.getInt(KEY_LAUNCH_TASK_ID, -1);
@@ -1472,6 +1482,17 @@ public class ActivityOptions {
        return this;
    }

    /** @hide */
    public IBinder getLaunchTaskFragmentToken() {
        return mLaunchTaskFragmentToken;
    }

    /** @hide */
    public ActivityOptions setLaunchTaskFragmentToken(IBinder taskFragmentToken) {
        mLaunchTaskFragmentToken = taskFragmentToken;
        return this;
    }

    /** @hide */
    public int getLaunchWindowingMode() {
        return mLaunchWindowingMode;
@@ -1882,6 +1903,9 @@ public class ActivityOptions {
        if (mLaunchRootTask != null) {
            b.putParcelable(KEY_LAUNCH_ROOT_TASK_TOKEN, mLaunchRootTask);
        }
        if (mLaunchTaskFragmentToken != null) {
            b.putBinder(KEY_LAUNCH_TASK_FRAGMENT_TOKEN, mLaunchTaskFragmentToken);
        }
        if (mLaunchWindowingMode != WINDOWING_MODE_UNDEFINED) {
            b.putInt(KEY_LAUNCH_WINDOWING_MODE, mLaunchWindowingMode);
        }
+35 −6
Original line number Diff line number Diff line
@@ -742,6 +742,17 @@ public class Instrumentation {
            }
        }

        /**
         * This overload is used for notifying the {@link android.window.TaskFragmentOrganizer}
         * implementation internally about started activities.
         *
         * @see #onStartActivity(Intent)
         * @hide
         */
        public ActivityResult onStartActivity(Context who, Intent intent, Bundle options) {
            return onStartActivity(intent);
        }

        /**
         * Used for intercepting any started activity.
         *
@@ -1722,7 +1733,10 @@ public class Instrumentation {
                    final ActivityMonitor am = mActivityMonitors.get(i);
                    ActivityResult result = null;
                    if (am.ignoreMatchingSpecificIntents()) {
                        result = am.onStartActivity(intent);
                        if (options == null) {
                            options = ActivityOptions.makeBasic().toBundle();
                        }
                        result = am.onStartActivity(who, intent, options);
                    }
                    if (result != null) {
                        am.mHits++;
@@ -1790,7 +1804,10 @@ public class Instrumentation {
                    final ActivityMonitor am = mActivityMonitors.get(i);
                    ActivityResult result = null;
                    if (am.ignoreMatchingSpecificIntents()) {
                        result = am.onStartActivity(intents[0]);
                        if (options == null) {
                            options = ActivityOptions.makeBasic().toBundle();
                        }
                        result = am.onStartActivity(who, intents[0], options);
                    }
                    if (result != null) {
                        am.mHits++;
@@ -1861,7 +1878,10 @@ public class Instrumentation {
                    final ActivityMonitor am = mActivityMonitors.get(i);
                    ActivityResult result = null;
                    if (am.ignoreMatchingSpecificIntents()) {
                        result = am.onStartActivity(intent);
                        if (options == null) {
                            options = ActivityOptions.makeBasic().toBundle();
                        }
                        result = am.onStartActivity(who, intent, options);
                    }
                    if (result != null) {
                        am.mHits++;
@@ -1928,7 +1948,10 @@ public class Instrumentation {
                    final ActivityMonitor am = mActivityMonitors.get(i);
                    ActivityResult result = null;
                    if (am.ignoreMatchingSpecificIntents()) {
                        result = am.onStartActivity(intent);
                        if (options == null) {
                            options = ActivityOptions.makeBasic().toBundle();
                        }
                        result = am.onStartActivity(who, intent, options);
                    }
                    if (result != null) {
                        am.mHits++;
@@ -1974,7 +1997,10 @@ public class Instrumentation {
                    final ActivityMonitor am = mActivityMonitors.get(i);
                    ActivityResult result = null;
                    if (am.ignoreMatchingSpecificIntents()) {
                        result = am.onStartActivity(intent);
                        if (options == null) {
                            options = ActivityOptions.makeBasic().toBundle();
                        }
                        result = am.onStartActivity(who, intent, options);
                    }
                    if (result != null) {
                        am.mHits++;
@@ -2021,7 +2047,10 @@ public class Instrumentation {
                    final ActivityMonitor am = mActivityMonitors.get(i);
                    ActivityResult result = null;
                    if (am.ignoreMatchingSpecificIntents()) {
                        result = am.onStartActivity(intent);
                        if (options == null) {
                            options = ActivityOptions.makeBasic().toBundle();
                        }
                        result = am.onStartActivity(who, intent, options);
                    }
                    if (result != null) {
                        am.mHits++;
+15 −9
Original line number Diff line number Diff line
@@ -155,6 +155,17 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
        applyTransaction(wct);
    }

    /**
     * @param ownerToken The token of the activity that creates this task fragment. It does not
     *                   have to be a child of this task fragment, but must belong to the same task.
     */
    void createTaskFragment(WindowContainerTransaction wct, IBinder fragmentToken,
            IBinder ownerToken, @NonNull Rect bounds, @WindowingMode int windowingMode) {
        final TaskFragmentCreationParams fragmentOptions =
                createFragmentOptions(fragmentToken, ownerToken, bounds, windowingMode);
        wct.createTaskFragment(fragmentOptions);
    }

    /**
     * @param ownerToken The token of the activity that creates this task fragment. It does not
     *                   have to be a child of this task fragment, but must belong to the same task.
@@ -162,10 +173,8 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
    private void createTaskFragmentAndReparentActivity(
            WindowContainerTransaction wct, IBinder fragmentToken, IBinder ownerToken,
            @NonNull Rect bounds, @WindowingMode int windowingMode, Activity activity) {
        final TaskFragmentCreationParams fragmentOptions =
                createFragmentOptions(fragmentToken, ownerToken, bounds, windowingMode);
        wct.createTaskFragment(fragmentOptions)
                .reparentActivityToTaskFragment(fragmentToken, activity.getActivityToken());
        createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode);
        wct.reparentActivityToTaskFragment(fragmentToken, activity.getActivityToken());
    }

    /**
@@ -176,11 +185,8 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer {
            WindowContainerTransaction wct, IBinder fragmentToken, IBinder ownerToken,
            @NonNull Rect bounds, @WindowingMode int windowingMode, Intent activityIntent,
            @Nullable Bundle activityOptions) {
        final TaskFragmentCreationParams fragmentOptions =
                createFragmentOptions(fragmentToken, ownerToken, bounds, windowingMode);
        wct.createTaskFragment(fragmentOptions)
                .startActivityInTaskFragment(fragmentToken, ownerToken, activityIntent,
                        activityOptions);
        createTaskFragment(wct, fragmentToken, ownerToken, bounds, windowingMode);
        wct.startActivityInTaskFragment(fragmentToken, ownerToken, activityIntent, activityOptions);
    }

    TaskFragmentCreationParams createFragmentOptions(IBinder fragmentToken, IBinder ownerToken,
+47 −2
Original line number Diff line number Diff line
@@ -20,9 +20,12 @@ 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.Application.ActivityLifecycleCallbacks;
import android.app.Instrumentation;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
@@ -61,9 +64,13 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen

    public SplitController() {
        mPresenter = new SplitPresenter(new MainThreadExecutor(), this);
        ActivityThread activityThread = ActivityThread.currentActivityThread();
        // Register a callback to be notified about activities being created.
        ActivityThread.currentActivityThread().getApplication().registerActivityLifecycleCallbacks(
        activityThread.getApplication().registerActivityLifecycleCallbacks(
                new LifecycleCallbacks());
        // Intercept activity starts to route activities to new containers if necessary.
        Instrumentation instrumentation = activityThread.getInstrumentation();
        instrumentation.addMonitor(new ActivityStartMonitor());
    }

    public void setSplitRules(@NonNull List<ExtensionSplitRule> splitRules) {
@@ -118,7 +125,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
        }

        container.setInfo(taskFragmentInfo);
        if (taskFragmentInfo.isEmpty()) {
        // Check if there are no running activities - consider the container empty if there are no
        // non-finishing activities left.
        if (!taskFragmentInfo.hasRunningActivity()) {
            cleanupContainer(container, true /* shouldFinishDependent */);
            updateCallbackIfNecessary();
        }
@@ -664,4 +673,40 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen
            handler.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(Context who, Intent intent,
                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;

            final ExtensionSplitPairRule splitPairRule = getSplitRule(
                    launchingActivity.getComponentName(), intent.getComponent(), getSplitRules());
            if (splitPairRule == null) {
                return super.onStartActivity(who, intent, options);
            }

            // Create a new split with an empty side container
            final TaskFragmentContainer 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 super.onStartActivity(who, intent, options);
        }
    }
}
+62 −35
Original line number Diff line number Diff line
@@ -82,6 +82,37 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {
        applyTransaction(wct);
    }

    /**
     * Creates a new split with the primary activity and an empty secondary container.
     * @return The newly created secondary container.
     */
    TaskFragmentContainer createNewSplitWithEmptySideContainer(@NonNull Activity primaryActivity,
            @NonNull ExtensionSplitPairRule rule) {
        final WindowContainerTransaction wct = new WindowContainerTransaction();

        final Rect parentBounds = getParentContainerBounds(primaryActivity);
        final Rect primaryRectBounds = getBoundsForPosition(POSITION_LEFT, parentBounds, rule);
        final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct,
                primaryActivity, primaryRectBounds, null);

        // Create new empty task fragment
        TaskFragmentContainer secondaryContainer = mController.newContainer(null);
        final Rect secondaryRectBounds = getBoundsForPosition(POSITION_RIGHT, parentBounds, rule);
        createTaskFragment(wct, secondaryContainer.getTaskFragmentToken(),
                primaryActivity.getActivityToken(), secondaryRectBounds,
                WINDOWING_MODE_MULTI_WINDOW);
        secondaryContainer.setLastRequestedBounds(secondaryRectBounds);

        // Set adjacent to each other so that the containers below will be invisible.
        wct.setAdjacentTaskFragments(
                primaryContainer.getTaskFragmentToken(), secondaryContainer.getTaskFragmentToken());
        applyTransaction(wct);

        mController.registerSplit(primaryContainer, primaryActivity, secondaryContainer, rule);

        return secondaryContainer;
    }

    /**
     * Creates a new split container with the two provided activities.
     * @param primaryActivity An activity that should be in the primary container. If it is not
@@ -99,55 +130,51 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer {

        final Rect parentBounds = getParentContainerBounds(primaryActivity);
        final Rect primaryRectBounds = getBoundsForPosition(POSITION_LEFT, parentBounds, rule);
        TaskFragmentContainer primaryContainer = mController.getContainerWithActivity(
                primaryActivity.getActivityToken());
        if (primaryContainer == null) {
            primaryContainer = mController.newContainer(primaryActivity);
        final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct,
                primaryActivity, primaryRectBounds, null);

            final TaskFragmentCreationParams fragmentOptions =
                    createFragmentOptions(
                            primaryContainer.getTaskFragmentToken(),
                            primaryActivity.getActivityToken(),
                            primaryRectBounds,
                            WINDOWING_MODE_MULTI_WINDOW);
            wct.createTaskFragment(fragmentOptions);
        final Rect secondaryRectBounds = getBoundsForPosition(POSITION_RIGHT, parentBounds, rule);
        final TaskFragmentContainer secondaryContainer = prepareContainerForActivity(wct,
                secondaryActivity, secondaryRectBounds, primaryContainer);

            wct.reparentActivityToTaskFragment(primaryContainer.getTaskFragmentToken(),
                    primaryActivity.getActivityToken());
        // Set adjacent to each other so that the containers below will be invisible.
        wct.setAdjacentTaskFragments(
                primaryContainer.getTaskFragmentToken(), secondaryContainer.getTaskFragmentToken());
        applyTransaction(wct);

            primaryContainer.setLastRequestedBounds(primaryRectBounds);
        } else {
            resizeTaskFragmentIfRegistered(wct, primaryContainer, primaryRectBounds);
        mController.registerSplit(primaryContainer, primaryActivity, secondaryContainer, rule);
    }

        final Rect secondaryRectBounds = getBoundsForPosition(POSITION_RIGHT, parentBounds, rule);
        TaskFragmentContainer secondaryContainer = mController.getContainerWithActivity(
                secondaryActivity.getActivityToken());
        if (secondaryContainer == null || secondaryContainer == primaryContainer) {
            secondaryContainer = mController.newContainer(secondaryActivity);
    /**
     * Creates a new container or resizes an existing container for activity to the provided bounds.
     * @param activity The activity to be re-parented to the container if necessary.
     * @param containerToAvoid Re-parent from this container if an activity is already in it.
     */
    private TaskFragmentContainer prepareContainerForActivity(
            @NonNull WindowContainerTransaction wct, @NonNull Activity activity,
            @NonNull Rect bounds, @Nullable TaskFragmentContainer containerToAvoid) {
        TaskFragmentContainer container = mController.getContainerWithActivity(
                activity.getActivityToken());
        if (container == null || container == containerToAvoid) {
            container = mController.newContainer(activity);

            final TaskFragmentCreationParams fragmentOptions =
                    createFragmentOptions(
                            secondaryContainer.getTaskFragmentToken(),
                            secondaryActivity.getActivityToken(),
                            secondaryRectBounds,
                            container.getTaskFragmentToken(),
                            activity.getActivityToken(),
                            bounds,
                            WINDOWING_MODE_MULTI_WINDOW);
            wct.createTaskFragment(fragmentOptions);

            wct.reparentActivityToTaskFragment(secondaryContainer.getTaskFragmentToken(),
                    secondaryActivity.getActivityToken());
            wct.reparentActivityToTaskFragment(container.getTaskFragmentToken(),
                    activity.getActivityToken());

            secondaryContainer.setLastRequestedBounds(secondaryRectBounds);
            container.setLastRequestedBounds(bounds);
        } else {
            resizeTaskFragmentIfRegistered(wct, secondaryContainer, secondaryRectBounds);
            resizeTaskFragmentIfRegistered(wct, container, bounds);
        }

        // Set adjacent to each other so that the containers below will be invisible.
        wct.setAdjacentTaskFragments(
                primaryContainer.getTaskFragmentToken(), secondaryContainer.getTaskFragmentToken());
        applyTransaction(wct);

        mController.registerSplit(primaryContainer, primaryActivity, secondaryContainer, rule);
        return container;
    }

    /**
Loading