Loading core/java/android/window/WindowContainerTransaction.java +23 −0 Original line number Diff line number Diff line Loading @@ -649,6 +649,26 @@ public final class WindowContainerTransaction implements Parcelable { return this; } /** * Requests focus on the top running Activity in the given TaskFragment. This will only take * effect if there is no focus, or if the current focus is in the same Task as the requested * TaskFragment. * @param fragmentToken client assigned unique token to create TaskFragment with specified in * {@link TaskFragmentCreationParams#getFragmentToken()}. * @hide */ @NonNull public WindowContainerTransaction requestFocusOnTaskFragment(@NonNull IBinder fragmentToken) { final HierarchyOp hierarchyOp = new HierarchyOp.Builder( HierarchyOp.HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT) .setContainer(fragmentToken) .build(); mHierarchyOps.add(hierarchyOp); return this; } /** * When this {@link WindowContainerTransaction} failed to finish on the server side, it will * trigger callback with this {@param errorCallbackToken}. Loading Loading @@ -1057,6 +1077,7 @@ public final class WindowContainerTransaction implements Parcelable { public static final int HIERARCHY_OP_TYPE_RESTORE_TRANSIENT_ORDER = 15; public static final int HIERARCHY_OP_TYPE_ADD_RECT_INSETS_PROVIDER = 16; public static final int HIERARCHY_OP_TYPE_REMOVE_INSETS_PROVIDER = 17; public static final int HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT = 18; // The following key(s) are for use with mLaunchOptions: // When launching a task (eg. from recents), this is the taskId to be launched. Loading Loading @@ -1368,6 +1389,8 @@ public final class WindowContainerTransaction implements Parcelable { case HIERARCHY_OP_TYPE_REMOVE_INSETS_PROVIDER: return "{removeLocalInsetsProvider: container=" + mContainer + " insetsType=" + Arrays.toString(mInsetsTypes) + "}"; case HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT: return "{requestFocusOnTaskFragment: container=" + mContainer + "}"; default: return "{mType=" + mType + " container=" + mContainer + " reparent=" + mReparent + " mToTop=" + mToTop Loading libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +5 −4 Original line number Diff line number Diff line Loading @@ -112,9 +112,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen */ public void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent intent, @Nullable Bundle options, @NonNull SplitRule sideRule, @Nullable Consumer<Exception> failureCallback) { @Nullable Consumer<Exception> failureCallback, boolean isPlaceholder) { try { mPresenter.startActivityToSide(launchingActivity, intent, options, sideRule); mPresenter.startActivityToSide(launchingActivity, intent, options, sideRule, isPlaceholder); } catch (Exception e) { if (failureCallback != null) { failureCallback.accept(e); Loading Loading @@ -710,8 +711,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } // TODO(b/190433398): Handle failed request startActivityToSide(activity, placeholderRule.getPlaceholderIntent(), null, placeholderRule, null); startActivityToSide(activity, placeholderRule.getPlaceholderIntent(), null /* options */, placeholderRule, null /* failureCallback */, true /* isPlaceholder */); return true; } Loading libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +18 −6 Original line number Diff line number Diff line Loading @@ -220,9 +220,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * @param activityIntent The intent to start the new activity. * @param activityOptions The options to apply to new activity start. * @param rule The split rule to be applied to the container. * @param isPlaceholder Whether the launch is a placeholder. */ void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent activityIntent, @Nullable Bundle activityOptions, @NonNull SplitRule rule) { @Nullable Bundle activityOptions, @NonNull SplitRule rule, boolean isPlaceholder) { final Rect parentBounds = getParentContainerBounds(launchingActivity); final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, isLtr(launchingActivity, rule)); Loading @@ -244,6 +245,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { startActivityToSide(wct, primaryContainer.getTaskFragmentToken(), primaryRectBounds, launchingActivity, secondaryContainer.getTaskFragmentToken(), secondaryRectBounds, activityIntent, activityOptions, rule); if (isPlaceholder) { // When placeholder is launched in split, we should keep the focus on the primary. wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken()); } applyTransaction(wct); primaryContainer.setLastRequestedBounds(primaryRectBounds); Loading Loading @@ -272,14 +277,21 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { isLtr); final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, isLtr); final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); // Whether the placeholder is becoming side-by-side with the primary from fullscreen. final boolean isPlaceholderBecomingSplit = splitContainer.isPlaceholderContainer() && secondaryContainer.areLastRequestedBoundsEqual(null /* bounds */) && !secondaryRectBounds.isEmpty(); // If the task fragments are not registered yet, the positions will be updated after they // are created again. resizeTaskFragmentIfRegistered(wct, primaryContainer, primaryRectBounds); final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); resizeTaskFragmentIfRegistered(wct, secondaryContainer, secondaryRectBounds); setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule); if (isPlaceholderBecomingSplit) { // When placeholder is shown in split, we should keep the focus on the primary. wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken()); } } private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, Loading services/core/java/com/android/server/wm/WindowOrganizerController.java +25 −0 Original line number Diff line number Diff line Loading @@ -30,6 +30,7 @@ import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_CHILDREN; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_RESTORE_TRANSIENT_ORDER; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS; Loading Loading @@ -775,6 +776,29 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } break; } case HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT: { final TaskFragment tf = mLaunchTaskFragments.get(hop.getContainer()); if (tf == null || !tf.isAttached()) { Slog.e(TAG, "Attempt to operate on detached container: " + tf); break; } final ActivityRecord curFocus = tf.getDisplayContent().mFocusedApp; if (curFocus != null && curFocus.getTaskFragment() == tf) { Slog.d(TAG, "The requested TaskFragment already has the focus."); break; } if (curFocus != null && curFocus.getTask() != tf.getTask()) { Slog.d(TAG, "The Task of the requested TaskFragment doesn't have focus."); break; } final ActivityRecord targetFocus = tf.getTopResumedActivity(); if (targetFocus == null) { Slog.d(TAG, "There is no resumed activity in the requested TaskFragment."); break; } tf.getDisplayContent().setFocusedApp(targetFocus); break; } default: { // The other operations may change task order so they are skipped while in lock // task mode. The above operations are still allowed because they don't move Loading Loading @@ -1318,6 +1342,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub case HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT: case HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT: case HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS: case HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT: // We are allowing organizer to start/reparent activity to a TaskFragment it // created, or set two TaskFragments adjacent to each other. Nothing to check // here because the TaskFragment may not be created yet, but will be created in Loading services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +49 −0 Original line number Diff line number Diff line Loading @@ -521,6 +521,55 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { verify(mAtm.mRootWindowContainer).resumeFocusedTasksTopActivities(); } @Test public void testApplyTransaction_requestFocusOnTaskFragment() { mOrganizer.applyTransaction(mTransaction); mController.registerOrganizer(mIOrganizer); final Task task = createTask(mDisplayContent); final IBinder token0 = new Binder(); final TaskFragment tf0 = new TaskFragmentBuilder(mAtm) .setParentTask(task) .setFragmentToken(token0) .setOrganizer(mOrganizer) .createActivityCount(1) .build(); final IBinder token1 = new Binder(); final TaskFragment tf1 = new TaskFragmentBuilder(mAtm) .setParentTask(task) .setFragmentToken(token1) .setOrganizer(mOrganizer) .createActivityCount(1) .build(); mAtm.mWindowOrganizerController.mLaunchTaskFragments.put(token0, tf0); mAtm.mWindowOrganizerController.mLaunchTaskFragments.put(token1, tf1); final ActivityRecord activity0 = tf0.getTopMostActivity(); final ActivityRecord activity1 = tf1.getTopMostActivity(); // No effect if the current focus is in a different Task. final ActivityRecord activityInOtherTask = createActivityRecord(mDefaultDisplay); mDisplayContent.setFocusedApp(activityInOtherTask); mTransaction.requestFocusOnTaskFragment(token0); mAtm.mWindowOrganizerController.applyTransaction(mTransaction); assertEquals(activityInOtherTask, mDisplayContent.mFocusedApp); // No effect if there is no resumed activity in the request TaskFragment. activity0.setState(ActivityRecord.State.PAUSED, "test"); activity1.setState(ActivityRecord.State.RESUMED, "test"); mDisplayContent.setFocusedApp(activity1); mAtm.mWindowOrganizerController.applyTransaction(mTransaction); assertEquals(activity1, mDisplayContent.mFocusedApp); // Set focus to the request TaskFragment when the current focus is in the same Task, and it // has a resumed activity. activity0.setState(ActivityRecord.State.RESUMED, "test"); mDisplayContent.setFocusedApp(activity1); mAtm.mWindowOrganizerController.applyTransaction(mTransaction); assertEquals(activity0, mDisplayContent.mFocusedApp); } @Test public void testTaskFragmentInPip_startActivityInTaskFragment() { setupTaskFragmentInPip(); Loading Loading
core/java/android/window/WindowContainerTransaction.java +23 −0 Original line number Diff line number Diff line Loading @@ -649,6 +649,26 @@ public final class WindowContainerTransaction implements Parcelable { return this; } /** * Requests focus on the top running Activity in the given TaskFragment. This will only take * effect if there is no focus, or if the current focus is in the same Task as the requested * TaskFragment. * @param fragmentToken client assigned unique token to create TaskFragment with specified in * {@link TaskFragmentCreationParams#getFragmentToken()}. * @hide */ @NonNull public WindowContainerTransaction requestFocusOnTaskFragment(@NonNull IBinder fragmentToken) { final HierarchyOp hierarchyOp = new HierarchyOp.Builder( HierarchyOp.HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT) .setContainer(fragmentToken) .build(); mHierarchyOps.add(hierarchyOp); return this; } /** * When this {@link WindowContainerTransaction} failed to finish on the server side, it will * trigger callback with this {@param errorCallbackToken}. Loading Loading @@ -1057,6 +1077,7 @@ public final class WindowContainerTransaction implements Parcelable { public static final int HIERARCHY_OP_TYPE_RESTORE_TRANSIENT_ORDER = 15; public static final int HIERARCHY_OP_TYPE_ADD_RECT_INSETS_PROVIDER = 16; public static final int HIERARCHY_OP_TYPE_REMOVE_INSETS_PROVIDER = 17; public static final int HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT = 18; // The following key(s) are for use with mLaunchOptions: // When launching a task (eg. from recents), this is the taskId to be launched. Loading Loading @@ -1368,6 +1389,8 @@ public final class WindowContainerTransaction implements Parcelable { case HIERARCHY_OP_TYPE_REMOVE_INSETS_PROVIDER: return "{removeLocalInsetsProvider: container=" + mContainer + " insetsType=" + Arrays.toString(mInsetsTypes) + "}"; case HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT: return "{requestFocusOnTaskFragment: container=" + mContainer + "}"; default: return "{mType=" + mType + " container=" + mContainer + " reparent=" + mReparent + " mToTop=" + mToTop Loading
libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +5 −4 Original line number Diff line number Diff line Loading @@ -112,9 +112,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen */ public void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent intent, @Nullable Bundle options, @NonNull SplitRule sideRule, @Nullable Consumer<Exception> failureCallback) { @Nullable Consumer<Exception> failureCallback, boolean isPlaceholder) { try { mPresenter.startActivityToSide(launchingActivity, intent, options, sideRule); mPresenter.startActivityToSide(launchingActivity, intent, options, sideRule, isPlaceholder); } catch (Exception e) { if (failureCallback != null) { failureCallback.accept(e); Loading Loading @@ -710,8 +711,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } // TODO(b/190433398): Handle failed request startActivityToSide(activity, placeholderRule.getPlaceholderIntent(), null, placeholderRule, null); startActivityToSide(activity, placeholderRule.getPlaceholderIntent(), null /* options */, placeholderRule, null /* failureCallback */, true /* isPlaceholder */); return true; } Loading
libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +18 −6 Original line number Diff line number Diff line Loading @@ -220,9 +220,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * @param activityIntent The intent to start the new activity. * @param activityOptions The options to apply to new activity start. * @param rule The split rule to be applied to the container. * @param isPlaceholder Whether the launch is a placeholder. */ void startActivityToSide(@NonNull Activity launchingActivity, @NonNull Intent activityIntent, @Nullable Bundle activityOptions, @NonNull SplitRule rule) { @Nullable Bundle activityOptions, @NonNull SplitRule rule, boolean isPlaceholder) { final Rect parentBounds = getParentContainerBounds(launchingActivity); final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, parentBounds, rule, isLtr(launchingActivity, rule)); Loading @@ -244,6 +245,10 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { startActivityToSide(wct, primaryContainer.getTaskFragmentToken(), primaryRectBounds, launchingActivity, secondaryContainer.getTaskFragmentToken(), secondaryRectBounds, activityIntent, activityOptions, rule); if (isPlaceholder) { // When placeholder is launched in split, we should keep the focus on the primary. wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken()); } applyTransaction(wct); primaryContainer.setLastRequestedBounds(primaryRectBounds); Loading Loading @@ -272,14 +277,21 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { isLtr); final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, parentBounds, rule, isLtr); final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); // Whether the placeholder is becoming side-by-side with the primary from fullscreen. final boolean isPlaceholderBecomingSplit = splitContainer.isPlaceholderContainer() && secondaryContainer.areLastRequestedBoundsEqual(null /* bounds */) && !secondaryRectBounds.isEmpty(); // If the task fragments are not registered yet, the positions will be updated after they // are created again. resizeTaskFragmentIfRegistered(wct, primaryContainer, primaryRectBounds); final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer(); resizeTaskFragmentIfRegistered(wct, secondaryContainer, secondaryRectBounds); setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule); if (isPlaceholderBecomingSplit) { // When placeholder is shown in split, we should keep the focus on the primary. wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken()); } } private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, Loading
services/core/java/com/android/server/wm/WindowOrganizerController.java +25 −0 Original line number Diff line number Diff line Loading @@ -30,6 +30,7 @@ import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_CHILDREN; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_RESTORE_TRANSIENT_ORDER; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS; Loading Loading @@ -775,6 +776,29 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } break; } case HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT: { final TaskFragment tf = mLaunchTaskFragments.get(hop.getContainer()); if (tf == null || !tf.isAttached()) { Slog.e(TAG, "Attempt to operate on detached container: " + tf); break; } final ActivityRecord curFocus = tf.getDisplayContent().mFocusedApp; if (curFocus != null && curFocus.getTaskFragment() == tf) { Slog.d(TAG, "The requested TaskFragment already has the focus."); break; } if (curFocus != null && curFocus.getTask() != tf.getTask()) { Slog.d(TAG, "The Task of the requested TaskFragment doesn't have focus."); break; } final ActivityRecord targetFocus = tf.getTopResumedActivity(); if (targetFocus == null) { Slog.d(TAG, "There is no resumed activity in the requested TaskFragment."); break; } tf.getDisplayContent().setFocusedApp(targetFocus); break; } default: { // The other operations may change task order so they are skipped while in lock // task mode. The above operations are still allowed because they don't move Loading Loading @@ -1318,6 +1342,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub case HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT: case HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT: case HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS: case HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT: // We are allowing organizer to start/reparent activity to a TaskFragment it // created, or set two TaskFragments adjacent to each other. Nothing to check // here because the TaskFragment may not be created yet, but will be created in Loading
services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +49 −0 Original line number Diff line number Diff line Loading @@ -521,6 +521,55 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { verify(mAtm.mRootWindowContainer).resumeFocusedTasksTopActivities(); } @Test public void testApplyTransaction_requestFocusOnTaskFragment() { mOrganizer.applyTransaction(mTransaction); mController.registerOrganizer(mIOrganizer); final Task task = createTask(mDisplayContent); final IBinder token0 = new Binder(); final TaskFragment tf0 = new TaskFragmentBuilder(mAtm) .setParentTask(task) .setFragmentToken(token0) .setOrganizer(mOrganizer) .createActivityCount(1) .build(); final IBinder token1 = new Binder(); final TaskFragment tf1 = new TaskFragmentBuilder(mAtm) .setParentTask(task) .setFragmentToken(token1) .setOrganizer(mOrganizer) .createActivityCount(1) .build(); mAtm.mWindowOrganizerController.mLaunchTaskFragments.put(token0, tf0); mAtm.mWindowOrganizerController.mLaunchTaskFragments.put(token1, tf1); final ActivityRecord activity0 = tf0.getTopMostActivity(); final ActivityRecord activity1 = tf1.getTopMostActivity(); // No effect if the current focus is in a different Task. final ActivityRecord activityInOtherTask = createActivityRecord(mDefaultDisplay); mDisplayContent.setFocusedApp(activityInOtherTask); mTransaction.requestFocusOnTaskFragment(token0); mAtm.mWindowOrganizerController.applyTransaction(mTransaction); assertEquals(activityInOtherTask, mDisplayContent.mFocusedApp); // No effect if there is no resumed activity in the request TaskFragment. activity0.setState(ActivityRecord.State.PAUSED, "test"); activity1.setState(ActivityRecord.State.RESUMED, "test"); mDisplayContent.setFocusedApp(activity1); mAtm.mWindowOrganizerController.applyTransaction(mTransaction); assertEquals(activity1, mDisplayContent.mFocusedApp); // Set focus to the request TaskFragment when the current focus is in the same Task, and it // has a resumed activity. activity0.setState(ActivityRecord.State.RESUMED, "test"); mDisplayContent.setFocusedApp(activity1); mAtm.mWindowOrganizerController.applyTransaction(mTransaction); assertEquals(activity0, mDisplayContent.mFocusedApp); } @Test public void testTaskFragmentInPip_startActivityInTaskFragment() { setupTaskFragmentInPip(); Loading