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;