From 94354a9a7f3010f2881729b3e47eba18d8ed1001 Mon Sep 17 00:00:00 2001 From: Andrii Kulian Date: Wed, 28 Jul 2021 16:07:53 -0700 Subject: [PATCH] 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 --- core/java/android/app/ActivityOptions.java | 24 ++++ core/java/android/app/Instrumentation.java | 41 ++++++- .../JetpackTaskFragmentOrganizer.java | 24 ++-- .../extensions/organizer/SplitController.java | 49 +++++++- .../extensions/organizer/SplitPresenter.java | 107 +++++++++++------- .../android/server/wm/ActivityStarter.java | 5 + .../com/android/server/wm/TaskFragment.java | 7 ++ .../server/wm/WindowOrganizerController.java | 5 + 8 files changed, 205 insertions(+), 57 deletions(-) diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index 8e1f263ebf03c..76f873185267a 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -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); } diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index b2184fe65887c..45db0f6ff3e29 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -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++; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/JetpackTaskFragmentOrganizer.java index 0f2f23ebdbf13..531df30a4e2cb 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/JetpackTaskFragmentOrganizer.java @@ -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, diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitController.java index e43c5bf71c050..a1ebb59c38220 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitController.java @@ -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 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); + } + } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitPresenter.java index 7ad83aafe7718..d8b1d2a623833 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/organizer/SplitPresenter.java @@ -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,48 +130,12 @@ 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 TaskFragmentCreationParams fragmentOptions = - createFragmentOptions( - primaryContainer.getTaskFragmentToken(), - primaryActivity.getActivityToken(), - primaryRectBounds, - WINDOWING_MODE_MULTI_WINDOW); - wct.createTaskFragment(fragmentOptions); - - wct.reparentActivityToTaskFragment(primaryContainer.getTaskFragmentToken(), - primaryActivity.getActivityToken()); - - primaryContainer.setLastRequestedBounds(primaryRectBounds); - } else { - resizeTaskFragmentIfRegistered(wct, primaryContainer, primaryRectBounds); - } + final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct, + primaryActivity, primaryRectBounds, null); final Rect secondaryRectBounds = getBoundsForPosition(POSITION_RIGHT, parentBounds, rule); - TaskFragmentContainer secondaryContainer = mController.getContainerWithActivity( - secondaryActivity.getActivityToken()); - if (secondaryContainer == null || secondaryContainer == primaryContainer) { - secondaryContainer = mController.newContainer(secondaryActivity); - - final TaskFragmentCreationParams fragmentOptions = - createFragmentOptions( - secondaryContainer.getTaskFragmentToken(), - secondaryActivity.getActivityToken(), - secondaryRectBounds, - WINDOWING_MODE_MULTI_WINDOW); - wct.createTaskFragment(fragmentOptions); - - wct.reparentActivityToTaskFragment(secondaryContainer.getTaskFragmentToken(), - secondaryActivity.getActivityToken()); - - secondaryContainer.setLastRequestedBounds(secondaryRectBounds); - } else { - resizeTaskFragmentIfRegistered(wct, secondaryContainer, secondaryRectBounds); - } + final TaskFragmentContainer secondaryContainer = prepareContainerForActivity(wct, + secondaryActivity, secondaryRectBounds, primaryContainer); // Set adjacent to each other so that the containers below will be invisible. wct.setAdjacentTaskFragments( @@ -150,6 +145,38 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { mController.registerSplit(primaryContainer, primaryActivity, secondaryContainer, rule); } + /** + * 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( + container.getTaskFragmentToken(), + activity.getActivityToken(), + bounds, + WINDOWING_MODE_MULTI_WINDOW); + wct.createTaskFragment(fragmentOptions); + + wct.reparentActivityToTaskFragment(container.getTaskFragmentToken(), + activity.getActivityToken()); + + container.setLastRequestedBounds(bounds); + } else { + resizeTaskFragmentIfRegistered(wct, container, bounds); + } + + return container; + } + /** * Starts a new activity to the side, creating a new split container. A new container will be * created for the activity that will be started. diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 395b25d7e6ad5..1da1aa3e5e1d1 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -2384,6 +2384,11 @@ class ActivityStarter { } mTransientLaunch = mOptions.getTransientLaunch(); mTargetRootTask = Task.fromWindowContainerToken(mOptions.getLaunchRootTask()); + + if (inTaskFragment == null) { + inTaskFragment = TaskFragment.fromTaskFragmentToken( + mOptions.getLaunchTaskFragmentToken(), mService); + } } mNotTop = (mLaunchFlags & FLAG_ACTIVITY_PREVIOUS_IS_TOP) != 0 ? sourceRecord : null; diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 255e4432753e8..17499060870c3 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -293,6 +293,13 @@ class TaskFragment extends WindowContainer { mRemoteToken = new RemoteToken(this); } + @NonNull + static TaskFragment fromTaskFragmentToken(@Nullable IBinder token, + @NonNull ActivityTaskManagerService service) { + if (token == null) return null; + return service.mWindowOrganizerController.getTaskFragment(token); + } + void setAdjacentTaskFragment(@Nullable TaskFragment taskFragment) { if (mAdjacentTaskFragment == taskFragment) { return; diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index e62a6e4e0ef9e..b5d98a6b0b772 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -1174,6 +1174,11 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub taskFragment.removeImmediately(); } + @Nullable + TaskFragment getTaskFragment(IBinder tfToken) { + return mLaunchTaskFragments.get(tfToken); + } + static class CallerInfo { final int mPid; final int mUid;