diff --git a/core/api/current.txt b/core/api/current.txt
index ffe14c3f19542..efbc60fb5280c 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -1655,6 +1655,9 @@ package android {
field public static final int windowShowAnimation = 16842934; // 0x10100b6
field public static final int windowShowWallpaper = 16843410; // 0x1010292
field public static final int windowSoftInputMode = 16843307; // 0x101022b
+ field public static final int windowSplashScreenAnimatedIcon = 16844333; // 0x101062d
+ field public static final int windowSplashScreenAnimationDuration = 16844334; // 0x101062e
+ field public static final int windowSplashScreenBackground = 16844332; // 0x101062c
field public static final int windowSplashscreenContent = 16844132; // 0x1010564
field @Deprecated public static final int windowSwipeToDismiss = 16843763; // 0x10103f3
field public static final int windowTitleBackgroundStyle = 16842844; // 0x101005c
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index cefd1b153268b..7b373201a3fc9 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -2729,6 +2729,10 @@ package android.window {
field public static final int FEATURE_WINDOW_TOKENS = 2; // 0x2
}
+ public final class SplashScreenView extends android.widget.FrameLayout {
+ method public boolean isIconAnimating();
+ }
+
public final class StartingWindowInfo implements android.os.Parcelable {
ctor public StartingWindowInfo();
method public int describeContents();
diff --git a/core/java/android/window/SplashScreenView.java b/core/java/android/window/SplashScreenView.java
index b8d3fc2eda05b..3d3d3468c6da4 100644
--- a/core/java/android/window/SplashScreenView.java
+++ b/core/java/android/window/SplashScreenView.java
@@ -17,14 +17,18 @@ package android.window;
import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
+import android.animation.Animator;
+import android.animation.ValueAnimator;
import android.annotation.ColorInt;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.TestApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Rect;
+import android.graphics.drawable.Animatable;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Parcel;
@@ -65,6 +69,11 @@ public final class SplashScreenView extends FrameLayout {
private int mInitBackgroundColor;
private View mIconView;
private Bitmap mParceledBitmap;
+
+ private Animatable mAnimatableIcon;
+ private ValueAnimator mAnimator;
+ private Runnable mAnimationFinishListener;
+
// cache original window and status
private Window mWindow;
private boolean mDrawBarBackground;
@@ -81,6 +90,7 @@ public final class SplashScreenView extends FrameLayout {
private @ColorInt int mBackgroundColor;
private Bitmap mParceledBitmap;
private Drawable mIconDrawable;
+ private int mIconAnimationDuration;
public Builder(@NonNull Context context) {
mContext = context;
@@ -124,6 +134,14 @@ public final class SplashScreenView extends FrameLayout {
return this;
}
+ /**
+ * Set the animation duration if icon is animatable.
+ */
+ public Builder setAnimationDuration(int duration) {
+ mIconAnimationDuration = duration;
+ return this;
+ }
+
/**
* Create SplashScreenWindowView object from materials.
*/
@@ -142,6 +160,7 @@ public final class SplashScreenView extends FrameLayout {
}
if (mIconDrawable != null) {
view.mIconView.setBackground(mIconDrawable);
+ view.initIconAnimation(mIconDrawable, mIconAnimationDuration);
}
if (mParceledBitmap != null) {
view.mParceledBitmap = mParceledBitmap;
@@ -180,6 +199,66 @@ public final class SplashScreenView extends FrameLayout {
return !mNotCopyable;
}
+ void initIconAnimation(Drawable iconDrawable, int duration) {
+ if (iconDrawable instanceof Animatable) {
+ mAnimatableIcon = (Animatable) iconDrawable;
+ mAnimator = ValueAnimator.ofInt(0, 1);
+ mAnimator.setDuration(duration);
+
+ mAnimator.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mAnimatableIcon.start();
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mAnimatableIcon.stop();
+ onIconAnimationFinish();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mAnimatableIcon.stop();
+ onIconAnimationFinish();
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ // do not repeat
+ mAnimatableIcon.stop();
+ }
+ });
+ }
+ }
+
+ private void onIconAnimationFinish() {
+ if (mAnimationFinishListener != null) {
+ mAnimationFinishListener.run();
+ mAnimationFinishListener = null;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @TestApi
+ public boolean isIconAnimating() {
+ return mAnimatableIcon != null && mAnimator.isRunning();
+ }
+
+ /**
+ * @hide
+ */
+ public void startIntroAnimation(Runnable finishListener) {
+ if (mAnimatableIcon != null) {
+ mAnimationFinishListener = finishListener;
+ mAnimator.start();
+ } else if (finishListener != null) {
+ finishListener.run();
+ }
+ }
+
/**
*
Remove this view and release its resource.
* Do not invoke this method from a drawing method
@@ -244,6 +323,7 @@ public final class SplashScreenView extends FrameLayout {
/**
* Get the view containing the Splash Screen icon and its background.
+ * @see android.R.attr#windowSplashScreenAnimatedIcon
*/
public @Nullable View getIconView() {
return mIconView;
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 823e10edaad51..6949c4bb9f41c 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -2262,6 +2262,18 @@
-->
+
+
+
+
+
+
+
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 3c97aec5d55d8..8f4d8baa845d6 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -3066,6 +3066,9 @@
+
+
+
diff --git a/core/res/res/values/themes.xml b/core/res/res/values/themes.xml
index 049ba23a1a97e..325e0578425d8 100644
--- a/core/res/res/values/themes.xml
+++ b/core/res/res/values/themes.xml
@@ -167,6 +167,8 @@ please see themes_device_defaults.xml.
- @drawable/screen_background_selector_dark
- ?attr/colorBackground
+ - @color/transparent
+ - @null
- false
- @null
- false
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index 13f1fddfdfb6c..24198659e15d6 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -48,4 +48,7 @@
- 0.5
+
+
+ 1000
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 a9d66ac945932..03724171da217 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -25,6 +25,8 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TASK_ORG;
import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.ActivityManager.RunningTaskInfo;
import android.content.Context;
import android.os.Binder;
@@ -38,9 +40,6 @@ import android.window.StartingWindowInfo;
import android.window.TaskAppearedInfo;
import android.window.TaskOrganizer;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.common.ShellExecutor;
@@ -111,23 +110,23 @@ public class ShellTaskOrganizer extends TaskOrganizer {
private final SizeCompatUIController mSizeCompatUI;
public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context) {
- this(null /* taskOrganizerController */, mainExecutor, context, null /* sizeCompatUI */);
+ this(null /* taskOrganizerController */, mainExecutor, context, null /* sizeCompatUI */,
+ new StartingSurfaceDrawer(context, mainExecutor));
}
public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context, @Nullable
SizeCompatUIController sizeCompatUI) {
- this(null /* taskOrganizerController */, mainExecutor, context, sizeCompatUI);
+ this(null /* taskOrganizerController */, mainExecutor, context, sizeCompatUI,
+ new StartingSurfaceDrawer(context, mainExecutor));
}
@VisibleForTesting
ShellTaskOrganizer(ITaskOrganizerController taskOrganizerController, ShellExecutor mainExecutor,
- Context context, @Nullable SizeCompatUIController sizeCompatUI) {
+ Context context, @Nullable SizeCompatUIController sizeCompatUI,
+ StartingSurfaceDrawer startingSurfaceDrawer) {
super(taskOrganizerController, mainExecutor);
- // TODO(b/131727939) temporarily live here, the starting surface drawer should be controlled
- // by a controller, that class should be create while porting
- // ActivityRecord#addStartingWindow to WMShell.
- mStartingSurfaceDrawer = new StartingSurfaceDrawer(context, mainExecutor);
mSizeCompatUI = sizeCompatUI;
+ mStartingSurfaceDrawer = startingSurfaceDrawer;
}
@Override
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 0e74c642520b4..db0a4a6ad731b 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
@@ -43,8 +43,9 @@ import java.util.List;
/**
* Util class to create the view for a splash screen content.
+ * @hide
*/
-class SplashscreenContentDrawer {
+public class SplashscreenContentDrawer {
private static final String TAG = StartingSurfaceDrawer.TAG;
private static final boolean DEBUG = StartingSurfaceDrawer.DEBUG_SPLASH_SCREEN;
@@ -55,10 +56,13 @@ class SplashscreenContentDrawer {
// also 108*108 pixels, then do not enlarge this icon if only need to show foreground icon.
private static final float ENLARGE_FOREGROUND_ICON_THRESHOLD = (72f * 72f) / (108f * 108f);
private final Context mContext;
+ private final int mMaxIconAnimationDuration;
+
private int mIconSize;
- SplashscreenContentDrawer(Context context) {
+ SplashscreenContentDrawer(Context context, int maxIconAnimationDuration) {
mContext = context;
+ mMaxIconAnimationDuration = maxIconAnimationDuration;
}
private void updateDensity() {
@@ -90,33 +94,71 @@ class SplashscreenContentDrawer {
return ssc;
}
- final TypedArray typedArray = context.obtainStyledAttributes(
- com.android.internal.R.styleable.Window);
- final int resId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0);
- typedArray.recycle();
+ final SplashScreenWindowAttrs attrs =
+ SplashScreenWindowAttrs.createWindowAttrs(context);
+ final StartingWindowViewBuilder builder = new StartingWindowViewBuilder();
final Drawable themeBGDrawable;
- if (resId == 0) {
+
+ if (attrs.mWindowBgColor != 0) {
+ themeBGDrawable = new ColorDrawable(attrs.mWindowBgColor);
+ } else if (attrs.mWindowBgResId != 0) {
+ themeBGDrawable = context.getDrawable(attrs.mWindowBgResId);
+ } else {
Slog.w(TAG, "Window background not exist!");
themeBGDrawable = createDefaultBackgroundDrawable();
- } else {
- themeBGDrawable = context.getDrawable(resId);
}
- final Drawable iconDrawable = iconRes != 0 ? context.getDrawable(iconRes)
- : context.getPackageManager().getDefaultActivityIcon();
+ final int animationDuration;
+ final Drawable iconDrawable;
+ if (attrs.mReplaceIcon != null) {
+ iconDrawable = attrs.mReplaceIcon;
+ animationDuration = Math.max(0,
+ Math.min(attrs.mAnimationDuration, mMaxIconAnimationDuration));
+ } else {
+ iconDrawable = iconRes != 0 ? context.getDrawable(iconRes)
+ : context.getPackageManager().getDefaultActivityIcon();
+ animationDuration = 0;
+ }
// TODO (b/173975965) Tracking the performance on improved splash screen.
- final StartingWindowViewBuilder builder = new StartingWindowViewBuilder();
return builder
.setWindow(win)
.setContext(context)
.setThemeDrawable(themeBGDrawable)
- .setIconDrawable(iconDrawable).build();
+ .setIconDrawable(iconDrawable)
+ .setIconAnimationDuration(animationDuration).build();
+ }
+
+ private static class SplashScreenWindowAttrs {
+ private int mWindowBgResId = 0;
+ private int mWindowBgColor = Color.TRANSPARENT;
+ private Drawable mReplaceIcon = null;
+ private int mAnimationDuration = 0;
+
+ static SplashScreenWindowAttrs createWindowAttrs(Context context) {
+ final SplashScreenWindowAttrs attrs = new SplashScreenWindowAttrs();
+ final TypedArray typedArray = context.obtainStyledAttributes(
+ com.android.internal.R.styleable.Window);
+ attrs.mWindowBgResId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0);
+ attrs.mWindowBgColor = typedArray.getColor(
+ R.styleable.Window_windowSplashScreenBackground, Color.TRANSPARENT);
+ attrs.mReplaceIcon = typedArray.getDrawable(
+ R.styleable.Window_windowSplashScreenAnimatedIcon);
+ attrs.mAnimationDuration = typedArray.getInt(
+ R.styleable.Window_windowSplashScreenAnimationDuration, 0);
+ typedArray.recycle();
+ if (DEBUG) {
+ Slog.d(TAG, "window attributes color: "
+ + Integer.toHexString(attrs.mWindowBgColor)
+ + " icon " + attrs.mReplaceIcon + " duration " + attrs.mAnimationDuration);
+ }
+ return attrs;
+ }
}
private class StartingWindowViewBuilder {
- // materials
private Drawable mThemeBGDrawable;
private Drawable mIconDrawable;
private Window mWindow;
+ private int mIconAnimationDuration;
private Context mContext;
// result
@@ -144,6 +186,12 @@ class SplashscreenContentDrawer {
return this;
}
+ StartingWindowViewBuilder setIconAnimationDuration(int iconAnimationDuration) {
+ mIconAnimationDuration = iconAnimationDuration;
+ mBuildComplete = false;
+ return this;
+ }
+
StartingWindowViewBuilder setContext(Context context) {
mContext = context;
mBuildComplete = false;
@@ -253,6 +301,7 @@ class SplashscreenContentDrawer {
if (iconDrawable != null) {
builder.setCenterViewDrawable(iconDrawable);
}
+ builder.setAnimationDuration(mIconAnimationDuration);
final SplashScreenView splashScreenView = builder.build();
if (DEBUG) {
Slog.d(TAG, "fillViewWithIcon surfaceWindowView " + splashScreenView);
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 03935cfd4259d..e4c3a0569c661 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
@@ -74,6 +74,8 @@ public class StartingSurfaceDrawer {
private final DisplayManager mDisplayManager;
final ShellExecutor mMainExecutor;
private final SplashscreenContentDrawer mSplashscreenContentDrawer;
+ private final IconAnimationFinishListener mIconAnimationFinishListener =
+ new IconAnimationFinishListener();
// TODO(b/131727939) remove this when clearing ActivityRecord
private static final int REMOVE_WHEN_TIMEOUT = 2000;
@@ -82,7 +84,10 @@ public class StartingSurfaceDrawer {
mContext = context;
mDisplayManager = mContext.getSystemService(DisplayManager.class);
mMainExecutor = mainExecutor;
- mSplashscreenContentDrawer = new SplashscreenContentDrawer(context);
+
+ final int maxIconAnimDuration = context.getResources().getInteger(
+ com.android.wm.shell.R.integer.max_starting_window_intro_icon_anim_duration);
+ mSplashscreenContentDrawer = new SplashscreenContentDrawer(mContext, maxIconAnimDuration);
}
private final SparseArray mStartingWindowRecords = new SparseArray<>();
@@ -401,10 +406,19 @@ public class StartingSurfaceDrawer {
*/
public void copySplashScreenView(int taskId) {
final StartingWindowRecord preView = mStartingWindowRecords.get(taskId);
- final SplashScreenViewParcelable parcelable;
+ SplashScreenViewParcelable parcelable;
if (preView != null && preView.mContentView != null
&& preView.mContentView.isCopyable()) {
- parcelable = new SplashScreenViewParcelable(preView.mContentView);
+ if (preView.mContentView.isIconAnimating()) {
+ // if animating, wait until animation finish
+ if (DEBUG_SPLASH_SCREEN) {
+ Slog.v(TAG, "Copying splash screen view but icon is animating " + taskId);
+ }
+ mIconAnimationFinishListener.waitingForCopyTask(taskId);
+ return;
+ } else {
+ parcelable = new SplashScreenViewParcelable(preView.mContentView);
+ }
} else {
parcelable = null;
}
@@ -445,11 +459,60 @@ public class StartingSurfaceDrawer {
mMainExecutor.executeDelayed(() -> removeWindowSynced(taskId), REMOVE_WHEN_TIMEOUT);
final StartingWindowRecord tView = new StartingWindowRecord(view,
null /* TaskSnapshotWindow */, splashScreenView);
+ splashScreenView.startIntroAnimation(
+ mIconAnimationFinishListener.makeListener(taskId));
mStartingWindowRecords.put(taskId, tView);
}
});
}
+ private class IconAnimationFinishListener {
+ private final SparseArray mListeners = new SparseArray<>();
+
+ private class TaskListener implements Runnable {
+ private int mTargetTaskId;
+ private boolean mWaitingForCopy;
+ private boolean mWaitingForRemove;
+ @Override
+ public void run() {
+ if (mWaitingForCopy) {
+ if (DEBUG_SPLASH_SCREEN) {
+ Slog.v(TAG, "Icon animation finish and waiting for copy at task "
+ + mTargetTaskId);
+ }
+ copySplashScreenView(mTargetTaskId);
+ }
+ if (mWaitingForRemove) {
+ if (DEBUG_SPLASH_SCREEN) {
+ Slog.v(TAG, "Icon animation finish and waiting for remove at task "
+ + mTargetTaskId);
+ }
+ mMainExecutor.execute(() -> removeWindowSynced(mTargetTaskId));
+ }
+ mListeners.remove(mTargetTaskId);
+ }
+ }
+
+ private Runnable makeListener(int taskId) {
+ final TaskListener listener = new TaskListener();
+ listener.mTargetTaskId = taskId;
+ mListeners.put(taskId, listener);
+ return listener;
+ }
+
+ private void waitingForCopyTask(int taskId) {
+ if (mListeners.contains(taskId)) {
+ mListeners.get(taskId).mWaitingForCopy = true;
+ }
+ }
+
+ private void waitingForRemove(int taskId) {
+ if (mListeners.contains(taskId)) {
+ mListeners.get(taskId).mWaitingForRemove = true;
+ }
+ }
+ }
+
protected void removeWindowSynced(int taskId) {
final StartingWindowRecord record = mStartingWindowRecords.get(taskId);
if (record != null) {
@@ -457,6 +520,11 @@ public class StartingSurfaceDrawer {
if (DEBUG_SPLASH_SCREEN) {
Slog.v(TAG, "Removing splash screen window for task: " + taskId);
}
+ if (record.mContentView != null && record.mContentView.isIconAnimating()) {
+ // do not remove until the animation is finish
+ mIconAnimationFinishListener.waitingForRemove(taskId);
+ return;
+ }
final WindowManager wm = record.mDecorView.getContext()
.getSystemService(WindowManager.class);
wm.removeView(record.mDecorView);
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 176b33dda020e..c815e65189e61 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
@@ -52,6 +52,7 @@ 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.sizecompatui.SizeCompatUIController;
+import com.android.wm.shell.startingsurface.StartingSurfaceDrawer;
import org.junit.Before;
import org.junit.Test;
@@ -77,6 +78,8 @@ public class ShellTaskOrganizerTests {
private Context mContext;
@Mock
private SizeCompatUIController mSizeCompatUI;
+ @Mock
+ private StartingSurfaceDrawer mStartingSurfaceDrawer;
ShellTaskOrganizer mOrganizer;
private final SyncTransactionQueue mSyncTransactionQueue = mock(SyncTransactionQueue.class);
@@ -112,7 +115,7 @@ public class ShellTaskOrganizerTests {
.when(mTaskOrganizerController).registerTaskOrganizer(any());
} catch (RemoteException e) {}
mOrganizer = spy(new ShellTaskOrganizer(mTaskOrganizerController, mTestExecutor, mContext,
- mSizeCompatUI));
+ mSizeCompatUI, mStartingSurfaceDrawer));
}
@Test