diff options
| author | 2024-01-09 22:29:03 +0000 | |
|---|---|---|
| committer | 2024-02-09 16:49:41 +0000 | |
| commit | ddcfd119a393e133f5831d1714cae7de001ab66e (patch) | |
| tree | b4699f8b8a9f49b35c6a31e5f18211714ab1dfdc | |
| parent | ebdb4991c62cd0aff0b42155c73caf04d478d64b (diff) | |
Add Config-at-end directive/support to Transitions
This is basically a way to tell the transition system (and WM)
that activities in a specific container should not have their
clients receive configuration changes until the END of the
transition animation.
Usage amounts to calling setConfigAtEnd(wtoken) on a WCT.
Bug: 202201326
Bug: 290992727
Test: atest TransitionTests#testConfigAtEnd
Change-Id: If4728eab6686a1d0e081af128606af150e0d7cc2
5 files changed, 238 insertions, 50 deletions
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index feae173f3e61..15b9b788bca9 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -159,8 +159,11 @@ public final class TransitionInfo implements Parcelable { */ public static final int FLAG_SYNC = 1 << 21; + /** This change represents its start configuration for the duration of the animation. */ + public static final int FLAG_CONFIG_AT_END = 1 << 22; + /** The first unused bit. This can be used by remotes to attach custom flags to this change. */ - public static final int FLAG_FIRST_CUSTOM = 1 << 22; + public static final int FLAG_FIRST_CUSTOM = 1 << 23; /** The change belongs to a window that won't contain activities. */ public static final int FLAGS_IS_NON_APP_WINDOW = @@ -193,6 +196,7 @@ public final class TransitionInfo implements Parcelable { FLAG_TASK_LAUNCHING_BEHIND, FLAG_MOVED_TO_TOP, FLAG_SYNC, + FLAG_CONFIG_AT_END, FLAG_FIRST_CUSTOM }) public @interface ChangeFlags {} diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java index d9b5b2d725e2..efc71d7b43de 100644 --- a/core/java/android/window/WindowContainerTransaction.java +++ b/core/java/android/window/WindowContainerTransaction.java @@ -914,6 +914,23 @@ public final class WindowContainerTransaction implements Parcelable { } /** + * Defers client-facing configuration changes for activities in `container` until the end of + * the transition animation. The configuration will still be applied to the WMCore hierarchy + * at the normal time (beginning); so, special consideration must be made for this in the + * animation. + * + * @param container WindowContainerToken who's children should defer config notification. + * @hide + */ + @NonNull + public WindowContainerTransaction deferConfigToTransitionEnd( + @NonNull WindowContainerToken container) { + final Change change = getOrCreateChange(container.asBinder()); + change.mConfigAtTransitionEnd = true; + return this; + } + + /** * Merges another WCT into this one. * @param transfer When true, this will transfer everything from other potentially leaving * other in an unusable state. When false, other is left alone, but @@ -1050,6 +1067,7 @@ public final class WindowContainerTransaction implements Parcelable { private Rect mBoundsChangeSurfaceBounds = null; @Nullable private Rect mRelativeBounds = null; + private boolean mConfigAtTransitionEnd = false; private int mActivityWindowingMode = -1; private int mWindowingMode = -1; @@ -1082,6 +1100,7 @@ public final class WindowContainerTransaction implements Parcelable { mRelativeBounds = new Rect(); mRelativeBounds.readFromParcel(in); } + mConfigAtTransitionEnd = in.readBoolean(); mWindowingMode = in.readInt(); mActivityWindowingMode = in.readInt(); @@ -1134,6 +1153,8 @@ public final class WindowContainerTransaction implements Parcelable { ? other.mRelativeBounds : new Rect(other.mRelativeBounds); } + mConfigAtTransitionEnd = mConfigAtTransitionEnd + || other.mConfigAtTransitionEnd; } public int getWindowingMode() { @@ -1191,6 +1212,11 @@ public final class WindowContainerTransaction implements Parcelable { return mDragResizing; } + /** Gets whether the config should be sent to the client at the end of the transition. */ + public boolean getConfigAtTransitionEnd() { + return mConfigAtTransitionEnd; + } + public int getChangeMask() { return mChangeMask; } @@ -1269,6 +1295,9 @@ public final class WindowContainerTransaction implements Parcelable { if ((mChangeMask & CHANGE_RELATIVE_BOUNDS) != 0) { sb.append("relativeBounds:").append(mRelativeBounds).append(","); } + if (mConfigAtTransitionEnd) { + sb.append("configAtTransitionEnd").append(","); + } sb.append("}"); return sb.toString(); } @@ -1297,6 +1326,7 @@ public final class WindowContainerTransaction implements Parcelable { if (mRelativeBounds != null) { mRelativeBounds.writeToParcel(dest, flags); } + dest.writeBoolean(mConfigAtTransitionEnd); dest.writeInt(mWindowingMode); dest.writeInt(mActivityWindowingMode); diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 2accf9a2a43a..5d9c42d4c3a8 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -44,6 +44,7 @@ import static android.view.WindowManager.TransitionType; import static android.view.WindowManager.transitTypeToString; import static android.window.TaskFragmentAnimationParams.DEFAULT_ANIMATION_BACKGROUND_COLOR; import static android.window.TransitionInfo.FLAGS_IS_OCCLUDED_NO_ANIMATION; +import static android.window.TransitionInfo.FLAG_CONFIG_AT_END; import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS; import static android.window.TransitionInfo.FLAG_FILLS_TASK; import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; @@ -65,6 +66,7 @@ import static com.android.server.wm.ActivityRecord.State.RESUMED; import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_RECENTS_ANIM; import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_SPLASH_SCREEN; import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_WINDOWS_DRAWN; +import static com.android.server.wm.WindowState.BLAST_TIMEOUT_DURATION; import android.annotation.IntDef; import android.annotation.NonNull; @@ -307,6 +309,12 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { */ int mAnimationTrack = 0; + /** + * List of activities whose configurations are sent to the client at the end of the transition + * instead of immediately when the configuration changes. + */ + ArrayList<ActivityRecord> mConfigAtEndActivities = null; + Transition(@TransitionType int type, @TransitionFlags int flags, TransitionController controller, BLASTSyncEngine syncEngine) { mType = type; @@ -484,6 +492,22 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { return mTargetDisplays.contains(dc); } + void setConfigAtEnd(@NonNull WindowContainer<?> wc) { + wc.forAllActivities(ar -> { + if (!ar.isVisible() || !ar.isVisibleRequested()) return; + if (mConfigAtEndActivities == null) { + mConfigAtEndActivities = new ArrayList<>(); + } + if (mConfigAtEndActivities.contains(ar)) { + return; + } + mConfigAtEndActivities.add(ar); + ar.pauseConfigurationDispatch(); + }); + snapshotStartState(wc); + mChanges.get(wc).mFlags |= ChangeInfo.FLAG_CHANGE_CONFIG_AT_END; + } + /** Set a transition to be a seamless-rotation. */ void setSeamlessRotation(@NonNull WindowContainer wc) { final ChangeInfo info = mChanges.get(wc); @@ -644,20 +668,8 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Collecting in transition %d: %s", mSyncId, wc); - // "snapshot" all parents (as potential promotion targets). Do this before checking - // if this is already a participant in case it has since been re-parented. - for (WindowContainer<?> curr = getAnimatableParent(wc); - curr != null && !mChanges.containsKey(curr); - curr = getAnimatableParent(curr)) { - final ChangeInfo info = new ChangeInfo(curr); - updateTransientFlags(info); - mChanges.put(curr, info); - if (isReadyGroup(curr)) { - mReadyTrackerOld.addGroup(curr); - ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " Creating Ready-group for" - + " Transition %d with root=%s", mSyncId, curr); - } - } + // Snapshot before checking if this is a participant in case it has been re-parented. + snapshotStartState(getAnimatableParent(wc)); if (mParticipants.contains(wc)) return; // Transient-hide may be hidden later, so no need to request redraw. if (!isInTransientHide(wc)) { @@ -688,6 +700,22 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } } + /** "snapshot" `wc` and all its parents (as potential promotion targets). */ + private void snapshotStartState(@NonNull WindowContainer<?> wc) { + for (WindowContainer<?> curr = wc; + curr != null && !mChanges.containsKey(curr); + curr = getAnimatableParent(curr)) { + final ChangeInfo info = new ChangeInfo(curr); + updateTransientFlags(info); + mChanges.put(curr, info); + if (isReadyGroup(curr)) { + mReadyTrackerOld.addGroup(curr); + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " Creating Ready-group for" + + " Transition %d with root=%s", mSyncId, curr); + } + } + } + private void updateTransientFlags(@NonNull ChangeInfo info) { final WindowContainer<?> wc = info.mContainer; // Only look at tasks, taskfragments, or activities @@ -934,47 +962,60 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } /** + * Populates `t` with instructions to reset surface transform of `change` so it matches + * the WM hierarchy. This "undoes" lingering state left by the animation. + */ + private void resetSurfaceTransform(SurfaceControl.Transaction t, WindowContainer target, + SurfaceControl targetLeash) { + final Point tmpPos = new Point(); + target.getRelativePosition(tmpPos); + t.setPosition(targetLeash, tmpPos.x, tmpPos.y); + // No need to clip the display in case seeing the clipped content when during the + // display rotation. No need to clip activities because they rely on clipping on + // task layers. + if (target.asTaskFragment() == null) { + t.setCrop(targetLeash, null /* crop */); + } else { + // Crop to the resolved override bounds. + final Rect clipRect = target.getResolvedOverrideBounds(); + t.setWindowCrop(targetLeash, clipRect.width(), clipRect.height()); + } + t.setMatrix(targetLeash, 1, 0, 0, 1); + // The bounds sent to the transition is always a real bounds. This means we lose + // information about "null" bounds (inheriting from parent). Core will fix-up + // non-organized window surface bounds; however, since Core can't touch organized + // surfaces, add the "inherit from parent" restoration here. + if (target.isOrganized() && target.matchParentBounds()) { + t.setWindowCrop(targetLeash, -1, -1); + } + } + + /** * Build a transaction that "resets" all the re-parenting and layer changes. This is * intended to be applied at the end of the transition but before the finish callback. This * needs to be passed/applied in shell because until finish is called, shell owns the surfaces. * Additionally, this gives shell the ability to better deal with merged transitions. */ private void buildFinishTransaction(SurfaceControl.Transaction t, TransitionInfo info) { - final Point tmpPos = new Point(); // usually only size 1 final ArraySet<DisplayContent> displays = new ArraySet<>(); for (int i = mTargets.size() - 1; i >= 0; --i) { - final WindowContainer target = mTargets.get(i).mContainer; - if (target.getParent() != null) { - final SurfaceControl targetLeash = getLeashSurface(target, null /* t */); - final SurfaceControl origParent = getOrigParentSurface(target); - // Ensure surfaceControls are re-parented back into the hierarchy. - t.reparent(targetLeash, origParent); - t.setLayer(targetLeash, target.getLastLayer()); - target.getRelativePosition(tmpPos); - t.setPosition(targetLeash, tmpPos.x, tmpPos.y); - // No need to clip the display in case seeing the clipped content when during the - // display rotation. No need to clip activities because they rely on clipping on - // task layers. - if (target.asTaskFragment() == null) { - t.setCrop(targetLeash, null /* crop */); - } else { - // Crop to the resolved override bounds. - final Rect clipRect = target.getResolvedOverrideBounds(); - t.setWindowCrop(targetLeash, clipRect.width(), clipRect.height()); - } - t.setCornerRadius(targetLeash, 0); - t.setShadowRadius(targetLeash, 0); - t.setMatrix(targetLeash, 1, 0, 0, 1); - t.setAlpha(targetLeash, 1); - // The bounds sent to the transition is always a real bounds. This means we lose - // information about "null" bounds (inheriting from parent). Core will fix-up - // non-organized window surface bounds; however, since Core can't touch organized - // surfaces, add the "inherit from parent" restoration here. - if (target.isOrganized() && target.matchParentBounds()) { - t.setWindowCrop(targetLeash, -1, -1); - } - displays.add(target.getDisplayContent()); + final WindowContainer<?> target = mTargets.get(i).mContainer; + if (target.getParent() == null) continue; + final SurfaceControl targetLeash = getLeashSurface(target, null /* t */); + final SurfaceControl origParent = getOrigParentSurface(target); + // Ensure surfaceControls are re-parented back into the hierarchy. + t.reparent(targetLeash, origParent); + t.setLayer(targetLeash, target.getLastLayer()); + t.setCornerRadius(targetLeash, 0); + t.setShadowRadius(targetLeash, 0); + t.setAlpha(targetLeash, 1); + displays.add(target.getDisplayContent()); + // For config-at-end, the end-transform will be reset after the config is actually + // applied in the client (since the transform depends on config). The other properties + // remain here because shell might want to persistently override them. + if ((mTargets.get(i).mFlags & ChangeInfo.FLAG_CHANGE_CONFIG_AT_END) == 0) { + resetSurfaceTransform(t, target, targetLeash); } } // Remove screenshot layers if necessary @@ -1304,6 +1345,8 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { mController.mAtm.mRootWindowContainer.rankTaskLayers(); } + commitConfigAtEndActivities(); + // dispatch legacy callback in a different loop. This is because multiple legacy handlers // (fixed-rotation/displaycontent) make global changes, so we want to ensure that we've // processed all the participants first (in particular, we want to trigger pip-enter first) @@ -1421,6 +1464,52 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { mController.updateAnimatingState(); } + private void commitConfigAtEndActivities() { + if (mConfigAtEndActivities == null || mConfigAtEndActivities.isEmpty()) { + return; + } + final SurfaceControl.Transaction t = + mController.mAtm.mWindowManager.mTransactionFactory.get(); + for (int i = 0; i < mTargets.size(); ++i) { + final WindowContainer target = mTargets.get(i).mContainer; + if (target.getParent() == null || (mTargets.get(i).mFlags + & ChangeInfo.FLAG_CHANGE_CONFIG_AT_END) == 0) { + continue; + } + final SurfaceControl targetLeash = getLeashSurface(target, null /* t */); + // Reset surface state here (since it was skipped in buildFinishTransaction). Since + // we are resuming config to the "current" state, we have to calculate the matching + // surface state now (rather than snapshotting it at animation start). + resetSurfaceTransform(t, target, targetLeash); + } + + // Now we resume the configuration dispatch, wait until the now resumed configs have been + // drawn, and then apply everything together. + final BLASTSyncEngine.SyncGroup sg = mSyncEngine.prepareSyncSet( + new BLASTSyncEngine.TransactionReadyListener() { + @Override + public void onTransactionReady(int mSyncId, + SurfaceControl.Transaction transaction) { + t.merge(transaction); + t.apply(); + } + + @Override + public void onTransactionCommitTimeout() { + t.apply(); + } + }, "ConfigAtTransitEnd"); + final int syncId = sg.mSyncId; + mSyncEngine.startSyncSet(sg, BLAST_TIMEOUT_DURATION, true /* parallel */); + mSyncEngine.setSyncMethod(syncId, BLASTSyncEngine.METHOD_BLAST); + for (int i = 0; i < mConfigAtEndActivities.size(); ++i) { + final ActivityRecord ar = mConfigAtEndActivities.get(i); + mSyncEngine.addToSyncSet(syncId, ar); + ar.resumeConfigurationDispatch(); + } + mSyncEngine.setReady(syncId); + } + @Nullable private ActivityRecord getVisibleTransientLaunch(TaskDisplayArea taskDisplayArea) { if (mTransientLaunches == null) return null; @@ -1546,6 +1635,12 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { if (mState == STATE_ABORT) { mController.onAbort(this); + if (mConfigAtEndActivities != null) { + for (int i = 0; i < mConfigAtEndActivities.size(); ++i) { + mConfigAtEndActivities.get(i).resumeConfigurationDispatch(); + } + mConfigAtEndActivities = null; + } primaryDisplay.getPendingTransaction().merge(transaction); mSyncId = -1; mOverrideOptions = null; @@ -2291,6 +2386,11 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } else { parentChange.mFlags |= ChangeInfo.FLAG_CHANGE_YES_ANIMATION; } + final ActivityRecord ar = targetChange.mContainer.asActivityRecord(); + if ((ar != null && ar.isConfigurationDispatchPaused()) + || ((targetChange.mFlags & ChangeInfo.FLAG_CHANGE_CONFIG_AT_END) != 0)) { + parentChange.mFlags |= ChangeInfo.FLAG_CHANGE_CONFIG_AT_END; + } } } @@ -2940,6 +3040,9 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { /** Whether this change's container moved to the top. */ private static final int FLAG_CHANGE_MOVED_TO_TOP = 0x20; + /** Whether this change contains config-at-end members. */ + private static final int FLAG_CHANGE_CONFIG_AT_END = 0x40; + @IntDef(prefix = { "FLAG_" }, value = { FLAG_NONE, FLAG_SEAMLESS_ROTATION, @@ -2947,7 +3050,8 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { FLAG_ABOVE_TRANSIENT_LAUNCH, FLAG_CHANGE_NO_ANIMATION, FLAG_CHANGE_YES_ANIMATION, - FLAG_CHANGE_MOVED_TO_TOP + FLAG_CHANGE_MOVED_TO_TOP, + FLAG_CHANGE_CONFIG_AT_END }) @Retention(RetentionPolicy.SOURCE) @interface Flag {} @@ -3095,6 +3199,9 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { flags |= FLAG_IS_VOICE_INTERACTION; } flags |= record.mTransitionChangeFlags; + if (record.isConfigurationDispatchPaused()) { + flags |= FLAG_CONFIG_AT_END; + } } final TaskFragment taskFragment = wc.asTaskFragment(); if (taskFragment != null && task == null) { @@ -3140,6 +3247,9 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { if ((mFlags & FLAG_CHANGE_MOVED_TO_TOP) != 0) { flags |= FLAG_MOVED_TO_TOP; } + if ((mFlags & FLAG_CHANGE_CONFIG_AT_END) != 0) { + flags |= FLAG_CONFIG_AT_END; + } return flags; } diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 3f889c01bafb..90d3276770b2 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -583,8 +583,21 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } final List<WindowContainerTransaction.HierarchyOp> hops = t.getHierarchyOps(); final int hopSize = hops.size(); - Iterator<Map.Entry<IBinder, WindowContainerTransaction.Change>> entries = - t.getChanges().entrySet().iterator(); + Iterator<Map.Entry<IBinder, WindowContainerTransaction.Change>> entries; + if (transition != null) { + // Mark any config-at-end containers before applying config changes so that + // the config changes don't dispatch to client. + entries = t.getChanges().entrySet().iterator(); + while (entries.hasNext()) { + final Map.Entry<IBinder, WindowContainerTransaction.Change> entry = + entries.next(); + if (!entry.getValue().getConfigAtTransitionEnd()) continue; + final WindowContainer wc = WindowContainer.fromBinder(entry.getKey()); + if (wc == null || !wc.isAttached()) continue; + transition.setConfigAtEnd(wc); + } + } + entries = t.getChanges().entrySet().iterator(); while (entries.hasNext()) { final Map.Entry<IBinder, WindowContainerTransaction.Change> entry = entries.next(); final WindowContainer wc = WindowContainer.fromBinder(entry.getKey()); diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index 7d8eb90ea79c..ce890f6d8751 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -33,6 +33,7 @@ import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.window.TransitionInfo.FLAG_CONFIG_AT_END; import static android.window.TransitionInfo.FLAG_FILLS_TASK; import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; @@ -2540,6 +2541,36 @@ public class TransitionTests extends WindowTestsBase { } @Test + public void testConfigAtEnd() { + final TransitionController controller = mDisplayContent.mTransitionController; + Transition transit = createTestTransition(TRANSIT_CHANGE, controller); + final TestTransitionPlayer player = registerTestTransitionPlayer(); + + final Task task = createTask(mDisplayContent); + final Rect taskBounds = new Rect(0, 0, 200, 300); + task.getConfiguration().windowConfiguration.setBounds(taskBounds); + final ActivityRecord activity = createActivityRecord(task); + activity.setVisibleRequested(true); + activity.setVisible(true); + + controller.moveToCollecting(transit); + transit.collect(task); + transit.setConfigAtEnd(task); + task.getRequestedOverrideConfiguration().windowConfiguration.setBounds( + new Rect(10, 10, 200, 300)); + task.onRequestedOverrideConfigurationChanged(task.getRequestedOverrideConfiguration()); + + controller.requestStartTransition(transit, task, null, null); + player.start(); + assertTrue(activity.isConfigurationDispatchPaused()); + // config-at-end flag must propagate up to task if activity was promoted. + assertTrue(player.mLastReady.getChange( + task.mRemoteToken.toWindowContainerToken()).hasFlags(FLAG_CONFIG_AT_END)); + player.finish(); + assertFalse(activity.isConfigurationDispatchPaused()); + } + + @Test public void testReadyTrackerBasics() { final TransitionController controller = new TestTransitionController( mock(ActivityTaskManagerService.class)); |