diff --git a/core/java/android/window/ITransitionPlayer.aidl b/core/java/android/window/ITransitionPlayer.aidl new file mode 100644 index 0000000000000..a8a29b26a1485 --- /dev/null +++ b/core/java/android/window/ITransitionPlayer.aidl @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.window; + +import android.view.SurfaceControl; +import android.window.TransitionInfo; +import android.window.WindowContainerTransaction; + +/** + * Implemented by WMShell to initiate and play transition animations. + * The flow (with {@link IWindowOrganizerController}) looks like this: + *

    + *
  1. Core starts an activity and calls {@link #requestStartTransition} + *
  2. This TransitionPlayer impl does whatever, then calls + * {@link IWindowOrganizerController#startTransition} to tell Core to formally start (until + * this happens, Core will collect changes on the transition, but won't consider it ready to + * animate). + *
  3. Once all collected changes on the transition have finished drawing, Core will then call + * {@link #onTransitionReady} here to delegate the actual animation. + *
  4. Once this TransitionPlayer impl finishes animating, it notifies Core via + * {@link IWindowOrganizerController#finishTransition}. At this point, ITransitionPlayer's + * responsibilities end. + * + * + * {@hide} + */ +oneway interface ITransitionPlayer { + + /** + * Called when all participants of a transition are ready to animate. This is in response to + * {@link IWindowOrganizerController#startTransition}. + * + * @param transitionToken An identifying token for the transition that is now ready to animate. + * @param info A collection of all the changes encapsulated by this transition. + * @param t A surface transaction containing the surface state prior to animating. + */ + void onTransitionReady(in IBinder transitionToken, in TransitionInfo info, + in SurfaceControl.Transaction t); + + /** + * Called when something in WMCore requires a transition to play -- for example when an Activity + * is started in a new Task. + * + * @param type The {@link WindowManager#TransitionType} of the transition to start. + * @param transitionToken An identifying token for the transition that needs to be started. + * Pass this to {@link IWindowOrganizerController#startTransition}. + */ + void requestStartTransition(int type, in IBinder transitionToken); +} diff --git a/core/java/android/window/IWindowOrganizerController.aidl b/core/java/android/window/IWindowOrganizerController.aidl index 7e9c783c83c6b..0cd9b366bf5c2 100644 --- a/core/java/android/window/IWindowOrganizerController.aidl +++ b/core/java/android/window/IWindowOrganizerController.aidl @@ -18,8 +18,10 @@ package android.window; import android.view.SurfaceControl; +import android.os.IBinder; import android.window.IDisplayAreaOrganizerController; import android.window.ITaskOrganizerController; +import android.window.ITransitionPlayer; import android.window.IWindowContainerTransactionCallback; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -45,6 +47,30 @@ interface IWindowOrganizerController { int applySyncTransaction(in WindowContainerTransaction t, in IWindowContainerTransactionCallback callback); + /** + * Starts a transition. + * @param type The transition type. + * @param transitionToken A token associated with the transition to start. If null, a new + * transition will be created of the provided type. + * @param t Operations that are part of the transition. + * @return a token representing the transition. This will just be transitionToken if it was + * non-null. + */ + IBinder startTransition(int type, in @nullable IBinder transitionToken, + in @nullable WindowContainerTransaction t); + + /** + * Finishes a transition. This must be called for all created transitions. + * @param transitionToken Which transition to finish + * @param t Changes to make before finishing but in the same SF Transaction. Can be null. + * @param callback Called when t is finished applying. + * @return An ID for the sync operation (see {@link #applySyncTransaction}. This will be + * negative if no sync transaction was attached (null t or callback) + */ + int finishTransition(in IBinder transitionToken, + in @nullable WindowContainerTransaction t, + in IWindowContainerTransactionCallback callback); + /** @return An interface enabling the management of task organizers. */ ITaskOrganizerController getTaskOrganizerController(); @@ -61,4 +87,10 @@ interface IWindowOrganizerController { * @return true if the screenshot was successful, false otherwise. */ boolean takeScreenshot(in WindowContainerToken token, out SurfaceControl outSurfaceControl); + + /** + * Registers a transition player with Core. There is only one of these at a time and calling + * this will replace the existing one if set. + */ + void registerTransitionPlayer(in ITransitionPlayer player); } diff --git a/core/java/android/window/TransitionInfo.aidl b/core/java/android/window/TransitionInfo.aidl new file mode 100644 index 0000000000000..6c33e9737f6a8 --- /dev/null +++ b/core/java/android/window/TransitionInfo.aidl @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.window; + +parcelable TransitionInfo; +parcelable TransitionInfo.Change; diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java new file mode 100644 index 0000000000000..34d1d4e8699d0 --- /dev/null +++ b/core/java/android/window/TransitionInfo.java @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.window; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.Rect; +import android.os.Parcel; +import android.os.Parcelable; +import android.view.SurfaceControl; +import android.view.WindowManager; + +import java.util.ArrayList; +import java.util.List; + +/** + * Used to communicate information about what is changing during a transition to a TransitionPlayer. + * @hide + */ +public final class TransitionInfo implements Parcelable { + + /** No transition mode. This is a placeholder, don't use this as an actual mode. */ + public static final int TRANSIT_NONE = 0; + + /** The container didn't exist before but will exist and be visible after. */ + public static final int TRANSIT_OPEN = 1; + + /** The container existed and was visible before but won't exist after. */ + public static final int TRANSIT_CLOSE = 2; + + /** The container existed before but was invisible and will be visible after. */ + public static final int TRANSIT_SHOW = 3; + + /** The container is going from visible to invisible but it will still exist after. */ + public static final int TRANSIT_HIDE = 4; + + /** The container exists and is visible before and after but it changes. */ + public static final int TRANSIT_CHANGE = 5; + + /** @hide */ + @IntDef(prefix = { "TRANSIT_" }, value = { + TRANSIT_NONE, + TRANSIT_OPEN, + TRANSIT_CLOSE, + TRANSIT_SHOW, + TRANSIT_HIDE, + TRANSIT_CHANGE + }) + public @interface TransitionMode {} + + private final @WindowManager.TransitionType int mType; + private final ArrayList mChanges = new ArrayList<>(); + + /** @hide */ + public TransitionInfo(@WindowManager.TransitionType int type) { + mType = type; + } + + private TransitionInfo(Parcel in) { + mType = in.readInt(); + in.readList(mChanges, null /* classLoader */); + } + + @Override + /** @hide */ + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mType); + dest.writeList(mChanges); + } + + @NonNull + public static final Creator CREATOR = + new Creator() { + @Override + public TransitionInfo createFromParcel(Parcel in) { + return new TransitionInfo(in); + } + + @Override + public TransitionInfo[] newArray(int size) { + return new TransitionInfo[size]; + } + }; + + @Override + /** @hide */ + public int describeContents() { + return 0; + } + + public int getType() { + return mType; + } + + @NonNull + public List getChanges() { + return mChanges; + } + + /** + * @return the Change that a window is undergoing or {@code null} if not directly + * represented. + */ + @Nullable + public Change getChange(@NonNull WindowContainerToken token) { + for (int i = mChanges.size() - 1; i >= 0; --i) { + if (mChanges.get(i).mContainer == token) { + return mChanges.get(i); + } + } + return null; + } + + /** + * Add a {@link Change} to this transition. + */ + public void addChange(@NonNull Change change) { + mChanges.add(change); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{t=" + mType + " c=["); + for (int i = 0; i < mChanges.size(); ++i) { + if (i > 0) { + sb.append(','); + } + sb.append(mChanges.get(i)); + } + sb.append("]}"); + return sb.toString(); + } + + /** Converts a transition mode/action to its string representation. */ + @NonNull + public static String modeToString(@TransitionMode int mode) { + switch(mode) { + case TRANSIT_NONE: return "NONE"; + case TRANSIT_OPEN: return "OPEN"; + case TRANSIT_CLOSE: return "CLOSE"; + case TRANSIT_SHOW: return "SHOW"; + case TRANSIT_HIDE: return "HIDE"; + case TRANSIT_CHANGE: return "CHANGE"; + default: return ""; + } + } + + /** Represents the change a WindowContainer undergoes during a transition */ + public static final class Change implements Parcelable { + private final WindowContainerToken mContainer; + private WindowContainerToken mParent; + private final SurfaceControl mLeash; + private int mMode = TRANSIT_NONE; + private final Rect mStartBounds = new Rect(); + private final Rect mEndBounds = new Rect(); + + public Change(@NonNull WindowContainerToken container, @NonNull SurfaceControl leash) { + mContainer = container; + mLeash = leash; + } + + private Change(Parcel in) { + mContainer = WindowContainerToken.CREATOR.createFromParcel(in); + mParent = in.readParcelable(WindowContainerToken.class.getClassLoader()); + mLeash = new SurfaceControl(); + mLeash.readFromParcel(in); + mMode = in.readInt(); + mStartBounds.readFromParcel(in); + mEndBounds.readFromParcel(in); + } + + /** Sets the parent of this change's container. The parent must be a participant or null. */ + public void setParent(@Nullable WindowContainerToken parent) { + mParent = parent; + } + + /** Sets the transition mode for this change */ + public void setMode(@TransitionMode int mode) { + mMode = mode; + } + + /** Sets the bounds this container occupied before the change */ + public void setStartBounds(@Nullable Rect rect) { + mStartBounds.set(rect); + } + + /** Sets the bounds this container will occupy after the change */ + public void setEndBounds(@Nullable Rect rect) { + mEndBounds.set(rect); + } + + /** @return the container that is changing */ + @NonNull + public WindowContainerToken getContainer() { + return mContainer; + } + + /** + * @return the parent of the changing container. This is the parent within the participants, + * not necessarily the actual parent. + */ + @Nullable + public WindowContainerToken getParent() { + return mParent; + } + + /** @return which action this change represents. */ + public @TransitionMode int getMode() { + return mMode; + } + + /** + * @return the bounds of the container before the change. It may be empty if the container + * is coming into existence. + */ + @NonNull + public Rect getStartBounds() { + return mStartBounds; + } + + /** + * @return the bounds of the container after the change. It may be empty if the container + * is disappearing. + */ + @NonNull + public Rect getEndBounds() { + return mEndBounds; + } + + /** @return the leash or surface to animate for this container */ + @NonNull + public SurfaceControl getLeash() { + return mLeash; + } + + @Override + /** @hide */ + public void writeToParcel(@NonNull Parcel dest, int flags) { + mContainer.writeToParcel(dest, flags); + dest.writeParcelable(mParent, 0); + mLeash.writeToParcel(dest, flags); + dest.writeInt(mMode); + mStartBounds.writeToParcel(dest, flags); + mEndBounds.writeToParcel(dest, flags); + } + + @NonNull + public static final Creator CREATOR = + new Creator() { + @Override + public Change createFromParcel(Parcel in) { + return new Change(in); + } + + @Override + public Change[] newArray(int size) { + return new Change[size]; + } + }; + + @Override + /** @hide */ + public int describeContents() { + return 0; + } + + @Override + public String toString() { + return "{" + mContainer + "(" + mParent + ") leash=" + mLeash + + " m=" + modeToString(mMode) + " sb=" + mStartBounds + + " eb=" + mEndBounds + "}"; + } + } +} diff --git a/core/java/android/window/WindowContainerToken.java b/core/java/android/window/WindowContainerToken.java index c92ccae66ff8f..96e8b44d13ccf 100644 --- a/core/java/android/window/WindowContainerToken.java +++ b/core/java/android/window/WindowContainerToken.java @@ -77,6 +77,11 @@ public final class WindowContainerToken implements Parcelable { return mRealToken.asBinder().hashCode(); } + @Override + public String toString() { + return "WCT{" + mRealToken + "}"; + } + @Override public boolean equals(Object obj) { if (!(obj instanceof WindowContainerToken)) { diff --git a/core/java/android/window/WindowOrganizer.java b/core/java/android/window/WindowOrganizer.java index 97a97d9984f92..5ac19fa685d79 100644 --- a/core/java/android/window/WindowOrganizer.java +++ b/core/java/android/window/WindowOrganizer.java @@ -19,8 +19,10 @@ package android.window; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; +import android.annotation.SuppressLint; import android.annotation.TestApi; import android.app.ActivityTaskManager; +import android.os.IBinder; import android.os.RemoteException; import android.util.Singleton; import android.view.SurfaceControl; @@ -65,6 +67,48 @@ public class WindowOrganizer { } } + /** + * Start a transition. + * @param type The type of the transition. This is ignored if a transitionToken is provided. + * @param transitionToken An existing transition to start. If null, a new transition is created. + * @param t The set of window operations that are part of this transition. + * @return A token identifying the transition. This will be the same as transitionToken if it + * was provided. + * @hide + */ + @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS) + @NonNull + public IBinder startTransition(int type, @Nullable IBinder transitionToken, + @Nullable WindowContainerTransaction t) { + try { + return getWindowOrganizerController().startTransition(type, transitionToken, t); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Finishes a running transition. + * @param transitionToken The transition to finish. Can't be null. + * @param t A set of window operations to apply before finishing. + * @param callback A sync callback (if provided). See {@link #applySyncTransaction}. + * @return An ID for the sync operation if performed. See {@link #applySyncTransaction}. + * + * @hide + */ + @SuppressLint("ExecutorRegistration") + @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS) + public int finishTransition(@NonNull IBinder transitionToken, + @Nullable WindowContainerTransaction t, + @Nullable WindowContainerTransactionCallback callback) { + try { + return getWindowOrganizerController().finishTransition(transitionToken, t, + callback != null ? callback.mInterface : null); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * Take a screenshot for a specified Window * @param token The token for the WindowContainer that should get a screenshot taken. @@ -87,6 +131,19 @@ public class WindowOrganizer { } } + /** + * Register an ITransitionPlayer to handle transition animations. + * @hide + */ + @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS) + public void registerTransitionPlayer(@Nullable ITransitionPlayer player) { + try { + getWindowOrganizerController().registerTransitionPlayer(player); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_STACKS) IWindowOrganizerController getWindowOrganizerController() { return IWindowOrganizerControllerSingleton.get(); diff --git a/core/java/com/android/internal/protolog/ProtoLogGroup.java b/core/java/com/android/internal/protolog/ProtoLogGroup.java index 9874c6aabf042..50ba42fdae2f7 100644 --- a/core/java/com/android/internal/protolog/ProtoLogGroup.java +++ b/core/java/com/android/internal/protolog/ProtoLogGroup.java @@ -74,6 +74,8 @@ public enum ProtoLogGroup implements IProtoLogGroup { Consts.TAG_WM), WM_DEBUG_SYNC_ENGINE(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM), + WM_DEBUG_WINDOW_TRANSITIONS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true, + Consts.TAG_WM), TEST_GROUP(true, true, false, "WindowManagerProtoLogTest"); private final boolean mEnabled; diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 9eaeed11c02f8..c53ea8789c8b5 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -205,6 +205,12 @@ "group": "WM_DEBUG_APP_TRANSITIONS", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, + "-1844540996": { + "message": " Initial targets: %s", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "-1838803135": { "message": "Attempted to set windowing mode to a display that does not exist: %d", "level": "WARN", @@ -343,12 +349,24 @@ "group": "WM_DEBUG_REMOTE_ANIMATIONS", "at": "com\/android\/server\/wm\/RemoteAnimationController.java" }, + "-1587921395": { + "message": " Top targets: %s", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "-1568331821": { "message": "Enabling listeners", "level": "VERBOSE", "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/DisplayRotation.java" }, + "-1567866547": { + "message": "Collecting in transition %d: %s", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "-1554521902": { "message": "showInsets(ime) was requested by different window: %s ", "level": "WARN", @@ -421,6 +439,12 @@ "group": "WM_DEBUG_ADD_REMOVE", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, + "-1452274694": { + "message": " CAN PROMOTE: promoting to parent %s", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "-1443029505": { "message": "SAFE MODE ENABLED (menu=%d s=%d dpad=%d trackball=%d)", "level": "INFO", @@ -475,6 +499,12 @@ "group": "WM_DEBUG_SYNC_ENGINE", "at": "com\/android\/server\/wm\/BLASTSyncEngine.java" }, + "-1375751630": { + "message": " --- Start combine pass ---", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "-1364754753": { "message": "Task vanished taskId=%d", "level": "VERBOSE", @@ -787,6 +817,12 @@ "group": "WM_SHOW_TRANSACTIONS", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "-855366859": { + "message": " merging children in from %s: %s", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "-853404763": { "message": "\twallpaper=%s", "level": "DEBUG", @@ -871,6 +907,12 @@ "group": "WM_DEBUG_FOCUS", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, + "-703543418": { + "message": " check sibling %s", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "-694710814": { "message": "Pausing rotation during drag", "level": "DEBUG", @@ -925,6 +967,12 @@ "group": "WM_DEBUG_ADD_REMOVE", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "-622017164": { + "message": "Finish Transition: %s", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/TransitionController.java" + }, "-618015844": { "message": "performEnableScreen: mDisplayEnabled=%b mForceDisplayEnabled=%b mShowingBootMessages=%b mSystemBooted=%b mOnlyCore=%b. %s", "level": "INFO", @@ -979,12 +1027,24 @@ "group": "WM_SHOW_TRANSACTIONS", "at": "com\/android\/server\/wm\/WindowAnimator.java" }, + "-532081937": { + "message": " Commit activity becoming invisible: %s", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "-519504830": { "message": "applyAnimation: anim=%s nextAppTransition=ANIM_CUSTOM transit=%s isEntrance=%b Callers=%s", "level": "VERBOSE", "group": "WM_DEBUG_APP_TRANSITIONS_ANIM", "at": "com\/android\/server\/wm\/AppTransition.java" }, + "-509601642": { + "message": " checking %s", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "-507657818": { "message": "Window %s is already added", "level": "WARN", @@ -1027,6 +1087,12 @@ "group": "WM_DEBUG_CONFIGURATION", "at": "com\/android\/server\/wm\/ActivityTaskManagerService.java" }, + "-446752714": { + "message": " SKIP: sibling contains top target %s", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "-445944810": { "message": "finish(%b): mCanceled=%b", "level": "DEBUG", @@ -1153,6 +1219,12 @@ "group": "WM_DEBUG_ADD_REMOVE", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, + "-302335479": { + "message": " remove from topTargets %s", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "-272719931": { "message": "startLockTaskModeLocked: %s", "level": "WARN", @@ -1441,12 +1513,24 @@ "group": "WM_DEBUG_WINDOW_ORGANIZER", "at": "com\/android\/server\/wm\/DisplayAreaOrganizerController.java" }, + "182319432": { + "message": " remove from targets %s", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "184362060": { "message": "screenshotTask(%d): mCanceled=%b", "level": "DEBUG", "group": "WM_DEBUG_RECENTS_ANIMATIONS", "at": "com\/android\/server\/wm\/RecentsAnimationController.java" }, + "184610856": { + "message": "Start calculating TransitionInfo based on participants: %s", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "186668272": { "message": "Now changing app %s", "level": "VERBOSE", @@ -1525,6 +1609,12 @@ "group": "WM_DEBUG_APP_TRANSITIONS", "at": "com\/android\/server\/wm\/AppTransitionController.java" }, + "259206414": { + "message": "Creating Transition: %s", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/TransitionController.java" + }, "269576220": { "message": "Resuming rotation after drag", "level": "DEBUG", @@ -1657,6 +1747,12 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "430260320": { + "message": " sibling is a top target with mode %s", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "435494046": { "message": "Attempted to add window to a display for which the application does not have access: %d. Aborting.", "level": "WARN", @@ -1711,6 +1807,12 @@ "group": "WM_DEBUG_APP_TRANSITIONS_ANIM", "at": "com\/android\/server\/wm\/AppTransition.java" }, + "528150092": { + "message": " keep as target %s", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "531242746": { "message": " THUMBNAIL %s: CREATE", "level": "INFO", @@ -1903,6 +2005,12 @@ "group": "WM_DEBUG_CONFIGURATION", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, + "744171317": { + "message": " SKIP: %s", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "745391677": { "message": " CREATE SURFACE %s IN SESSION %s: pid=%d format=%d flags=0x%x \/ %s", "level": "INFO", @@ -1921,6 +2029,12 @@ "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/TaskPositioner.java" }, + "793568608": { + "message": " SKIP: sibling is visible but not part of transition", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "794570322": { "message": "Now closing app %s", "level": "VERBOSE", @@ -1957,6 +2071,12 @@ "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/DisplayContent.java" }, + "849147756": { + "message": "Finish collecting in transition %d", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "853091290": { "message": "Moved stack=%s behind stack=%s", "level": "DEBUG", @@ -2041,6 +2161,12 @@ "group": "WM_DEBUG_REMOTE_ANIMATIONS", "at": "com\/android\/server\/wm\/RemoteAnimationController.java" }, + "996960396": { + "message": "Starting Transition %d", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "1000601037": { "message": "SyncSet{%x:%d} Set ready", "level": "VERBOSE", @@ -2101,6 +2227,12 @@ "group": "WM_DEBUG_ADD_REMOVE", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "1115248873": { + "message": "Calling onTransitionReady: %s", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "1115417974": { "message": "FORCED DISPLAY SIZE: %dx%d", "level": "INFO", @@ -2125,6 +2257,12 @@ "group": "WM_DEBUG_APP_TRANSITIONS", "at": "com\/android\/server\/wm\/DisplayContent.java" }, + "1186730970": { + "message": " no common mode yet, so set it", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "1208313423": { "message": "addWindowToken: Attempted to add token: %s for non-exiting displayId=%d", "level": "WARN", @@ -2293,6 +2431,12 @@ "group": "WM_DEBUG_APP_TRANSITIONS_ANIM", "at": "com\/android\/server\/wm\/AppTransitionController.java" }, + "1469310004": { + "message": " SKIP: common mode mismatch. was %s", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/Transition.java" + }, "1495525537": { "message": "createWallpaperAnimations()", "level": "DEBUG", @@ -2509,6 +2653,12 @@ "group": "WM_DEBUG_ADD_REMOVE", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, + "1794249572": { + "message": "Requesting StartTransition: %s", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS", + "at": "com\/android\/server\/wm\/TransitionController.java" + }, "1822843721": { "message": "Aborted starting %s: startingData=%s", "level": "VERBOSE", @@ -2826,6 +2976,9 @@ "WM_DEBUG_WINDOW_ORGANIZER": { "tag": "WindowManager" }, + "WM_DEBUG_WINDOW_TRANSITIONS": { + "tag": "WindowManager" + }, "WM_ERROR": { "tag": "WindowManager" }, diff --git a/libs/WindowManager/Shell/res/raw/wm_shell_protolog.json b/libs/WindowManager/Shell/res/raw/wm_shell_protolog.json index a13e98c0d1ada..227eec2adc874 100644 --- a/libs/WindowManager/Shell/res/raw/wm_shell_protolog.json +++ b/libs/WindowManager/Shell/res/raw/wm_shell_protolog.json @@ -1,12 +1,24 @@ { "version": "1.0.0", "messages": { + "-1534364071": { + "message": "onTransitionReady %s: %s", + "level": "VERBOSE", + "group": "WM_SHELL_TRANSITIONS", + "at": "com\/android\/wm\/shell\/Transitions.java" + }, "-1501874464": { "message": "Fullscreen Task Appeared: #%d", "level": "VERBOSE", "group": "WM_SHELL_TASK_ORG", "at": "com\/android\/wm\/shell\/FullscreenTaskListener.java" }, + "-1480787369": { + "message": "Transition requested: type=%d %s", + "level": "VERBOSE", + "group": "WM_SHELL_TRANSITIONS", + "at": "com\/android\/wm\/shell\/Transitions.java" + }, "-1340279385": { "message": "Remove listener=%s", "level": "VERBOSE", @@ -31,6 +43,12 @@ "group": "WM_SHELL_TASK_ORG", "at": "com\/android\/wm\/shell\/ShellTaskOrganizer.java" }, + "-191422040": { + "message": "Transition animations finished, notifying core %s", + "level": "VERBOSE", + "group": "WM_SHELL_TRANSITIONS", + "at": "com\/android\/wm\/shell\/Transitions.java" + }, "157713005": { "message": "Task info changed taskId=%d", "level": "VERBOSE", @@ -53,6 +71,9 @@ "groups": { "WM_SHELL_TASK_ORG": { "tag": "WindowManagerShell" + }, + "WM_SHELL_TRANSITIONS": { + "tag": "WindowManagerShell" } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.java index b275331fa9539..9d6271bca426d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/FullscreenTaskListener.java @@ -50,9 +50,13 @@ class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener { // properties in a bad state). t.setPosition(leash, 0, 0); t.setWindowCrop(leash, null); - t.setAlpha(leash, 1f); - t.setMatrix(leash, 1, 0, 0, 1); - t.show(leash); + // TODO(shell-transitions): Eventually set everything in transition so there's no + // SF Transaction here. + if (!Transitions.ENABLE_SHELL_TRANSITIONS) { + t.setAlpha(leash, 1f); + t.setMatrix(leash, 1, 0, 0, 1); + t.show(leash); + } }); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java index d650a958a2036..8f496d01c83b1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -28,7 +28,9 @@ import android.window.TaskOrganizer; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.util.ArrayList; @@ -59,16 +61,25 @@ public class ShellTaskOrganizer extends TaskOrganizer { // require us to report to both old and new listeners) private final SparseArray> mTasks = new SparseArray<>(); - public ShellTaskOrganizer(SyncTransactionQueue syncQueue) { + // TODO(shell-transitions): move to a more "global" Shell location as this isn't only for Tasks + private final Transitions mTransitions; + + public ShellTaskOrganizer(SyncTransactionQueue syncQueue, TransactionPool transactionPool, + ShellExecutor mainExecutor, ShellExecutor animExecutor) { super(); addListener(new FullscreenTaskListener(syncQueue), WINDOWING_MODE_FULLSCREEN); + mTransitions = new Transitions(this, transactionPool, mainExecutor, animExecutor); + if (Transitions.ENABLE_SHELL_TRANSITIONS) registerTransitionPlayer(mTransitions); } @VisibleForTesting ShellTaskOrganizer(ITaskOrganizerController taskOrganizerController, - SyncTransactionQueue syncQueue) { + SyncTransactionQueue syncQueue, TransactionPool transactionPool, + ShellExecutor mainExecutor, ShellExecutor animExecutor) { super(taskOrganizerController); addListener(new FullscreenTaskListener(syncQueue), WINDOWING_MODE_FULLSCREEN); + mTransitions = new Transitions(this, transactionPool, mainExecutor, animExecutor); + if (Transitions.ENABLE_SHELL_TRANSITIONS) registerTransitionPlayer(mTransitions); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/Transitions.java new file mode 100644 index 0000000000000..36e49d9fd770d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/Transitions.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell; + +import static android.window.TransitionInfo.TRANSIT_CLOSE; +import static android.window.TransitionInfo.TRANSIT_HIDE; +import static android.window.TransitionInfo.TRANSIT_OPEN; +import static android.window.TransitionInfo.TRANSIT_SHOW; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.annotation.MainThread; +import android.annotation.NonNull; +import android.os.IBinder; +import android.os.SystemProperties; +import android.util.ArrayMap; +import android.util.Slog; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.ITransitionPlayer; +import android.window.TransitionInfo; +import android.window.WindowOrganizer; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.TransactionPool; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.util.ArrayList; + +/** Plays transition animations */ +public class Transitions extends ITransitionPlayer.Stub { + private static final String TAG = "ShellTransitions"; + + /** Set to {@code true} to enable shell transitions. */ + public static final boolean ENABLE_SHELL_TRANSITIONS = + SystemProperties.getBoolean("persist.debug.shell_transit", false); + + private final WindowOrganizer mOrganizer; + private final TransactionPool mTransactionPool; + private final ShellExecutor mMainExecutor; + private final ShellExecutor mAnimExecutor; + + /** Keeps track of currently tracked transitions and all the animations associated with each */ + private final ArrayMap> mActiveTransitions = new ArrayMap<>(); + + Transitions(@NonNull WindowOrganizer organizer, @NonNull TransactionPool pool, + @NonNull ShellExecutor mainExecutor, @NonNull ShellExecutor animExecutor) { + mOrganizer = organizer; + mTransactionPool = pool; + mMainExecutor = mainExecutor; + mAnimExecutor = animExecutor; + } + + // TODO(shell-transitions): real animations + private void startExampleAnimation(@NonNull IBinder transition, @NonNull SurfaceControl leash, + boolean show) { + final float end = show ? 1.f : 0.f; + final float start = 1.f - end; + final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); + final ValueAnimator va = ValueAnimator.ofFloat(start, end); + va.setDuration(500); + va.addUpdateListener(animation -> { + float fraction = animation.getAnimatedFraction(); + transaction.setAlpha(leash, start * (1.f - fraction) + end * fraction); + transaction.apply(); + }); + final Runnable finisher = () -> { + transaction.setAlpha(leash, end); + transaction.apply(); + mTransactionPool.release(transaction); + mMainExecutor.execute(() -> { + mActiveTransitions.get(transition).remove(va); + onFinish(transition); + }); + }; + va.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { } + + @Override + public void onAnimationEnd(Animator animation) { + finisher.run(); + } + + @Override + public void onAnimationCancel(Animator animation) { + finisher.run(); + } + + @Override + public void onAnimationRepeat(Animator animation) { } + }); + mActiveTransitions.get(transition).add(va); + mAnimExecutor.execute(va::start); + } + + private static boolean isOpeningType(@WindowManager.TransitionType int legacyType) { + // TODO(shell-transitions): consider providing and using z-order vs the global type for + // this determination. + return legacyType == WindowManager.TRANSIT_TASK_OPEN + || legacyType == WindowManager.TRANSIT_TASK_TO_FRONT + || legacyType == WindowManager.TRANSIT_TASK_OPEN_BEHIND + || legacyType == WindowManager.TRANSIT_KEYGUARD_GOING_AWAY; + } + + @Override + public void onTransitionReady(@NonNull IBinder transitionToken, TransitionInfo info, + @NonNull SurfaceControl.Transaction t) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "onTransitionReady %s: %s", + transitionToken, info); + // start task + mMainExecutor.execute(() -> { + if (!mActiveTransitions.containsKey(transitionToken)) { + Slog.e(TAG, "Got transitionReady for non-active transition " + transitionToken + + " expecting one of " + mActiveTransitions.keySet()); + } + if (mActiveTransitions.get(transitionToken) != null) { + throw new IllegalStateException("Got a duplicate onTransitionReady call for " + + transitionToken); + } + mActiveTransitions.put(transitionToken, new ArrayList<>()); + for (int i = 0; i < info.getChanges().size(); ++i) { + final SurfaceControl leash = info.getChanges().get(i).getLeash(); + final int mode = info.getChanges().get(i).getMode(); + if (mode == TRANSIT_OPEN || mode == TRANSIT_SHOW) { + t.show(leash); + t.setMatrix(leash, 1, 0, 0, 1); + if (isOpeningType(info.getType())) { + t.setAlpha(leash, 0.f); + startExampleAnimation(transitionToken, leash, true /* show */); + } else { + t.setAlpha(leash, 1.f); + } + } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_HIDE) { + if (!isOpeningType(info.getType())) { + startExampleAnimation(transitionToken, leash, false /* show */); + } + } + } + t.apply(); + onFinish(transitionToken); + }); + } + + @MainThread + private void onFinish(IBinder transition) { + if (!mActiveTransitions.get(transition).isEmpty()) return; + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "Transition animations finished, notifying core %s", transition); + mActiveTransitions.remove(transition); + mOrganizer.finishTransition(transition, null, null); + } + + @Override + public void requestStartTransition(int type, @NonNull IBinder transitionToken) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition requested: type=%d %s", + type, transitionToken); + mMainExecutor.execute(() -> { + if (mActiveTransitions.containsKey(transitionToken)) { + throw new RuntimeException("Transition already started " + transitionToken); + } + IBinder transition = mOrganizer.startTransition(type, transitionToken, null /* wct */); + mActiveTransitions.put(transition, null); + }); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/AnimationThread.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/AnimationThread.java new file mode 100644 index 0000000000000..96b9f86673fc7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/AnimationThread.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common; + +import static android.os.Process.THREAD_PRIORITY_DISPLAY; + +import android.annotation.NonNull; +import android.os.HandlerThread; +import android.util.Singleton; + +/** + * A singleton thread for Shell to run animations on. + */ +public class AnimationThread extends HandlerThread { + private ShellExecutor mExecutor; + + private AnimationThread() { + super("wmshell.anim", THREAD_PRIORITY_DISPLAY); + } + + /** Get the singleton instance of this thread */ + public static AnimationThread instance() { + return sAnimationThreadSingleton.get(); + } + + /** + * @return a shared {@link ShellExecutor} associated with this thread + * @hide + */ + @NonNull + public ShellExecutor getExecutor() { + if (mExecutor == null) { + mExecutor = new HandlerExecutor(getThreadHandler()); + } + return mExecutor; + } + + private static final Singleton sAnimationThreadSingleton = + new Singleton() { + @Override + protected AnimationThread create() { + final AnimationThread animThread = new AnimationThread(); + animThread.start(); + return animThread; + } + }; +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java new file mode 100644 index 0000000000000..cd75840b8c711 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/HandlerExecutor.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common; + +import android.annotation.NonNull; +import android.os.Handler; + +/** Executor implementation which is backed by a Handler. */ +public class HandlerExecutor implements ShellExecutor { + private final Handler mHandler; + + public HandlerExecutor(@NonNull Handler handler) { + mHandler = handler; + } + + @Override + public void executeDelayed(@NonNull Runnable r, long delayMillis) { + if (!mHandler.postDelayed(r, delayMillis)) { + throw new RuntimeException(mHandler + " is probably exiting"); + } + } + + @Override + public void removeCallbacks(@NonNull Runnable r) { + mHandler.removeCallbacks(r); + } + + @Override + public void execute(@NonNull Runnable command) { + if (!mHandler.post(command)) { + throw new RuntimeException(mHandler + " is probably exiting"); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java new file mode 100644 index 0000000000000..aafe2407a1eac --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ShellExecutor.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common; + +import java.util.concurrent.Executor; + +/** + * Super basic Executor interface that adds support for delayed execution and removing callbacks. + * Intended to wrap Handler while better-supporting testing. + */ +public interface ShellExecutor extends Executor { + /** + * See {@link android.os.Handler#postDelayed(Runnable, long)}. + */ + void executeDelayed(Runnable r, long delayMillis); + + /** + * See {@link android.os.Handler#removeCallbacks}. + */ + void removeCallbacks(Runnable r); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java index e3029e55a2143..a0ce9dabffe6c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java @@ -28,6 +28,8 @@ public enum ShellProtoLogGroup implements IProtoLogGroup { // with those in the framework ProtoLogGroup WM_SHELL_TASK_ORG(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false, Consts.TAG_WM_SHELL), + WM_SHELL_TRANSITIONS(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, true, + Consts.TAG_WM_SHELL), TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest"); private final boolean mEnabled; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java index 823e0b7f03c55..1bc5cea40a8b5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java @@ -33,7 +33,9 @@ import android.window.ITaskOrganizerController; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.common.TransactionPool; import org.junit.Before; import org.junit.Test; @@ -55,6 +57,8 @@ public class ShellTaskOrganizerTests { ShellTaskOrganizer mOrganizer; private final SyncTransactionQueue mSyncTransactionQueue = mock(SyncTransactionQueue.class); + private final TransactionPool mTransactionPool = mock(TransactionPool.class); + private final ShellExecutor mTestExecutor = mock(ShellExecutor.class); private class TrackingTaskListener implements ShellTaskOrganizer.TaskListener { final ArrayList appeared = new ArrayList<>(); @@ -85,7 +89,8 @@ public class ShellTaskOrganizerTests { @Before public void setUp() { MockitoAnnotations.initMocks(this); - mOrganizer = new ShellTaskOrganizer(mTaskOrganizerController, mSyncTransactionQueue); + mOrganizer = new ShellTaskOrganizer(mTaskOrganizerController, mSyncTransactionQueue, + mTransactionPool, mTestExecutor, mTestExecutor); } @Test diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java index bb3b1b42f1201..970d5001172ee 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShellBaseModule.java @@ -32,8 +32,10 @@ import com.android.systemui.util.DeviceConfigProxy; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.animation.FlingAnimationUtils; +import com.android.wm.shell.common.AnimationThread; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.FloatingContentCoordinator; +import com.android.wm.shell.common.HandlerExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.TransactionPool; @@ -132,8 +134,10 @@ public abstract class WMShellBaseModule { @SysUISingleton @Provides - static ShellTaskOrganizer provideShellTaskOrganizer(SyncTransactionQueue syncQueue) { - ShellTaskOrganizer organizer = new ShellTaskOrganizer(syncQueue); + static ShellTaskOrganizer provideShellTaskOrganizer(SyncTransactionQueue syncQueue, + @Main Handler handler, TransactionPool transactionPool) { + ShellTaskOrganizer organizer = new ShellTaskOrganizer(syncQueue, transactionPool, + new HandlerExecutor(handler), AnimationThread.instance().getExecutor()); organizer.registerOrganizer(); return organizer; } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index f29ad2434dc96..2355ed3272eaa 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -1308,6 +1308,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return; } + // TODO(b/169035022): move to a more-appropriate place. + mAtmService.getTransitionController().collect(this); if (prevDc.mOpeningApps.remove(this)) { // Transfer opening transition to new display. mDisplayContent.mOpeningApps.add(this); @@ -3234,6 +3236,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mStackSupervisor.getActivityMetricsLogger().notifyActivityRemoved(this); waitingToShow = false; + // TODO(b/169035022): move to a more-appropriate place. + mAtmService.getTransitionController().collect(this); // Defer removal of this activity when either a child is animating, or app transition is on // going. App transition animation might be applied on the parent stack not on the activity, // but the actual frame buffer is associated with the activity, so we have to keep the @@ -3245,6 +3249,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } else if (getDisplayContent().mAppTransition.isTransitionSet()) { getDisplayContent().mClosingApps.add(this); delayed = true; + } else if (mAtmService.getTransitionController().inTransition()) { + delayed = true; } ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, @@ -4076,6 +4082,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return mVisible; } + @Override + boolean isVisibleRequested() { + return mVisibleRequested; + } + void setVisible(boolean visible) { if (visible != mVisible) { mVisible = visible; @@ -4206,6 +4217,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A transferStartingWindowFromHiddenAboveTokenIfNeeded(); } + // TODO(b/169035022): move to a more-appropriate place. + mAtmService.getTransitionController().collect(this); + if (!visible && mAtmService.getTransitionController().inTransition()) { + return; + } // If we are preparing an app transition, then delay changing // the visibility of this token until we execute that transition. // Note that we ignore display frozen since we want the opening / closing transition type diff --git a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java index 783f8e8642ae6..3ef383b2eed79 100644 --- a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java @@ -1830,7 +1830,8 @@ public class ActivityStackSupervisor implements RecentTasks.Callbacks { for (int i = mStoppingActivities.size() - 1; i >= 0; --i) { final ActivityRecord s = mStoppingActivities.get(i); final boolean animating = s.isAnimating(TRANSITION | PARENTS, - ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_RECENTS); + ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_RECENTS) + || mService.getTransitionController().inTransition(s); if (DEBUG_STATES) Slog.v(TAG, "Stopping " + s + ": nowVisible=" + s.nowVisible + " animating=" + animating + " finishing=" + s.finishing); if (!animating || mService.mShuttingDown) { diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 0402140c6fc1a..c0e31f90b0ae3 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -958,6 +958,10 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { return mLockTaskController; } + TransitionController getTransitionController() { + return mWindowOrganizerController.getTransitionController(); + } + /** * Return the global configuration used by the process corresponding to the input pid. This is * usually the global configuration with some overrides specific to that process. diff --git a/services/core/java/com/android/server/wm/AppTransition.java b/services/core/java/com/android/server/wm/AppTransition.java index f76108f332d19..0e47ea8058f1e 100644 --- a/services/core/java/com/android/server/wm/AppTransition.java +++ b/services/core/java/com/android/server/wm/AppTransition.java @@ -2214,6 +2214,10 @@ public class AppTransition implements Dump { */ boolean prepareAppTransitionLocked(@TransitionType int transit, boolean alwaysKeepCurrent, @TransitionFlags int flags, boolean forceOverride) { + if (mService.mAtmService.getTransitionController().adaptLegacyPrepare( + transit, flags, forceOverride)) { + return false; + } ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "Prepare app transition: transit=%s %s alwaysKeepCurrent=%b displayId=%d " + "Callers=%s", @@ -2255,7 +2259,7 @@ public class AppTransition implements Dump { || transit == TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER; } - private static boolean isKeyguardTransit(int transit) { + static boolean isKeyguardTransit(int transit) { return isKeyguardGoingAwayTransit(transit) || transit == TRANSIT_KEYGUARD_OCCLUDE || transit == TRANSIT_KEYGUARD_UNOCCLUDE; } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 565a64557e4b7..22b446d32ecc6 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -2281,6 +2281,11 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp return true; } + @Override + boolean isVisibleRequested() { + return isVisible(); + } + @Override void onAppTransitionDone() { super.onAppTransitionDone(); @@ -4441,6 +4446,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } void executeAppTransition() { + mAtmService.getTransitionController().setReady(); if (mAppTransition.isTransitionSet()) { ProtoLog.w(WM_DEBUG_APP_TRANSITIONS, "Execute app transition: %s, displayId: %d Callers=%s", diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index a236699209a0b..22776e0d9ee0d 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -2372,6 +2372,7 @@ class Task extends WindowContainer { private void initializeChangeTransition(Rect startBounds) { mDisplayContent.prepareAppTransition(TRANSIT_TASK_CHANGE_WINDOWING_MODE, false /* alwaysKeepCurrent */, 0, false /* forceOverride */); + mAtmService.getTransitionController().collect(this); mDisplayContent.mChangingContainers.add(this); mSurfaceFreezer.freeze(getPendingTransaction(), startBounds); @@ -4761,6 +4762,16 @@ class Task extends WindowContainer { return mTaskOrganizer != null; } + @Override + boolean showSurfaceOnCreation() { + // Organized tasks handle their own surface visibility + final boolean willBeOrganized = + mAtmService.mTaskOrganizerController.isSupportedWindowingMode(getWindowingMode()) + && isRootTask(); + return !mAtmService.getTransitionController().isShellTransitionsEnabled() + || !willBeOrganized; + } + @Override protected void reparentSurfaceControl(SurfaceControl.Transaction t, SurfaceControl newParent) { /** @@ -4781,7 +4792,9 @@ class Task extends WindowContainer { // hide it to allow the task organizer to show it when it is properly reparented. We // skip this for tasks created by the organizer because they can synchronously update // the leash before new children are added to the task. - if (!mCreatedByOrganizer && mTaskOrganizer != null && !prevHasBeenVisible) { + if (!mAtmService.getTransitionController().isShellTransitionsEnabled() + && !mCreatedByOrganizer + && mTaskOrganizer != null && !prevHasBeenVisible) { getSyncTransaction().hide(getSurfaceControl()); commitPendingTransaction(); } @@ -6362,7 +6375,15 @@ class Task extends WindowContainer { transit = TRANSIT_TASK_OPEN; } } - dc.prepareAppTransition(transit, keepCurTransition); + if (mAtmService.getTransitionController().isShellTransitionsEnabled() + // TODO(shell-transitions): eventually all transitions. + && transit == TRANSIT_TASK_OPEN) { + Transition transition = + mAtmService.getTransitionController().requestTransition(transit); + transition.collect(task); + } else { + dc.prepareAppTransition(transit, keepCurTransition); + } mStackSupervisor.mNoAnimActivities.remove(r); } boolean doShow = true; diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java index e07c5677214bb..8201d108c883c 100644 --- a/services/core/java/com/android/server/wm/TaskOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java @@ -354,7 +354,7 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { : null; } - private boolean isSupportedWindowingMode(int winMode) { + boolean isSupportedWindowingMode(int winMode) { return !ArrayUtils.contains(UNSUPPORTED_WINDOWING_MODES, winMode); } diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java new file mode 100644 index 0000000000000..fc67cd22ee69a --- /dev/null +++ b/services/core/java/com/android/server/wm/Transition.java @@ -0,0 +1,487 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + + +import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION; +import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_SUBTLE_ANIMATION; +import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE; +import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER; +import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY; +import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER; + +import android.annotation.NonNull; +import android.os.Binder; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Slog; +import android.view.Display; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.view.animation.Animation; +import android.window.TransitionInfo; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.ProtoLogGroup; +import com.android.internal.protolog.common.ProtoLog; + +import java.util.ArrayList; +import java.util.Map; +import java.util.Set; + +/** + * Represents a logical transition. + * @see TransitionController + */ +class Transition extends Binder implements BLASTSyncEngine.TransactionReadyListener { + private static final String TAG = "Transition"; + + /** The transition has been created and is collecting, but hasn't formally started. */ + private static final int STATE_COLLECTING = 0; + + /** + * The transition has formally started. It is still collecting but will stop once all + * participants are ready to animate (finished drawing). + */ + private static final int STATE_STARTED = 1; + + /** + * This transition is currently playing its animation and can no longer collect or be changed. + */ + private static final int STATE_PLAYING = 2; + + final @WindowManager.TransitionType int mType; + private int mSyncId; + private @WindowManager.TransitionFlags int mFlags; + private final TransitionController mController; + final ArrayMap mParticipants = new ArrayMap<>(); + private int mState = STATE_COLLECTING; + private boolean mReadyCalled = false; + + Transition(@WindowManager.TransitionType int type, + @WindowManager.TransitionFlags int flags, TransitionController controller) { + mType = type; + mFlags = flags; + mController = controller; + mSyncId = mController.mSyncEngine.startSyncSet(this); + } + + /** + * Formally starts the transition. Participants can be collected before this is started, + * but this won't consider itself ready until started -- even if all the participants have + * drawn. + */ + void start() { + if (mState >= STATE_STARTED) { + Slog.w(TAG, "Transition already started: " + mSyncId); + } + mState = STATE_STARTED; + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Starting Transition %d", + mSyncId); + if (mReadyCalled) { + setReady(); + } + } + + /** Adds wc to set of WindowContainers participating in this transition. */ + void collect(@NonNull WindowContainer wc) { + if (mSyncId < 0) return; + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Collecting in transition %d: %s", + mSyncId, wc); + // Add to sync set before checking contains because it may not have added it at other + // times (eg. if wc was previously invisible). + mController.mSyncEngine.addToSyncSet(mSyncId, wc); + if (mParticipants.containsKey(wc)) return; + mParticipants.put(wc, new ChangeInfo()); + } + + /** + * Call this when all known changes related to this transition have been applied. Until + * all participants have finished drawing, the transition can still collect participants. + * + * If this is called before the transition is started, it will be deferred until start. + */ + void setReady() { + if (mSyncId < 0) return; + if (mState < STATE_STARTED) { + mReadyCalled = true; + return; + } + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, + "Finish collecting in transition %d", mSyncId); + mController.mSyncEngine.setReady(mSyncId); + mController.mAtm.mWindowManager.mWindowPlacerLocked.requestTraversal(); + } + + /** The transition has finished animating and is ready to finalize WM state */ + void finishTransition() { + if (mState < STATE_PLAYING) { + throw new IllegalStateException("Can't finish a non-playing transition " + mSyncId); + } + for (int i = 0; i < mParticipants.size(); ++i) { + final ActivityRecord ar = mParticipants.keyAt(i).asActivityRecord(); + if (ar == null || ar.mVisibleRequested) { + continue; + } + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, + " Commit activity becoming invisible: %s", ar); + ar.commitVisibility(false /* visible */, false /* performLayout */); + } + } + + @Override + public void onTransactionReady(int syncId, Set windowContainersReady) { + if (syncId != mSyncId) { + Slog.e(TAG, "Unexpected Sync ID " + syncId + ". Expected " + mSyncId); + return; + } + mState = STATE_PLAYING; + mController.moveToPlaying(this); + final TransitionInfo info = calculateTransitionInfo(mType, mParticipants); + + SurfaceControl.Transaction mergedTransaction = new SurfaceControl.Transaction(); + int displayId = Display.DEFAULT_DISPLAY; + for (WindowContainer container : windowContainersReady) { + container.mergeBlastSyncTransaction(mergedTransaction); + displayId = container.mDisplayContent.getDisplayId(); + } + + handleNonAppWindowsInTransition(displayId, mType, mFlags); + + if (mController.getTransitionPlayer() != null) { + try { + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, + "Calling onTransitionReady: %s", info); + mController.getTransitionPlayer().onTransitionReady(this, info, mergedTransaction); + } catch (RemoteException e) { + // If there's an exception when trying to send the mergedTransaction to the + // client, we should immediately apply it here so the transactions aren't lost. + mergedTransaction.apply(); + } + } else { + mergedTransaction.apply(); + } + mSyncId = -1; + } + + private void handleNonAppWindowsInTransition(int displayId, int transit, int flags) { + final DisplayContent dc = + mController.mAtm.mRootWindowContainer.getDisplayContent(displayId); + if (dc == null) { + return; + } + if (transit == TRANSIT_KEYGUARD_GOING_AWAY) { + if ((flags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER) != 0 + && (flags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION) == 0 + && (flags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY_SUBTLE_ANIMATION) == 0) { + Animation anim = mController.mAtm.mWindowManager.mPolicy + .createKeyguardWallpaperExit( + (flags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE) != 0); + if (anim != null) { + anim.scaleCurrentDuration( + mController.mAtm.mWindowManager.getTransitionAnimationScaleLocked()); + dc.mWallpaperController.startWallpaperAnimation(anim); + } + } + } + if (transit == TRANSIT_KEYGUARD_GOING_AWAY + || transit == TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER) { + dc.startKeyguardExitOnNonAppWindows( + transit == TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER, + (flags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE) != 0, + (flags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY_SUBTLE_ANIMATION) != 0); + mController.mAtm.mWindowManager.mPolicy.startKeyguardExitAnimation(transit, 0); + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(64); + sb.append("TransitionRecord{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(" id=" + mSyncId); + sb.append(" type=" + mType); + sb.append(" flags=" + mFlags); + sb.append('}'); + return sb.toString(); + } + + private static boolean reportIfNotTop(WindowContainer wc) { + // Organized tasks need to be reported anyways because Core won't show() their surfaces + // and we can't rely on onTaskAppeared because it isn't in sync. + // TODO(shell-transitions): switch onTaskAppeared usage over to transitions OPEN. + return wc.isOrganized(); + } + + /** @return the depth of child within ancestor, 0 if child == ancestor, or -1 if not a child. */ + private static int getChildDepth(WindowContainer child, WindowContainer ancestor) { + WindowContainer parent = child; + int depth = 0; + while (parent != null) { + if (parent == ancestor) { + return depth; + } + parent = parent.getParent(); + ++depth; + } + return -1; + } + + private static @TransitionInfo.TransitionMode int getModeFor(WindowContainer wc) { + if (wc.isVisibleRequested()) { + final Task t = wc.asTask(); + if (t != null && t.getHasBeenVisible()) { + return TransitionInfo.TRANSIT_SHOW; + } + return TransitionInfo.TRANSIT_OPEN; + } + return TransitionInfo.TRANSIT_CLOSE; + } + + /** + * Under some conditions (eg. all visible targets within a parent container are transitioning + * the same way) the transition can be "promoted" to the parent container. This means an + * animation can play just on the parent rather than all the individual children. + * + * @return {@code true} if transition in target can be promoted to its parent. + */ + private static boolean canPromote( + WindowContainer target, ArraySet topTargets) { + final WindowContainer parent = target.getParent(); + if (parent == null || !parent.canCreateRemoteAnimationTarget()) { + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " SKIP: %s", + parent == null ? "no parent" : ("parent can't be target " + parent)); + return false; + } + @TransitionInfo.TransitionMode int mode = TransitionInfo.TRANSIT_NONE; + // Go through all siblings of this target to see if any of them would prevent + // the target from promoting. + siblingLoop: + for (int i = parent.getChildCount() - 1; i >= 0; --i) { + final WindowContainer sibling = parent.getChildAt(i); + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " check sibling %s", + sibling); + // Check if any topTargets are the sibling or within it + for (int j = topTargets.size() - 1; j >= 0; --j) { + final int depth = getChildDepth(topTargets.valueAt(j), sibling); + if (depth < 0) continue; + if (depth == 0) { + final int siblingMode = sibling.isVisibleRequested() + ? TransitionInfo.TRANSIT_OPEN : TransitionInfo.TRANSIT_CLOSE; + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, + " sibling is a top target with mode %s", + TransitionInfo.modeToString(siblingMode)); + if (mode == TransitionInfo.TRANSIT_NONE) { + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, + " no common mode yet, so set it"); + mode = siblingMode; + } else if (mode != siblingMode) { + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, + " SKIP: common mode mismatch. was %s", + TransitionInfo.modeToString(mode)); + return false; + } + continue siblingLoop; + } else { + // Sibling subtree may not be promotable. + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, + " SKIP: sibling contains top target %s", + topTargets.valueAt(j)); + return false; + } + } + // No other animations are playing in this sibling + if (sibling.isVisibleRequested()) { + // Sibling is visible but not animating, so no promote. + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, + " SKIP: sibling is visible but not part of transition"); + return false; + } + } + return true; + } + + /** + * Go through topTargets and try to promote (see {@link #canPromote}) one of them. + * + * @param topTargets set of just the top-most targets in the hierarchy of participants. + * @param targets all targets that will be sent to the player. + * @return {@code true} if something was promoted. + */ + private static boolean tryPromote(ArraySet topTargets, + ArrayMap targets) { + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " --- Start combine pass ---"); + // Go through each target until we find one that can be promoted. + targetLoop: + for (WindowContainer targ : topTargets) { + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " checking %s", targ); + if (!canPromote(targ, topTargets)) { + continue; + } + final WindowContainer parent = targ.getParent(); + // No obstructions found to promotion, so promote + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, + " CAN PROMOTE: promoting to parent %s", parent); + final ChangeInfo parentInfo = new ChangeInfo(); + targets.put(parent, parentInfo); + // Go through all children of newly-promoted container and remove them from + // the top-targets. + for (int i = parent.getChildCount() - 1; i >= 0; --i) { + final WindowContainer child = parent.getChildAt(i); + int idx = targets.indexOfKey(child); + if (idx >= 0) { + if (reportIfNotTop(child)) { + targets.valueAt(idx).mParent = parent; + parentInfo.addChild(child); + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, + " keep as target %s", child); + } else { + if (targets.valueAt(idx).mChildren != null) { + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, + " merging children in from %s: %s", child, + targets.valueAt(idx).mChildren); + parentInfo.addChildren(targets.valueAt(idx).mChildren); + } + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, + " remove from targets %s", child); + targets.removeAt(idx); + } + } + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, + " remove from topTargets %s", child); + topTargets.remove(child); + } + topTargets.add(parent); + return true; + } + return false; + } + + /** + * Find WindowContainers to be animated from a set of opening and closing apps. We will promote + * animation targets to higher level in the window hierarchy if possible. + */ + @VisibleForTesting + static TransitionInfo calculateTransitionInfo( + int type, Map participants) { + final TransitionInfo out = new TransitionInfo(type); + + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, + "Start calculating TransitionInfo based on participants: %s", + new ArraySet<>(participants.keySet())); + + final ArraySet topTargets = new ArraySet<>(); + // The final animation targets which cannot promote to higher level anymore. + final ArrayMap targets = new ArrayMap<>(); + + final ArrayList tmpList = new ArrayList<>(); + + // Build initial set of top-level participants by removing any participants that are + // children of other participants or are otherwise invalid. + for (Map.Entry entry : participants.entrySet()) { + final WindowContainer wc = entry.getKey(); + // Don't include detached windows. + if (!wc.isAttached()) continue; + + final ChangeInfo changeInfo = entry.getValue(); + WindowContainer parent = wc.getParent(); + WindowContainer topParent = null; + // Keep track of always-report parents in bottom-to-top order + tmpList.clear(); + while (parent != null) { + if (participants.containsKey(parent)) { + topParent = parent; + } else if (reportIfNotTop(parent)) { + tmpList.add(parent); + } + parent = parent.getParent(); + } + if (topParent != null) { + // Add always-report parents along the way + parent = topParent; + for (int i = tmpList.size() - 1; i >= 0; --i) { + if (!participants.containsKey(tmpList.get(i))) { + final ChangeInfo info = new ChangeInfo(); + info.mParent = parent; + targets.put(tmpList.get(i), info); + } + parent = tmpList.get(i); + } + continue; + } + targets.put(wc, changeInfo); + topTargets.add(wc); + } + + // Populate children lists + for (int i = targets.size() - 1; i >= 0; --i) { + if (targets.valueAt(i).mParent != null) { + targets.get(targets.valueAt(i).mParent).addChild(targets.keyAt(i)); + } + } + + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, + " Initial targets: %s", new ArraySet<>(targets.keySet())); + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " Top targets: %s", topTargets); + + // Combine targets by repeatedly going through the topTargets to see if they can be + // promoted until there aren't any promotions possible. + while (tryPromote(topTargets, targets)) { + // Empty on purpose + } + + // Convert all the resolved ChangeInfos into a TransactionInfo object. + for (int i = targets.size() - 1; i >= 0; --i) { + final WindowContainer target = targets.keyAt(i); + final ChangeInfo info = targets.valueAt(i); + final TransitionInfo.Change change = new TransitionInfo.Change( + target.mRemoteToken.toWindowContainerToken(), target.getSurfaceControl()); + if (info.mParent != null) { + change.setParent(info.mParent.mRemoteToken.toWindowContainerToken()); + } + change.setMode(getModeFor(target)); + out.addChange(change); + } + + return out; + } + + static Transition fromBinder(IBinder binder) { + return (Transition) binder; + } + + @VisibleForTesting + static class ChangeInfo { + WindowContainer mParent; + ArraySet mChildren; + // TODO(shell-transitions): other tracking like before state and bounds + void addChild(@NonNull WindowContainer wc) { + if (mChildren == null) { + mChildren = new ArraySet<>(); + } + mChildren.add(wc); + } + void addChildren(@NonNull ArraySet wcs) { + if (mChildren == null) { + mChildren = new ArraySet<>(); + } + mChildren.addAll(wcs); + } + } +} diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java new file mode 100644 index 0000000000000..d102c19bfff92 --- /dev/null +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static android.view.WindowManager.TRANSIT_CRASHING_ACTIVITY_CLOSE; +import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY; +import static android.view.WindowManager.TRANSIT_TASK_CLOSE; +import static android.view.WindowManager.TRANSIT_TASK_OPEN; +import static android.view.WindowManager.TRANSIT_TASK_OPEN_BEHIND; +import static android.view.WindowManager.TRANSIT_TASK_TO_BACK; +import static android.view.WindowManager.TRANSIT_TASK_TO_FRONT; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Slog; +import android.view.WindowManager; +import android.window.ITransitionPlayer; + +import com.android.internal.protolog.ProtoLogGroup; +import com.android.internal.protolog.common.ProtoLog; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * Handles all the aspects of recording and synchronizing transitions. + */ +class TransitionController { + private static final String TAG = "TransitionController"; + + private static final int[] SUPPORTED_LEGACY_TRANSIT_TYPES = {TRANSIT_TASK_OPEN, + TRANSIT_TASK_CLOSE, TRANSIT_TASK_TO_FRONT, TRANSIT_TASK_TO_BACK, + TRANSIT_TASK_OPEN_BEHIND, TRANSIT_KEYGUARD_GOING_AWAY}; + static { + Arrays.sort(SUPPORTED_LEGACY_TRANSIT_TYPES); + } + + final BLASTSyncEngine mSyncEngine = new BLASTSyncEngine(); + private ITransitionPlayer mTransitionPlayer; + private final IBinder.DeathRecipient mTransitionPlayerDeath = () -> mTransitionPlayer = null; + final ActivityTaskManagerService mAtm; + + /** Currently playing transitions. When finished, records are removed from this list. */ + private final ArrayList mPlayingTransitions = new ArrayList<>(); + + /** + * The transition currently being constructed (collecting participants). + * TODO(shell-transitions): When simultaneous transitions are supported, merge this with + * mPlayingTransitions. + */ + private Transition mCollectingTransition = null; + + TransitionController(ActivityTaskManagerService atm) { + mAtm = atm; + } + + /** @see #createTransition(int, int) */ + @NonNull + Transition createTransition(int type) { + return createTransition(type, 0 /* flags */); + } + + /** + * Creates a transition. It can immediately collect participants. + */ + @NonNull + Transition createTransition(@WindowManager.TransitionType int type, + @WindowManager.TransitionFlags int flags) { + if (mCollectingTransition != null) { + throw new IllegalStateException("Simultaneous transitions not supported yet."); + } + mCollectingTransition = new Transition(type, flags, this); + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Creating Transition: %s", + mCollectingTransition); + return mCollectingTransition; + } + + void registerTransitionPlayer(@Nullable ITransitionPlayer player) { + try { + if (mTransitionPlayer != null) { + mTransitionPlayer.asBinder().unlinkToDeath(mTransitionPlayerDeath, 0); + mTransitionPlayer = null; + } + player.asBinder().linkToDeath(mTransitionPlayerDeath, 0); + mTransitionPlayer = player; + } catch (RemoteException e) { + throw new RuntimeException("Unable to set transition player"); + } + } + + @Nullable ITransitionPlayer getTransitionPlayer() { + return mTransitionPlayer; + } + + boolean isShellTransitionsEnabled() { + return mTransitionPlayer != null; + } + + /** @return {@code true} if a transition is running */ + boolean inTransition() { + // TODO(shell-transitions): eventually properly support multiple + return mCollectingTransition != null || !mPlayingTransitions.isEmpty(); + } + + /** @return {@code true} if wc is in a participant subtree */ + boolean inTransition(@NonNull WindowContainer wc) { + if (mCollectingTransition != null && mCollectingTransition.mParticipants.containsKey(wc)) { + return true; + } + for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) { + for (WindowContainer p = wc; p != null; p = p.getParent()) { + if (mPlayingTransitions.get(i).mParticipants.containsKey(p)) { + return true; + } + } + } + return false; + } + + /** + * Creates a transition and asks the TransitionPlayer (Shell) to start it. + * @return the created transition. Collection can start immediately. + */ + @NonNull + Transition requestTransition(@WindowManager.TransitionType int type) { + return requestTransition(type, 0 /* flags */); + } + + /** @see #requestTransition */ + @NonNull + Transition requestTransition(@WindowManager.TransitionType int type, + @WindowManager.TransitionFlags int flags) { + if (mTransitionPlayer == null) { + throw new IllegalStateException("Shell Transitions not enabled"); + } + final Transition transition = createTransition(type, flags); + try { + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, + "Requesting StartTransition: %s", transition); + mTransitionPlayer.requestStartTransition(type, transition); + } catch (RemoteException e) { + Slog.e(TAG, "Error requesting transition", e); + transition.start(); + } + return transition; + } + + /** + * Temporary adapter that converts the legacy AppTransition's prepareAppTransition call into + * a Shell transition request. If shell transitions are enabled, this will take priority in + * handling transition types that it supports. All other transitions will be ignored and thus + * be handled by the legacy apptransition system. This allows both worlds to live in tandem + * during migration. + * + * @return {@code true} if the transition is handled. + */ + boolean adaptLegacyPrepare(@WindowManager.TransitionType int transit, + @WindowManager.TransitionFlags int flags, boolean forceOverride) { + if (!isShellTransitionsEnabled() + || Arrays.binarySearch(SUPPORTED_LEGACY_TRANSIT_TYPES, transit) < 0) { + return false; + } + if (inTransition()) { + if (AppTransition.isKeyguardTransit(transit)) { + // TODO(shell-transitions): add to flags + } else if (forceOverride) { + // TODO(shell-transitions): sort out these flags + } else if (transit == TRANSIT_CRASHING_ACTIVITY_CLOSE) { + // TODO(shell-transitions): record crashing + } + } else { + requestTransition(transit, flags); + } + return true; + } + + /** @see Transition#collect */ + void collect(@NonNull WindowContainer wc) { + if (mCollectingTransition == null) return; + mCollectingTransition.collect(wc); + } + + /** @see Transition#setReady */ + void setReady() { + if (mCollectingTransition == null) return; + mCollectingTransition.setReady(); + } + + /** @see Transition#finishTransition */ + void finishTransition(@NonNull IBinder token) { + final Transition record = Transition.fromBinder(token); + if (record == null || !mPlayingTransitions.contains(record)) { + Slog.e(TAG, "Trying to finish a non-playing transition " + token); + return; + } + ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Finish Transition: %s", record); + mPlayingTransitions.remove(record); + record.finishTransition(); + } + + void moveToPlaying(Transition transition) { + if (transition != mCollectingTransition) { + throw new IllegalStateException("Trying to move non-collecting transition to playing"); + } + mCollectingTransition = null; + mPlayingTransitions.add(transition); + } + +} diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 2b93080a8dad8..2ece30d24b3a3 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -417,7 +417,9 @@ class WindowContainer extends ConfigurationContainer< void setInitialSurfaceControlProperties(SurfaceControl.Builder b) { setSurfaceControl(b.setCallsite("WindowContainer.setInitialSurfaceControlProperties").build()); - getSyncTransaction().show(mSurfaceControl); + if (showSurfaceOnCreation()) { + getSyncTransaction().show(mSurfaceControl); + } onSurfaceShown(getSyncTransaction()); updateSurfacePositionNonOrganized(); } @@ -998,6 +1000,21 @@ class WindowContainer extends ConfigurationContainer< return false; } + /** + * Is this window's surface needed? This is almost like isVisible, except when participating + * in a transition, this will reflect the final visibility while isVisible won't change until + * the transition is finished. + */ + boolean isVisibleRequested() { + for (int i = mChildren.size() - 1; i >= 0; --i) { + final WindowContainer child = mChildren.get(i); + if (child.isVisibleRequested()) { + return true; + } + } + return false; + } + /** * Called when the visibility of a child is asked to change. This is before visibility actually * changes (eg. a transition animation might play out first). @@ -2816,6 +2833,13 @@ class WindowContainer extends ConfigurationContainer< return false; } + /** + * @return {@code true} if this container's surface should be shown when it is created. + */ + boolean showSurfaceOnCreation() { + return true; + } + static WindowContainer fromBinder(IBinder binder) { return RemoteToken.fromBinder(binder).getContainer(); } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index f237598cb5972..19cfcb21c8ac0 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -2462,7 +2462,10 @@ public class WindowManagerService extends IWindowManager.Stub if (win.mAttrs.type == TYPE_APPLICATION_STARTING) { transit = WindowManagerPolicy.TRANSIT_PREVIEW_DONE; } - if (win.isWinVisibleLw() && winAnimator.applyAnimationLocked(transit, false)) { + if (mAtmService.getTransitionController().inTransition(win)) { + focusMayChange = true; + win.mAnimatingExit = true; + } else if (win.isWinVisibleLw() && winAnimator.applyAnimationLocked(transit, false)) { focusMayChange = true; win.mAnimatingExit = true; } else if (win.isAnimating(TRANSITION | PARENTS)) { diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 999181dc486cd..f1641cdfcf67e 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -26,6 +26,8 @@ import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_TASK_ORG; import static com.android.server.wm.WindowContainer.POSITION_BOTTOM; import static com.android.server.wm.WindowContainer.POSITION_TOP; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.WindowConfiguration; import android.content.pm.ActivityInfo; import android.content.res.Configuration; @@ -40,6 +42,7 @@ import android.view.Surface; import android.view.SurfaceControl; import android.window.IDisplayAreaOrganizerController; import android.window.ITaskOrganizerController; +import android.window.ITransitionPlayer; import android.window.IWindowContainerTransactionCallback; import android.window.IWindowOrganizerController; import android.window.WindowContainerToken; @@ -88,21 +91,85 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub final TaskOrganizerController mTaskOrganizerController; final DisplayAreaOrganizerController mDisplayAreaOrganizerController; + final TransitionController mTransitionController; + WindowOrganizerController(ActivityTaskManagerService atm) { mService = atm; mGlobalLock = atm.mGlobalLock; mTaskOrganizerController = new TaskOrganizerController(mService); mDisplayAreaOrganizerController = new DisplayAreaOrganizerController(mService); + mTransitionController = new TransitionController(atm); + } + + TransitionController getTransitionController() { + return mTransitionController; } @Override public void applyTransaction(WindowContainerTransaction t) { - applySyncTransaction(t, null /*callback*/); + applyTransaction(t, null /*callback*/, null /*transition*/); } @Override public int applySyncTransaction(WindowContainerTransaction t, IWindowContainerTransactionCallback callback) { + return applyTransaction(t, callback, null /*transition*/); + } + + @Override + public IBinder startTransition(int type, @Nullable IBinder transitionToken, + @Nullable WindowContainerTransaction t) { + enforceStackPermission("startTransition()"); + long ident = Binder.clearCallingIdentity(); + try { + synchronized (mGlobalLock) { + Transition transition = Transition.fromBinder(transitionToken); + if (transition == null) { + if (type < 0) { + throw new IllegalArgumentException("Can't create transition with no type"); + } + transition = mTransitionController.createTransition(type); + } + transition.start(); + if (t == null) { + t = new WindowContainerTransaction(); + } + applyTransaction(t, null /*callback*/, transition); + return transition; + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override + public int finishTransition(@NonNull IBinder transitionToken, + @Nullable WindowContainerTransaction t, + @Nullable IWindowContainerTransactionCallback callback) { + enforceStackPermission("finishTransition()"); + long ident = Binder.clearCallingIdentity(); + try { + synchronized (mGlobalLock) { + int syncId = -1; + if (t != null) { + syncId = applyTransaction(t, callback, null /*transition*/); + } + getTransitionController().finishTransition(transitionToken); + return syncId; + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + /** + * @param callback If non-null, this will be a sync-transaction. + * @param transition A transition to collect changes into. + * @return a BLAST sync-id if this is a non-transition, sync transaction. + */ + private int applyTransaction(@NonNull WindowContainerTransaction t, + @Nullable IWindowContainerTransactionCallback callback, + @Nullable Transition transition) { enforceStackPermission("applySyncTransaction()"); int syncId = -1; if (t == null) { @@ -152,6 +219,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } int containerEffect = applyWindowContainerChange(wc, entry.getValue()); + if (transition != null) transition.collect(wc); effects |= containerEffect; // Lifecycle changes will trigger ensureConfig for everything. @@ -173,6 +241,12 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub addToSyncSet(syncId, wc); } effects |= sanitizeAndApplyHierarchyOp(wc, hop); + if (transition != null) { + transition.collect(wc); + if (hop.isReparent() && hop.getNewParent() != null) { + transition.collect(WindowContainer.fromBinder(hop.getNewParent())); + } + } } // Queue-up bounds-change transactions for tasks which are now organized. Do // this after hierarchy ops so we have the final organized state. @@ -512,6 +586,19 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub return true; } + @Override + public void registerTransitionPlayer(ITransitionPlayer player) { + enforceStackPermission("registerTransitionPlayer()"); + long ident = Binder.clearCallingIdentity(); + try { + synchronized (mGlobalLock) { + mTransitionController.registerTransitionPlayer(player); + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + private void enforceStackPermission(String func) { mService.mAmInternal.enforceCallingPermission(MANAGE_ACTIVITY_STACKS, func); } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 89a8cb712e984..8f42b3f154f71 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -1683,6 +1683,11 @@ class WindowState extends WindowContainer implements WindowManagerP || mControllableInsetProvider.isClientVisible()); } + @Override + boolean isVisibleRequested() { + return isVisible(); + } + /** * Ensures that all the policy visibility bits are set. * @return {@code true} if all flags about visiblity are set @@ -1771,7 +1776,7 @@ class WindowState extends WindowContainer implements WindowManagerP } final ActivityRecord atoken = mActivityRecord; if (atoken != null) { - return ((!isParentWindowHidden() && atoken.mVisibleRequested) + return ((!isParentWindowHidden() && atoken.isVisible()) || isAnimating(TRANSITION | PARENTS)); } return !isParentWindowHidden() || isAnimating(TRANSITION | PARENTS); diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java new file mode 100644 index 0000000000000..ce22205c75f0e --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.view.WindowManager.TRANSIT_TASK_OPEN; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; + +import android.platform.test.annotations.Presubmit; +import android.util.ArrayMap; +import android.window.ITaskOrganizer; +import android.window.TransitionInfo; + +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Build/Install/Run: + * atest WmTests:TransitionRecordTests + */ +@SmallTest +@Presubmit +@RunWith(WindowTestRunner.class) +public class TransitionTests extends WindowTestsBase { + + @Test + public void testCreateInfo_NewTask() { + final Task newTask = createTaskStackOnDisplay(WINDOWING_MODE_FULLSCREEN, + ACTIVITY_TYPE_STANDARD, mDisplayContent); + final Task oldTask = createTaskStackOnDisplay(WINDOWING_MODE_FULLSCREEN, + ACTIVITY_TYPE_STANDARD, mDisplayContent); + newTask.setHasBeenVisible(true); + oldTask.setHasBeenVisible(false); + final ActivityRecord closing = createActivityRecordInTask(oldTask); + final ActivityRecord opening = createActivityRecordInTask(newTask); + closing.setVisible(true); + closing.mVisibleRequested = false; + opening.setVisible(false); + opening.mVisibleRequested = true; + ArrayMap participants = new ArrayMap<>(); + + int transitType = TRANSIT_TASK_OPEN; + + // Check basic both tasks participating + participants.put(oldTask, new Transition.ChangeInfo()); + participants.put(newTask, new Transition.ChangeInfo()); + TransitionInfo info = + Transition.calculateTransitionInfo(transitType, participants); + assertEquals(2, info.getChanges().size()); + assertEquals(transitType, info.getType()); + + // Check that children are pruned + participants.put(opening, new Transition.ChangeInfo()); + participants.put(closing, new Transition.ChangeInfo()); + info = Transition.calculateTransitionInfo(transitType, participants); + assertEquals(2, info.getChanges().size()); + assertNotNull(info.getChange(newTask.mRemoteToken.toWindowContainerToken())); + assertNotNull(info.getChange(oldTask.mRemoteToken.toWindowContainerToken())); + + // Check combined prune and promote + participants.remove(newTask); + info = Transition.calculateTransitionInfo(transitType, participants); + assertEquals(2, info.getChanges().size()); + assertNotNull(info.getChange(newTask.mRemoteToken.toWindowContainerToken())); + assertNotNull(info.getChange(oldTask.mRemoteToken.toWindowContainerToken())); + + // Check multi promote + participants.remove(oldTask); + info = Transition.calculateTransitionInfo(transitType, participants); + assertEquals(2, info.getChanges().size()); + assertNotNull(info.getChange(newTask.mRemoteToken.toWindowContainerToken())); + assertNotNull(info.getChange(oldTask.mRemoteToken.toWindowContainerToken())); + } + + @Test + public void testCreateInfo_NestedTasks() { + final Task newTask = createTaskStackOnDisplay(WINDOWING_MODE_FULLSCREEN, + ACTIVITY_TYPE_STANDARD, mDisplayContent); + final Task newNestedTask = createTaskInStack(newTask, 0); + final Task newNestedTask2 = createTaskInStack(newTask, 0); + final Task oldTask = createTaskStackOnDisplay(WINDOWING_MODE_FULLSCREEN, + ACTIVITY_TYPE_STANDARD, mDisplayContent); + newTask.setHasBeenVisible(true); + oldTask.setHasBeenVisible(false); + final ActivityRecord closing = createActivityRecordInTask(oldTask); + final ActivityRecord opening = createActivityRecordInTask(newNestedTask); + final ActivityRecord opening2 = createActivityRecordInTask(newNestedTask2); + closing.setVisible(true); + closing.mVisibleRequested = false; + opening.setVisible(false); + opening.mVisibleRequested = true; + opening2.setVisible(false); + opening2.mVisibleRequested = true; + ArrayMap participants = new ArrayMap<>(); + + int transitType = TRANSIT_TASK_OPEN; + + // Check full promotion from leaf + participants.put(oldTask, new Transition.ChangeInfo()); + participants.put(opening, new Transition.ChangeInfo()); + participants.put(opening2, new Transition.ChangeInfo()); + TransitionInfo info = + Transition.calculateTransitionInfo(transitType, participants); + assertEquals(2, info.getChanges().size()); + assertEquals(transitType, info.getType()); + assertNotNull(info.getChange(newTask.mRemoteToken.toWindowContainerToken())); + assertNotNull(info.getChange(oldTask.mRemoteToken.toWindowContainerToken())); + + // Check that unchanging but visible descendant of sibling prevents promotion + participants.remove(opening2); + info = Transition.calculateTransitionInfo(transitType, participants); + assertEquals(2, info.getChanges().size()); + assertNotNull(info.getChange(newNestedTask.mRemoteToken.toWindowContainerToken())); + assertNotNull(info.getChange(oldTask.mRemoteToken.toWindowContainerToken())); + } + + @Test + public void testCreateInfo_DisplayArea() { + final Task showTask = createTaskStackOnDisplay(WINDOWING_MODE_FULLSCREEN, + ACTIVITY_TYPE_STANDARD, mDisplayContent); + final Task showNestedTask = createTaskInStack(showTask, 0); + final Task showTask2 = createTaskStackOnDisplay(WINDOWING_MODE_FULLSCREEN, + ACTIVITY_TYPE_STANDARD, mDisplayContent); + final DisplayArea tda = showTask.getDisplayArea(); + showTask.setHasBeenVisible(true); + showTask2.setHasBeenVisible(true); + final ActivityRecord showing = createActivityRecordInTask(showNestedTask); + final ActivityRecord showing2 = createActivityRecordInTask(showTask2); + showing.setVisible(false); + showing.mVisibleRequested = true; + showing2.setVisible(false); + showing2.mVisibleRequested = true; + ArrayMap participants = new ArrayMap<>(); + + int transitType = TRANSIT_TASK_OPEN; + + // Check promotion to DisplayArea + participants.put(showing, new Transition.ChangeInfo()); + participants.put(showing2, new Transition.ChangeInfo()); + TransitionInfo info = + Transition.calculateTransitionInfo(transitType, participants); + assertEquals(1, info.getChanges().size()); + assertEquals(transitType, info.getType()); + assertNotNull(info.getChange(tda.mRemoteToken.toWindowContainerToken())); + + ITaskOrganizer mockOrg = mock(ITaskOrganizer.class); + // Check that organized tasks get reported even if not top + showTask.mTaskOrganizer = mockOrg; + info = Transition.calculateTransitionInfo(transitType, participants); + assertEquals(2, info.getChanges().size()); + assertNotNull(info.getChange(tda.mRemoteToken.toWindowContainerToken())); + assertNotNull(info.getChange(showTask.mRemoteToken.toWindowContainerToken())); + // Even if DisplayArea explicitly participating + participants.put(tda, new Transition.ChangeInfo()); + info = Transition.calculateTransitionInfo(transitType, participants); + assertEquals(2, info.getChanges().size()); + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index 986807e661f12..6237be0f4b268 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -265,6 +265,11 @@ class WindowTestsBase extends SystemServiceTestsBase { return activity; } + /** Creates an {@link ActivityRecord} and adds it to the specified {@link Task}. */ + static ActivityRecord createActivityRecordInTask(Task task) { + return createActivityRecordInTask(task.getDisplayContent(), task); + } + static ActivityRecord createTestActivityRecord(DisplayContent dc) { final ActivityRecord activity = new ActivityBuilder(dc.mWmService.mAtmService).build(); postCreateActivitySetup(activity, dc);