Merge "Avoid cutting off AVD on splash screen."
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
index e255e44..025bcad 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
@@ -22,6 +22,9 @@
 import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN;
 import static android.window.StartingWindowInfo.STARTING_WINDOW_TYPE_SPLASH_SCREEN;
 
+import static com.android.wm.shell.startingsurface.StartingSurfaceDrawer.MAX_ANIMATION_DURATION;
+import static com.android.wm.shell.startingsurface.StartingSurfaceDrawer.MINIMAL_ANIMATION_DURATION;
+
 import android.annotation.ColorInt;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
@@ -46,6 +49,7 @@
 import android.net.Uri;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.SystemClock;
 import android.os.Trace;
 import android.os.UserHandle;
 import android.util.ArrayMap;
@@ -322,6 +326,33 @@
         private int mAnimationDuration = 0;
     }
 
+    /**
+     * Get an optimal animation duration to keep the splash screen from showing.
+     *
+     * @param animationDuration The animation duration defined from app.
+     * @param appReadyDuration The real duration from the starting the app to the first app window
+     *                         drawn.
+     */
+    @VisibleForTesting
+    static long getShowingDuration(long animationDuration, long appReadyDuration) {
+        if (animationDuration <= appReadyDuration) {
+            // app window ready took longer time than animation, it can be removed ASAP.
+            return appReadyDuration;
+        }
+        if (appReadyDuration < MAX_ANIMATION_DURATION) {
+            if (animationDuration > MAX_ANIMATION_DURATION
+                    || appReadyDuration < MINIMAL_ANIMATION_DURATION) {
+                // animation is too long or too short, cut off with minimal duration
+                return MINIMAL_ANIMATION_DURATION;
+            }
+            // animation is longer than dOpt but shorter than max, allow it to play till finish
+            return MAX_ANIMATION_DURATION;
+        }
+        // the shortest duration is longer than dMax, cut off no matter how long the animation
+        // will be.
+        return appReadyDuration;
+    }
+
     private class StartingWindowViewBuilder {
         private final Context mContext;
         private final ActivityInfo mActivityInfo;
@@ -977,9 +1008,27 @@
      * Create and play the default exit animation for splash screen view.
      */
     void applyExitAnimation(SplashScreenView view, SurfaceControl leash,
-            Rect frame, Runnable finishCallback) {
-        final SplashScreenExitAnimation animation = new SplashScreenExitAnimation(mContext, view,
-                leash, frame, mMainWindowShiftLength, mTransactionPool, finishCallback);
-        animation.startAnimations();
+            Rect frame, Runnable finishCallback, long createTime) {
+        final Runnable playAnimation = () -> {
+            final SplashScreenExitAnimation animation = new SplashScreenExitAnimation(mContext,
+                    view, leash, frame, mMainWindowShiftLength, mTransactionPool, finishCallback);
+            animation.startAnimations();
+        };
+        if (view.getIconView() == null) {
+            playAnimation.run();
+            return;
+        }
+        final long appReadyDuration = SystemClock.uptimeMillis() - createTime;
+        final long animDuration = view.getIconAnimationDuration() != null
+                ? view.getIconAnimationDuration().toMillis() : 0;
+        final long minimumShowingDuration = getShowingDuration(animDuration, appReadyDuration);
+        final long delayed = minimumShowingDuration - appReadyDuration;
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
+                "applyExitAnimation delayed: %s", delayed);
+        if (delayed > 0) {
+            view.postDelayed(playAnimation, delayed);
+        } else {
+            playAnimation.run();
+        }
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
index 9a966b8..3442699 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
@@ -41,6 +41,7 @@
 import android.os.IBinder;
 import android.os.RemoteCallback;
 import android.os.RemoteException;
+import android.os.SystemClock;
 import android.os.Trace;
 import android.os.UserHandle;
 import android.util.Slog;
@@ -121,6 +122,25 @@
     private final StartingWindowRemovalInfo mTmpRemovalInfo = new StartingWindowRemovalInfo();
 
     /**
+     * The minimum duration during which the splash screen is shown when the splash screen icon is
+     * animated.
+     */
+    static final long MINIMAL_ANIMATION_DURATION = 400L;
+
+    /**
+     * Allow the icon style splash screen to be displayed for longer to give time for the animation
+     * to finish, i.e. the extra buffer time to keep the splash screen if the animation is slightly
+     * longer than the {@link #MINIMAL_ANIMATION_DURATION} duration.
+     */
+    static final long TIME_WINDOW_DURATION = 100L;
+
+    /**
+     * The maximum duration during which the splash screen will be shown if the application is ready
+     * to show before the icon animation finishes.
+     */
+    static final long MAX_ANIMATION_DURATION = MINIMAL_ANIMATION_DURATION + TIME_WINDOW_DURATION;
+
+    /**
      * @param splashScreenExecutor The thread used to control add and remove starting window.
      */
     public StartingSurfaceDrawer(Context context, ShellExecutor splashScreenExecutor,
@@ -593,7 +613,8 @@
                         if (removalInfo.playRevealAnimation) {
                             mSplashscreenContentDrawer.applyExitAnimation(record.mContentView,
                                     removalInfo.windowAnimationLeash, removalInfo.mainFrame,
-                                    () -> removeWindowInner(record.mDecorView, true));
+                                    () -> removeWindowInner(record.mDecorView, true),
+                                    record.mCreateTime);
                         } else {
                             // the SplashScreenView has been copied to client, hide the view to skip
                             // default exit animation
@@ -641,6 +662,7 @@
         private boolean mSetSplashScreen;
         private @StartingWindowType int mSuggestType;
         private int mBGColor;
+        private final long mCreateTime;
 
         StartingWindowRecord(IBinder appToken, View decorView,
                 TaskSnapshotWindow taskSnapshotWindow, @StartingWindowType int suggestType) {
@@ -651,6 +673,7 @@
                 mBGColor = mTaskSnapshotWindow.getBackgroundColor();
             }
             mSuggestType = suggestType;
+            mCreateTime = SystemClock.uptimeMillis();
         }
 
         private void setSplashScreenView(SplashScreenView splashScreenView) {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
index d92b12e..630d0d2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawerTests.java
@@ -24,6 +24,8 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.wm.shell.startingsurface.StartingSurfaceDrawer.MAX_ANIMATION_DURATION;
+import static com.android.wm.shell.startingsurface.StartingSurfaceDrawer.MINIMAL_ANIMATION_DURATION;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
@@ -297,6 +299,56 @@
         assertEquals(mStartingSurfaceDrawer.mStartingWindowRecords.size(), 0);
     }
 
+    @Test
+    public void testMinimumAnimationDuration() {
+        final long maxDuration = MAX_ANIMATION_DURATION;
+        final long minDuration = MINIMAL_ANIMATION_DURATION;
+
+        final long shortDuration = minDuration - 1;
+        final long medianShortDuration = minDuration + 1;
+        final long medianLongDuration = maxDuration - 1;
+        final long longAppDuration = maxDuration + 1;
+
+        // static icon
+        assertEquals(shortDuration, SplashscreenContentDrawer.getShowingDuration(
+                0, shortDuration));
+        // median launch + static icon
+        assertEquals(medianShortDuration, SplashscreenContentDrawer.getShowingDuration(
+                0, medianShortDuration));
+        // long launch + static icon
+        assertEquals(longAppDuration, SplashscreenContentDrawer.getShowingDuration(
+                0, longAppDuration));
+
+        // fast launch + animatable icon
+        assertEquals(shortDuration, SplashscreenContentDrawer.getShowingDuration(
+                shortDuration, shortDuration));
+        assertEquals(minDuration, SplashscreenContentDrawer.getShowingDuration(
+                medianShortDuration, shortDuration));
+        assertEquals(minDuration, SplashscreenContentDrawer.getShowingDuration(
+                longAppDuration, shortDuration));
+
+        // median launch + animatable icon
+        assertEquals(medianShortDuration, SplashscreenContentDrawer.getShowingDuration(
+                shortDuration, medianShortDuration));
+        assertEquals(medianShortDuration, SplashscreenContentDrawer.getShowingDuration(
+                medianShortDuration, medianShortDuration));
+        assertEquals(minDuration, SplashscreenContentDrawer.getShowingDuration(
+                longAppDuration, medianShortDuration));
+        // between min < max launch + animatable icon
+        assertEquals(medianLongDuration, SplashscreenContentDrawer.getShowingDuration(
+                medianShortDuration, medianLongDuration));
+        assertEquals(maxDuration, SplashscreenContentDrawer.getShowingDuration(
+                medianLongDuration, medianShortDuration));
+
+        // long launch + animatable icon
+        assertEquals(longAppDuration, SplashscreenContentDrawer.getShowingDuration(
+                shortDuration, longAppDuration));
+        assertEquals(longAppDuration, SplashscreenContentDrawer.getShowingDuration(
+                medianShortDuration, longAppDuration));
+        assertEquals(longAppDuration, SplashscreenContentDrawer.getShowingDuration(
+                longAppDuration, longAppDuration));
+    }
+
     private StartingWindowInfo createWindowInfo(int taskId, int themeResId) {
         StartingWindowInfo windowInfo = new StartingWindowInfo();
         final ActivityInfo info = new ActivityInfo();