diff options
58 files changed, 2426 insertions, 229 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index cc5399de24cc..9ef3eb462552 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -1295,6 +1295,7 @@ package android { field public static final int spinnerMode = 16843505; // 0x10102f1 field public static final int spinnerStyle = 16842881; // 0x1010081 field public static final int spinnersShown = 16843595; // 0x101034b + field public static final int splashScreenTheme = 16844336; // 0x1010630 field public static final int splitMotionEvents = 16843503; // 0x10102ef field public static final int splitName = 16844105; // 0x1010549 field public static final int splitTrack = 16843852; // 0x101044c @@ -1655,6 +1656,10 @@ 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 windowSplashScreenBrandingImage = 16844335; // 0x101062f 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 @@ -3865,6 +3870,7 @@ package android.app { method @Nullable public android.net.Uri getReferrer(); method public int getRequestedOrientation(); method public final android.view.SearchEvent getSearchEvent(); + method @NonNull public final android.window.SplashScreen getSplashScreen(); method public int getTaskId(); method public final CharSequence getTitle(); method public final int getTitleColor(); @@ -6653,6 +6659,7 @@ package android.app { method @NonNull public java.time.LocalTime getCustomNightModeEnd(); method @NonNull public java.time.LocalTime getCustomNightModeStart(); method public int getNightMode(); + method public void setApplicationNightMode(int); method public void setCustomNightModeEnd(@NonNull java.time.LocalTime); method public void setCustomNightModeStart(@NonNull java.time.LocalTime); method public void setNightMode(int); @@ -12779,6 +12786,7 @@ package android.content.pm { method @NonNull public android.content.pm.ShortcutInfo.Builder setPersons(@NonNull android.app.Person[]); method @NonNull public android.content.pm.ShortcutInfo.Builder setRank(int); method @NonNull public android.content.pm.ShortcutInfo.Builder setShortLabel(@NonNull CharSequence); + method @NonNull public android.content.pm.ShortcutInfo.Builder setStartingTheme(int); } public class ShortcutManager { @@ -55824,6 +55832,25 @@ package android.widget.inline { } +package android.window { + + public interface SplashScreen { + method public void setOnExitAnimationListener(@Nullable android.window.SplashScreen.OnExitAnimationListener); + } + + public static interface SplashScreen.OnExitAnimationListener { + method public void onSplashScreenExit(@NonNull android.window.SplashScreenView); + } + + public final class SplashScreenView extends android.widget.FrameLayout { + method public long getIconAnimationDurationMillis(); + method public long getIconAnimationStartMillis(); + method @Nullable public android.view.View getIconView(); + method public void remove(); + } + +} + package javax.microedition.khronos.egl { public interface EGL { diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 1be0fba570ab..29be5c6e4681 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 @Nullable public android.view.View getBrandingView(); + } + public final class StartingWindowInfo implements android.os.Parcelable { ctor public StartingWindowInfo(); method public int describeContents(); @@ -2748,6 +2752,7 @@ package android.window { public class TaskOrganizer extends android.window.WindowOrganizer { ctor public TaskOrganizer(); method @BinderThread public void addStartingWindow(@NonNull android.window.StartingWindowInfo, @NonNull android.os.IBinder); + method @BinderThread public void copySplashScreenView(int); method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public void createRootTask(int, int, @Nullable android.os.IBinder); method @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public boolean deleteRootTask(@NonNull android.window.WindowContainerToken); method @Nullable @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public java.util.List<android.app.ActivityManager.RunningTaskInfo> getChildTasks(@NonNull android.window.WindowContainerToken, @NonNull int[]); diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 4728f11a402d..992d054737b9 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -139,6 +139,8 @@ import android.view.translation.UiTranslationController; import android.widget.AdapterView; import android.widget.Toast; import android.widget.Toolbar; +import android.window.SplashScreen; +import android.window.SplashScreenView; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; @@ -961,6 +963,10 @@ public class Activity extends ContextThemeWrapper private UiTranslationController mUiTranslationController; + private SplashScreen mSplashScreen; + /** @hide */ + SplashScreenView mSplashScreenView; + private final WindowControllerCallback mWindowControllerCallback = new WindowControllerCallback() { /** @@ -1603,6 +1609,23 @@ public class Activity extends ContextThemeWrapper } /** + * Get the interface that activity use to talk to the splash screen. + * @see SplashScreen + */ + public final @NonNull SplashScreen getSplashScreen() { + return getOrCreateSplashScreen(); + } + + private SplashScreen getOrCreateSplashScreen() { + synchronized (this) { + if (mSplashScreen == null) { + mSplashScreen = new SplashScreen.SplashScreenImpl(this); + } + return mSplashScreen; + } + } + + /** * Same as {@link #onCreate(android.os.Bundle)} but called for those activities created with * the attribute {@link android.R.attr#persistableMode} set to * <code>persistAcrossReboots</code>. diff --git a/core/java/android/app/ActivityClient.java b/core/java/android/app/ActivityClient.java index 401f8cc13bad..e3b5e9a32324 100644 --- a/core/java/android/app/ActivityClient.java +++ b/core/java/android/app/ActivityClient.java @@ -46,9 +46,9 @@ public class ActivityClient { } /** Reports {@link Activity#onResume()} is done. */ - public void activityResumed(IBinder token) { + public void activityResumed(IBinder token, boolean handleSplashScreenExit) { try { - getActivityClientController().activityResumed(token); + getActivityClientController().activityResumed(token, handleSplashScreenExit); } catch (RemoteException e) { e.rethrowFromSystemServer(); } @@ -488,6 +488,17 @@ public class ActivityClient { } } + /** + * Reports the splash screen view has attached to client. + */ + void reportSplashScreenAttached(IBinder token) { + try { + getActivityClientController().splashScreenAttached(token); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + public static ActivityClient getInstance() { return sInstance.get(); } diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index 28da1c3a3eb7..73cc13c82bcb 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -160,6 +160,12 @@ public class ActivityOptions { public static final String KEY_ANIM_START_LISTENER = "android:activity.animStartListener"; /** + * Specific a theme for a splash screen window. + * @hide + */ + public static final String KEY_SPLASH_SCREEN_THEME = "android.activity.splashScreenTheme"; + + /** * Callback for when the last frame of the animation is played. * @hide */ @@ -398,6 +404,7 @@ public class ActivityOptions { private IBinder mLaunchCookie; private IRemoteTransition mRemoteTransition; private boolean mOverrideTaskTransition; + private int mSplashScreenThemeResId; /** * Create an ActivityOptions specifying a custom animation to run when @@ -1147,6 +1154,7 @@ public class ActivityOptions { mRemoteTransition = IRemoteTransition.Stub.asInterface(opts.getBinder( KEY_REMOTE_TRANSITION)); mOverrideTaskTransition = opts.getBoolean(KEY_OVERRIDE_TASK_TRANSITION); + mSplashScreenThemeResId = opts.getInt(KEY_SPLASH_SCREEN_THEME); } /** @@ -1333,6 +1341,14 @@ public class ActivityOptions { } /** + * Gets whether the activity want to be launched as other theme for the splash screen. + * @hide + */ + public int getSplashScreenThemeResId() { + return mSplashScreenThemeResId; + } + + /** * Sets whether the activity is to be launched into LockTask mode. * * Use this option to start an activity in LockTask mode. Note that only apps permitted by @@ -1838,6 +1854,9 @@ public class ActivityOptions { if (mOverrideTaskTransition) { b.putBoolean(KEY_OVERRIDE_TASK_TRANSITION, mOverrideTaskTransition); } + if (mSplashScreenThemeResId != 0) { + b.putInt(KEY_SPLASH_SCREEN_THEME, mSplashScreenThemeResId); + } return b; } diff --git a/core/java/android/app/ActivityTaskManager.java b/core/java/android/app/ActivityTaskManager.java index 70fa4445479b..233f737b8e0f 100644 --- a/core/java/android/app/ActivityTaskManager.java +++ b/core/java/android/app/ActivityTaskManager.java @@ -19,6 +19,7 @@ package android.app; import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemService; import android.annotation.TestApi; @@ -37,6 +38,7 @@ import android.os.ServiceManager; import android.util.DisplayMetrics; import android.util.Singleton; import android.view.RemoteAnimationDefinition; +import android.window.SplashScreenView.SplashScreenViewParcelable; import java.util.List; @@ -243,6 +245,22 @@ public class ActivityTaskManager { } /** + * Notify the server that splash screen of the given task has been copied" + * + * @param taskId Id of task to handle the material to reconstruct the splash screen view. + * @param parcelable Used to reconstruct the view, null means the surface is un-copyable. + * @hide + */ + public void onSplashScreenViewCopyFinished(int taskId, + @Nullable SplashScreenViewParcelable parcelable) { + try { + getService().onSplashScreenViewCopyFinished(taskId, parcelable); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Return the default limit on the number of recents that an app can make. * @hide */ diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index bb6a774cbee2..3d9f6123963f 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -171,12 +171,15 @@ import android.view.View; import android.view.ViewDebug; import android.view.ViewManager; import android.view.ViewRootImpl; +import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.view.autofill.AutofillId; import android.view.translation.TranslationSpec; import android.webkit.WebView; +import android.window.SplashScreen; +import android.window.SplashScreenView; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; @@ -185,6 +188,7 @@ import com.android.internal.content.ReferrerIntent; import com.android.internal.os.BinderInternal; import com.android.internal.os.RuntimeInit; import com.android.internal.os.SomeArgs; +import com.android.internal.policy.DecorView; import com.android.internal.util.ArrayUtils; import com.android.internal.util.FastPrintWriter; import com.android.internal.util.Preconditions; @@ -227,6 +231,7 @@ import java.util.Map; import java.util.Objects; import java.util.TimeZone; import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; @@ -285,6 +290,7 @@ public final class ActivityThread extends ClientTransactionHandler { /** Use background GC policy and default JIT threshold. */ private static final int VM_PROCESS_STATE_JANK_IMPERCEPTIBLE = 1; + private static final int REMOVE_SPLASH_SCREEN_VIEW_TIMEOUT = 5000; /** * Denotes an invalid sequence number corresponding to a process state change. */ @@ -486,6 +492,8 @@ public final class ActivityThread extends ClientTransactionHandler { final ArrayMap<Activity, ArrayList<OnActivityPausedListener>> mOnPauseListeners = new ArrayMap<Activity, ArrayList<OnActivityPausedListener>>(); + private SplashScreen.SplashScreenManagerGlobal mSplashScreenGlobal; + final GcIdler mGcIdler = new GcIdler(); final PurgeIdler mPurgeIdler = new PurgeIdler(); @@ -1930,6 +1938,8 @@ public final class ActivityThread extends ClientTransactionHandler { public static final int INSTRUMENT_WITHOUT_RESTART = 170; public static final int FINISH_INSTRUMENTATION_WITHOUT_RESTART = 171; + public static final int REMOVE_SPLASH_SCREEN_VIEW = 172; + String codeToString(int code) { if (DEBUG_MESSAGES) { switch (code) { @@ -1976,6 +1986,8 @@ public final class ActivityThread extends ClientTransactionHandler { case INSTRUMENT_WITHOUT_RESTART: return "INSTRUMENT_WITHOUT_RESTART"; case FINISH_INSTRUMENTATION_WITHOUT_RESTART: return "FINISH_INSTRUMENTATION_WITHOUT_RESTART"; + case REMOVE_SPLASH_SCREEN_VIEW: + return "REMOVE_SPLASH_SCREEN_VIEW"; } } return Integer.toString(code); @@ -2169,6 +2181,9 @@ public final class ActivityThread extends ClientTransactionHandler { case FINISH_INSTRUMENTATION_WITHOUT_RESTART: handleFinishInstrumentationWithoutRestart(); break; + case REMOVE_SPLASH_SCREEN_VIEW: + handleRemoveSplashScreenView((ActivityClientRecord) msg.obj); + break; } Object obj = msg.obj; if (obj instanceof SomeArgs) { @@ -3955,6 +3970,106 @@ public final class ActivityThread extends ClientTransactionHandler { } /** + * Register a splash screen manager to this process. + */ + public void registerSplashScreenManager( + @NonNull SplashScreen.SplashScreenManagerGlobal manager) { + synchronized (this) { + mSplashScreenGlobal = manager; + } + } + + @Override + public boolean isHandleSplashScreenExit(@NonNull IBinder token) { + synchronized (this) { + return mSplashScreenGlobal != null && mSplashScreenGlobal.containsExitListener(token); + } + } + + @Override + public void handleAttachSplashScreenView(@NonNull ActivityClientRecord r, + @Nullable SplashScreenView.SplashScreenViewParcelable parcelable) { + final DecorView decorView = (DecorView) r.window.peekDecorView(); + if (parcelable != null && decorView != null) { + createSplashScreen(r, decorView, parcelable); + } else { + // shouldn't happen! + Slog.e(TAG, "handleAttachSplashScreenView failed, unable to attach"); + } + } + + private void createSplashScreen(ActivityClientRecord r, DecorView decorView, + SplashScreenView.SplashScreenViewParcelable parcelable) { + final SplashScreenView.Builder builder = new SplashScreenView.Builder(r.activity); + final SplashScreenView view = builder.createFromParcel(parcelable).build(); + decorView.addView(view); + view.cacheRootWindow(r.window); + view.makeSystemUIColorsTransparent(); + r.activity.mSplashScreenView = view; + view.requestLayout(); + // Ensure splash screen view is shown before remove the splash screen window. + final ViewRootImpl impl = decorView.getViewRootImpl(); + final boolean hardwareEnabled = impl != null && impl.isHardwareEnabled(); + final AtomicBoolean notified = new AtomicBoolean(); + if (hardwareEnabled) { + final Runnable frameCommit = new Runnable() { + @Override + public void run() { + view.post(() -> { + if (!notified.get()) { + view.getViewTreeObserver().unregisterFrameCommitCallback(this); + ActivityClient.getInstance().reportSplashScreenAttached( + r.token); + notified.set(true); + } + }); + } + }; + view.getViewTreeObserver().registerFrameCommitCallback(frameCommit); + } else { + final ViewTreeObserver.OnDrawListener onDrawListener = + new ViewTreeObserver.OnDrawListener() { + @Override + public void onDraw() { + view.post(() -> { + if (!notified.get()) { + view.getViewTreeObserver().removeOnDrawListener(this); + ActivityClient.getInstance().reportSplashScreenAttached( + r.token); + notified.set(true); + } + }); + } + }; + view.getViewTreeObserver().addOnDrawListener(onDrawListener); + } + } + + @Override + public void handOverSplashScreenView(@NonNull ActivityClientRecord r) { + if (r.activity.mSplashScreenView != null) { + Message msg = mH.obtainMessage(H.REMOVE_SPLASH_SCREEN_VIEW, r); + mH.sendMessageDelayed(msg, REMOVE_SPLASH_SCREEN_VIEW_TIMEOUT); + synchronized (this) { + if (mSplashScreenGlobal != null) { + mSplashScreenGlobal.dispatchOnExitAnimation(r.token, + r.activity.mSplashScreenView); + } + } + } + } + + /** + * Force remove splash screen view. + */ + private void handleRemoveSplashScreenView(@NonNull ActivityClientRecord r) { + if (r.activity.mSplashScreenView != null) { + r.activity.mSplashScreenView.remove(); + r.activity.mSplashScreenView = null; + } + } + + /** * Cycle activity through onPause and onUserLeaveHint so that PIP is entered if supported, then * return to its previous state. This allows activities that rely on onUserLeaveHint instead of * onPictureInPictureRequested to enter picture-in-picture. @@ -5174,6 +5289,11 @@ public final class ActivityThread extends ClientTransactionHandler { r.setState(ON_DESTROY); mLastReportedWindowingMode.remove(r.activity.getActivityToken()); schedulePurgeIdler(); + synchronized (this) { + if (mSplashScreenGlobal != null) { + mSplashScreenGlobal.tokenDestroyed(r.token); + } + } // updatePendingActivityConfiguration() reads from mActivities to update // ActivityClientRecord which runs in a different thread. Protect modifications to // mActivities to avoid race. diff --git a/core/java/android/app/ClientTransactionHandler.java b/core/java/android/app/ClientTransactionHandler.java index 0e1c827145d2..cf5fd148c030 100644 --- a/core/java/android/app/ClientTransactionHandler.java +++ b/core/java/android/app/ClientTransactionHandler.java @@ -28,6 +28,7 @@ import android.content.res.Configuration; import android.os.IBinder; import android.util.MergedConfiguration; import android.view.DisplayAdjustments.FixedRotationAdjustments; +import android.window.SplashScreenView.SplashScreenViewParcelable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.ReferrerIntent; @@ -158,6 +159,16 @@ public abstract class ClientTransactionHandler { /** Request that an activity enter picture-in-picture. */ public abstract void handlePictureInPictureRequested(@NonNull ActivityClientRecord r); + /** Whether the activity want to handle splash screen exit animation */ + public abstract boolean isHandleSplashScreenExit(@NonNull IBinder token); + + /** Attach a splash screen window view to the top of the activity */ + public abstract void handleAttachSplashScreenView(@NonNull ActivityClientRecord r, + @NonNull SplashScreenViewParcelable parcelable); + + /** Hand over the splash screen window view to the activity */ + public abstract void handOverSplashScreenView(@NonNull ActivityClientRecord r); + /** Perform activity launch. */ public abstract Activity handleLaunchActivity(@NonNull ActivityClientRecord r, PendingTransactionActions pendingActions, Intent customIntent); diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl index 9d3286fa271c..bb743b89e00f 100644 --- a/core/java/android/app/IActivityClientController.aidl +++ b/core/java/android/app/IActivityClientController.aidl @@ -34,7 +34,7 @@ import com.android.internal.policy.IKeyguardDismissCallback; */ interface IActivityClientController { oneway void activityIdle(in IBinder token, in Configuration config, in boolean stopProfiling); - oneway void activityResumed(in IBinder token); + oneway void activityResumed(in IBinder token, in boolean handleSplashScreenExit); oneway void activityTopResumedStateLost(); /** * Notifies that the activity has completed paused. This call is not one-way because it can make @@ -142,4 +142,7 @@ interface IActivityClientController { * on the back stack. */ oneway void onBackPressedOnTaskRoot(in IBinder token); + + /** Reports that the splash screen view has attached to activity. */ + oneway void splashScreenAttached(in IBinder token); } diff --git a/core/java/android/app/IActivityTaskManager.aidl b/core/java/android/app/IActivityTaskManager.aidl index 38a3e70b3742..542f754ce364 100644 --- a/core/java/android/app/IActivityTaskManager.aidl +++ b/core/java/android/app/IActivityTaskManager.aidl @@ -70,6 +70,7 @@ import android.view.IRecentsAnimationRunner; import android.view.RemoteAnimationDefinition; import android.view.RemoteAnimationAdapter; import android.window.IWindowOrganizerController; +import android.window.SplashScreenView; import com.android.internal.app.IVoiceInteractor; import com.android.internal.os.IResultReceiver; @@ -348,4 +349,10 @@ interface IActivityTaskManager { * Clears launch params for given packages. */ void clearLaunchParamsForPackages(in List<String> packageNames); + + /** + * A splash screen view has copied. + */ + void onSplashScreenViewCopyFinished(int taskId, + in SplashScreenView.SplashScreenViewParcelable material); } diff --git a/core/java/android/app/IUiModeManager.aidl b/core/java/android/app/IUiModeManager.aidl index 0ba5beccbf32..f71eebdc66ec 100644 --- a/core/java/android/app/IUiModeManager.aidl +++ b/core/java/android/app/IUiModeManager.aidl @@ -62,6 +62,16 @@ interface IUiModeManager { int getNightMode(); /** + * Sets the dark mode for the given application. This setting is persisted and will override the + * system configuration for this application. + * 1 - notnight mode + * 2 - night mode + * 3 - automatic mode switching + * @throws RemoteException + */ + void setApplicationNightMode(in int mode); + + /** * Tells if UI mode is locked or not. */ boolean isUiModeLocked(); diff --git a/core/java/android/app/UiModeManager.java b/core/java/android/app/UiModeManager.java index e1c262caf44f..9b99ab8e31cb 100644 --- a/core/java/android/app/UiModeManager.java +++ b/core/java/android/app/UiModeManager.java @@ -477,11 +477,13 @@ public class UiModeManager { * Changes to night mode take effect globally and will result in a configuration change * (and potentially an Activity lifecycle event) being applied to all running apps. * Developers interested in an app-local implementation of night mode should consider using - * {@link android.support.v7.app.AppCompatDelegate#setDefaultNightMode(int)} to manage the - * -night qualifier locally. + * {@link #setApplicationNightMode(int)} to set and persist the -night qualifier locally or + * {@link android.support.v7.app.AppCompatDelegate#setDefaultNightMode(int)} for the + * backward compatible implementation. * * @param mode the night mode to set * @see #getNightMode() + * @see #setApplicationNightMode(int) */ public void setNightMode(@NightMode int mode) { if (mService != null) { @@ -494,6 +496,44 @@ public class UiModeManager { } /** + * Sets and persist the night mode for this application. + * <p> + * The mode can be one of: + * <ul> + * <li><em>{@link #MODE_NIGHT_NO}<em> sets the device into + * {@code notnight} mode</li> + * <li><em>{@link #MODE_NIGHT_YES}</em> sets the device into + * {@code night} mode</li> + * <li><em>{@link #MODE_NIGHT_CUSTOM}</em> automatically switches between + * {@code night} and {@code notnight} based on the custom time set (or default)</li> + * <li><em>{@link #MODE_NIGHT_AUTO}</em> automatically switches between + * {@code night} and {@code notnight} based on the device's current + * location and certain other sensors</li> + * </ul> + * <p> + * Changes to night mode take effect locally and will result in a configuration change + * (and potentially an Activity lifecycle event) being applied to this application. The mode + * is persisted for this application until it is either modified by the application, the + * user clears the data for the application, or this application is uninstalled. + * <p> + * Developers interested in a non-persistent app-local implementation of night mode should + * consider using {@link android.support.v7.app.AppCompatDelegate#setDefaultNightMode(int)} + * to manage the -night qualifier locally. + * + * @param mode the night mode to set + * @see #setNightMode(int) + */ + public void setApplicationNightMode(@NightMode int mode) { + if (mService != null) { + try { + mService.setApplicationNightMode(mode); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** * Returns the currently configured night mode. * <p> * May be one of: diff --git a/core/java/android/app/servertransaction/ResumeActivityItem.java b/core/java/android/app/servertransaction/ResumeActivityItem.java index d451599cc7b0..e6fdc006615a 100644 --- a/core/java/android/app/servertransaction/ResumeActivityItem.java +++ b/core/java/android/app/servertransaction/ResumeActivityItem.java @@ -60,7 +60,7 @@ public class ResumeActivityItem extends ActivityLifecycleItem { public void postExecute(ClientTransactionHandler client, IBinder token, PendingTransactionActions pendingActions) { // TODO(lifecycler): Use interface callback instead of actual implementation. - ActivityClient.getInstance().activityResumed(token); + ActivityClient.getInstance().activityResumed(token, client.isHandleSplashScreenExit(token)); } @Override diff --git a/core/java/android/app/servertransaction/TransferSplashScreenViewStateItem.java b/core/java/android/app/servertransaction/TransferSplashScreenViewStateItem.java new file mode 100644 index 000000000000..5374984d31d0 --- /dev/null +++ b/core/java/android/app/servertransaction/TransferSplashScreenViewStateItem.java @@ -0,0 +1,105 @@ +/* + * 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.app.servertransaction; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityThread; +import android.app.ClientTransactionHandler; +import android.os.Parcel; +import android.window.SplashScreenView.SplashScreenViewParcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Transfer a splash screen view to an Activity. + * @hide + */ +public class TransferSplashScreenViewStateItem extends ActivityTransactionItem { + + private SplashScreenViewParcelable mSplashScreenViewParcelable; + private @TransferRequest int mRequest; + + @IntDef(value = { + ATTACH_TO, + HANDOVER_TO + }) + @Retention(RetentionPolicy.SOURCE) + public @interface TransferRequest {} + // request client to attach the view on it. + public static final int ATTACH_TO = 0; + // tell client that you can handle the splash screen view. + public static final int HANDOVER_TO = 1; + + @Override + public void execute(@NonNull ClientTransactionHandler client, + @NonNull ActivityThread.ActivityClientRecord r, + PendingTransactionActions pendingActions) { + switch (mRequest) { + case ATTACH_TO: + client.handleAttachSplashScreenView(r, mSplashScreenViewParcelable); + break; + case HANDOVER_TO: + client.handOverSplashScreenView(r); + break; + } + } + + @Override + public void recycle() { + ObjectPool.recycle(this); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mRequest); + dest.writeTypedObject(mSplashScreenViewParcelable, flags); + } + + private TransferSplashScreenViewStateItem() {} + private TransferSplashScreenViewStateItem(Parcel in) { + mRequest = in.readInt(); + mSplashScreenViewParcelable = in.readTypedObject(SplashScreenViewParcelable.CREATOR); + } + + /** Obtain an instance initialized with provided params. */ + public static TransferSplashScreenViewStateItem obtain(@TransferRequest int state, + @Nullable SplashScreenViewParcelable parcelable) { + TransferSplashScreenViewStateItem instance = + ObjectPool.obtain(TransferSplashScreenViewStateItem.class); + if (instance == null) { + instance = new TransferSplashScreenViewStateItem(); + } + instance.mRequest = state; + instance.mSplashScreenViewParcelable = parcelable; + + return instance; + } + + public static final @NonNull Creator<TransferSplashScreenViewStateItem> CREATOR = + new Creator<TransferSplashScreenViewStateItem>() { + public TransferSplashScreenViewStateItem createFromParcel(Parcel in) { + return new TransferSplashScreenViewStateItem(in); + } + + public TransferSplashScreenViewStateItem[] newArray(int size) { + return new TransferSplashScreenViewStateItem[size]; + } + }; +} diff --git a/core/java/android/content/pm/AppSearchShortcutInfo.java b/core/java/android/content/pm/AppSearchShortcutInfo.java index 85549d854c6d..ebe202b2a3fa 100644 --- a/core/java/android/content/pm/AppSearchShortcutInfo.java +++ b/core/java/android/content/pm/AppSearchShortcutInfo.java @@ -273,7 +273,7 @@ public class AppSearchShortcutInfo extends GenericDocument { text, 0, null, disabledMessage, 0, null, categoriesSet, intents, rank, extras, getCreationTimestampMillis(), flags, iconResId, iconResName, bitmapPath, iconUri, - disabledReason, persons, locusId); + disabledReason, persons, locusId, 0); } /** @hide */ diff --git a/core/java/android/content/pm/ShortcutInfo.java b/core/java/android/content/pm/ShortcutInfo.java index 522f4ca88519..ce0547f5d0f4 100644 --- a/core/java/android/content/pm/ShortcutInfo.java +++ b/core/java/android/content/pm/ShortcutInfo.java @@ -434,6 +434,8 @@ public final class ShortcutInfo implements Parcelable { private int mDisabledReason; + private int mStartingThemeResId; + private ShortcutInfo(Builder b) { mUserId = b.mContext.getUserId(); @@ -462,6 +464,7 @@ public final class ShortcutInfo implements Parcelable { mLocusId = b.mLocusId; updateTimestamp(); + mStartingThemeResId = b.mStartingThemeResId; } /** @@ -608,6 +611,7 @@ public final class ShortcutInfo implements Parcelable { // Set this bit. mFlags |= FLAG_KEY_FIELDS_ONLY; } + mStartingThemeResId = source.mStartingThemeResId; } /** @@ -931,6 +935,9 @@ public final class ShortcutInfo implements Parcelable { if (source.mLocusId != null) { mLocusId = source.mLocusId; } + if (source.mStartingThemeResId != 0) { + mStartingThemeResId = source.mStartingThemeResId; + } } /** @@ -1000,6 +1007,8 @@ public final class ShortcutInfo implements Parcelable { private LocusId mLocusId; + private int mStartingThemeResId; + /** * Old style constructor. * @hide @@ -1102,6 +1111,15 @@ public final class ShortcutInfo implements Parcelable { } /** + * Sets a theme resource id for the splash screen. + */ + @NonNull + public Builder setStartingTheme(int themeResId) { + mStartingThemeResId = themeResId; + return this; + } + + /** * @hide We don't support resource strings for dynamic shortcuts for now. (But unit tests * use it.) */ @@ -1420,6 +1438,14 @@ public final class ShortcutInfo implements Parcelable { return mIcon; } + /** + * Returns the theme resource id used for the splash screen. + * @hide + */ + public int getStartingThemeResId() { + return mStartingThemeResId; + } + /** @hide -- old signature, the internal code still uses it. */ @Nullable @Deprecated @@ -2138,6 +2164,7 @@ public final class ShortcutInfo implements Parcelable { mPersons = source.readParcelableArray(cl, Person.class); mLocusId = source.readParcelable(cl); mIconUri = source.readString8(); + mStartingThemeResId = source.readInt(); } @Override @@ -2189,6 +2216,7 @@ public final class ShortcutInfo implements Parcelable { dest.writeParcelableArray(mPersons, flags); dest.writeParcelable(mLocusId, flags); dest.writeString8(mIconUri); + dest.writeInt(mStartingThemeResId); } public static final @NonNull Creator<ShortcutInfo> CREATOR = @@ -2345,6 +2373,12 @@ public final class ShortcutInfo implements Parcelable { sb.append("disabledReason="); sb.append(getDisabledReasonDebugString(mDisabledReason)); + if (mStartingThemeResId != 0) { + addIndentOrComma(sb, indent); + sb.append("SplashScreenThemeResId="); + sb.append(Integer.toHexString(mStartingThemeResId)); + } + addIndentOrComma(sb, indent); sb.append("categories="); @@ -2430,7 +2464,7 @@ public final class ShortcutInfo implements Parcelable { Set<String> categories, Intent[] intentsWithExtras, int rank, PersistableBundle extras, long lastChangedTimestamp, int flags, int iconResId, String iconResName, String bitmapPath, String iconUri, - int disabledReason, Person[] persons, LocusId locusId) { + int disabledReason, Person[] persons, LocusId locusId, int startingThemeResId) { mUserId = userId; mId = id; mPackageName = packageName; @@ -2459,5 +2493,6 @@ public final class ShortcutInfo implements Parcelable { mDisabledReason = disabledReason; mPersons = persons; mLocusId = locusId; + mStartingThemeResId = startingThemeResId; } } diff --git a/core/java/android/content/pm/ShortcutServiceInternal.java b/core/java/android/content/pm/ShortcutServiceInternal.java index c62767ee031b..233abf36131b 100644 --- a/core/java/android/content/pm/ShortcutServiceInternal.java +++ b/core/java/android/content/pm/ShortcutServiceInternal.java @@ -71,6 +71,13 @@ public abstract class ShortcutServiceInternal { public abstract int getShortcutIconResId(int launcherUserId, @NonNull String callingPackage, @NonNull String packageName, @NonNull String shortcutId, int userId); + /** + * Get the theme res ID of the starting window, it can be 0 if not specified. + */ + public abstract int getShortcutStartingThemeResId(int launcherUserId, + @NonNull String callingPackage, @NonNull String packageName, @NonNull String shortcutId, + int userId); + public abstract ParcelFileDescriptor getShortcutIconFd(int launcherUserId, @NonNull String callingPackage, @NonNull String packageName, @NonNull String shortcutId, int userId); diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 6b13a290b20e..228ee3c76b14 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -2836,8 +2836,7 @@ public final class ViewRootImpl implements ViewParent, mScroller.abortAnimation(); } // Our surface is gone - if (mAttachInfo.mThreadedRenderer != null && - mAttachInfo.mThreadedRenderer.isEnabled()) { + if (isHardwareEnabled()) { mAttachInfo.mThreadedRenderer.destroy(); } } else if ((surfaceReplaced @@ -3923,8 +3922,15 @@ public final class ViewRootImpl implements ViewParent, }; } + /** + * @hide + */ + public boolean isHardwareEnabled() { + return mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled(); + } + private boolean addFrameCompleteCallbackIfNeeded() { - if (mAttachInfo.mThreadedRenderer == null || !mAttachInfo.mThreadedRenderer.isEnabled()) { + if (!isHardwareEnabled()) { return false; } @@ -4268,7 +4274,7 @@ public final class ViewRootImpl implements ViewParent, boolean useAsyncReport = false; if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) { - if (mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled()) { + if (isHardwareEnabled()) { // If accessibility focus moved, always invalidate the root. boolean invalidateRoot = accessibilityFocusDirty || mInvalidateRootRequested; mInvalidateRootRequested = false; diff --git a/core/java/android/window/ITaskOrganizer.aidl b/core/java/android/window/ITaskOrganizer.aidl index 88b2257a55b1..8f541d0bd194 100644 --- a/core/java/android/window/ITaskOrganizer.aidl +++ b/core/java/android/window/ITaskOrganizer.aidl @@ -42,6 +42,11 @@ oneway interface ITaskOrganizer { void removeStartingWindow(int taskId); /** + * Called when the Task want to copy the splash screen. + */ + void copySplashScreenView(int taskId); + + /** * A callback when the Task is available for the registered organizer. The client is responsible * for releasing the SurfaceControl in the callback. For non-root tasks, the leash may initially * be hidden so it is up to the organizer to show this task. diff --git a/core/java/android/window/SplashScreen.java b/core/java/android/window/SplashScreen.java new file mode 100644 index 000000000000..4b88a9bc39de --- /dev/null +++ b/core/java/android/window/SplashScreen.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2021 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.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.ActivityThread; +import android.content.Context; +import android.os.IBinder; +import android.util.Singleton; +import android.util.Slog; + +import java.util.ArrayList; + +/** + * The interface that apps use to talk to the splash screen. + * <p> + * Each splash screen instance is bound to a particular {@link Activity}. + * To obtain a {@link SplashScreen} for an Activity, use + * <code>Activity.getSplashScreen()</code> to get the SplashScreen.</p> + */ +public interface SplashScreen { + /** + * <p>Specifies whether an {@link Activity} wants to handle the splash screen animation on its + * own. Normally the splash screen will show on screen before the content of the activity has + * been drawn, and disappear when the activity is showing on the screen. With this listener set, + * the activity will receive {@link OnExitAnimationListener#onSplashScreenExit} callback if + * splash screen is showed, then the activity can create its own exit animation based on the + * SplashScreenView.</p> + * + * <p> Note that this method must be called before splash screen leave, so it only takes effect + * during or before {@link Activity#onResume}.</p> + * + * @param listener the listener for receive the splash screen with + * + * @see OnExitAnimationListener#onSplashScreenExit(SplashScreenView) + */ + @SuppressLint("ExecutorRegistration") + void setOnExitAnimationListener(@Nullable SplashScreen.OnExitAnimationListener listener); + + /** + * Listens for the splash screen exit event. + */ + interface OnExitAnimationListener { + /** + * When receiving this callback, the {@link SplashScreenView} object will be drawing on top + * of the activity. The {@link SplashScreenView} represents the splash screen view + * object, developer can make an exit animation based on this view.</p> + * + * <p>If {@link SplashScreenView#remove} is not called after 5000ms, the method will be + * automatically called and the splash screen removed.</p> + * + * <p>This method is never invoked if your activity sets + * {@link #setOnExitAnimationListener} to <code>null</code>.. + * + * @param view The view object which on top of this Activity. + * @see #setOnExitAnimationListener + */ + void onSplashScreenExit(@NonNull SplashScreenView view); + } + + /** + * @hide + */ + class SplashScreenImpl implements SplashScreen { + private OnExitAnimationListener mExitAnimationListener; + private final IBinder mActivityToken; + private final SplashScreenManagerGlobal mGlobal; + + public SplashScreenImpl(Context context) { + mActivityToken = context.getActivityToken(); + mGlobal = SplashScreenManagerGlobal.getInstance(); + } + + @Override + public void setOnExitAnimationListener( + @Nullable SplashScreen.OnExitAnimationListener listener) { + if (mActivityToken == null) { + // This is not an activity. + return; + } + synchronized (mGlobal.mGlobalLock) { + mExitAnimationListener = listener; + if (listener != null) { + mGlobal.addImpl(this); + } else { + mGlobal.removeImpl(this); + } + } + } + } + + /** + * This class is only used internally to manage the activities for this process. + * + * @hide + */ + class SplashScreenManagerGlobal { + private static final String TAG = SplashScreen.class.getSimpleName(); + private final Object mGlobalLock = new Object(); + private final ArrayList<SplashScreenImpl> mImpls = new ArrayList<>(); + + private SplashScreenManagerGlobal() { + ActivityThread.currentActivityThread().registerSplashScreenManager(this); + } + + public static SplashScreenManagerGlobal getInstance() { + return sInstance.get(); + } + + private static final Singleton<SplashScreenManagerGlobal> sInstance = + new Singleton<SplashScreenManagerGlobal>() { + @Override + protected SplashScreenManagerGlobal create() { + return new SplashScreenManagerGlobal(); + } + }; + + private void addImpl(SplashScreenImpl impl) { + synchronized (mGlobalLock) { + mImpls.add(impl); + } + } + + private void removeImpl(SplashScreenImpl impl) { + synchronized (mGlobalLock) { + mImpls.remove(impl); + } + } + + private SplashScreenImpl findImpl(IBinder token) { + synchronized (mGlobalLock) { + for (SplashScreenImpl impl : mImpls) { + if (impl.mActivityToken == token) { + return impl; + } + } + } + return null; + } + + public void tokenDestroyed(IBinder token) { + synchronized (mGlobalLock) { + final SplashScreenImpl impl = findImpl(token); + if (impl != null) { + removeImpl(impl); + } + } + } + + public void dispatchOnExitAnimation(IBinder token, SplashScreenView view) { + synchronized (mGlobalLock) { + final SplashScreenImpl impl = findImpl(token); + if (impl == null) { + return; + } + if (impl.mExitAnimationListener == null) { + Slog.e(TAG, "cannot dispatch onExitAnimation to listener " + token); + return; + } + impl.mExitAnimationListener.onSplashScreenExit(view); + } + } + + public boolean containsExitListener(IBinder token) { + synchronized (mGlobalLock) { + final SplashScreenImpl impl = findImpl(token); + return impl != null && impl.mExitAnimationListener != null; + } + } + } +} diff --git a/core/java/android/window/SplashScreenView.aidl b/core/java/android/window/SplashScreenView.aidl new file mode 100644 index 000000000000..cc7ac1edce80 --- /dev/null +++ b/core/java/android/window/SplashScreenView.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; + +/** @hide */ +parcelable SplashScreenView.SplashScreenViewParcelable; diff --git a/core/java/android/window/SplashScreenView.java b/core/java/android/window/SplashScreenView.java new file mode 100644 index 000000000000..35ccfca101d3 --- /dev/null +++ b/core/java/android/window/SplashScreenView.java @@ -0,0 +1,510 @@ +/* + * 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 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; +import android.os.Parcelable; +import android.os.SystemClock; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.FrameLayout; + +import com.android.internal.R; +import com.android.internal.policy.DecorView; + +/** + * <p>The view which allows an activity to customize its splash screen exit animation.</p> + * + * <p>Activities will receive this view as a parameter of + * {@link SplashScreen.OnExitAnimationListener#onSplashScreenExit} if + * they set {@link SplashScreen#setOnExitAnimationListener}. + * When this callback is called, this view will be on top of the activity.</p> + * + * <p>This view is composed of a view containing the splashscreen icon (see + * windowSplashscreenAnimatedIcon) and a background. + * Developers can use {@link #getIconView} to get this view and replace the drawable or + * add animation to it. The background of this view is filled with a single color, which can be + * edited during the animation by {@link View#setBackground} or {@link View#setBackgroundColor}.</p> + * + * @see SplashScreen + */ +public final class SplashScreenView extends FrameLayout { + private static final String TAG = SplashScreenView.class.getSimpleName(); + private static final boolean DEBUG = false; + + private boolean mNotCopyable; + private int mInitBackgroundColor; + private View mIconView; + private Bitmap mParceledIconBitmap; + private View mBrandingImageView; + private Bitmap mParceledBrandingBitmap; + private long mIconAnimationDuration; + private long mIconAnimationStart; + + private Animatable mAnimatableIcon; + private ValueAnimator mAnimator; + + // cache original window and status + private Window mWindow; + private boolean mDrawBarBackground; + private int mStatusBarColor; + private int mNavigationBarColor; + + /** + * Internal builder to create a SplashScreenWindowView object. + * @hide + */ + public static class Builder { + private final Context mContext; + private int mIconSize; + private @ColorInt int mBackgroundColor; + private Bitmap mParceledIconBitmap; + private Drawable mIconDrawable; + private int mBrandingImageWidth; + private int mBrandingImageHeight; + private Drawable mBrandingDrawable; + private Bitmap mParceledBrandingBitmap; + private long mIconAnimationStart; + private long mIconAnimationDuration; + + public Builder(@NonNull Context context) { + mContext = context; + } + + /** + * When create from {@link SplashScreenViewParcelable}, all the materials were be settled so + * you do not need to call other set methods. + */ + public Builder createFromParcel(SplashScreenViewParcelable parcelable) { + mIconSize = parcelable.getIconSize(); + mBackgroundColor = parcelable.getBackgroundColor(); + if (parcelable.mIconBitmap != null) { + mIconDrawable = new BitmapDrawable(mContext.getResources(), parcelable.mIconBitmap); + mParceledIconBitmap = parcelable.mIconBitmap; + } + if (parcelable.mBrandingBitmap != null) { + setBrandingDrawable(new BitmapDrawable(mContext.getResources(), + parcelable.mBrandingBitmap), parcelable.mBrandingWidth, + parcelable.mBrandingHeight); + mParceledBrandingBitmap = parcelable.mBrandingBitmap; + } + mIconAnimationStart = parcelable.mIconAnimationStart; + mIconAnimationDuration = parcelable.mIconAnimationDuration; + return this; + } + + /** + * Set the rectangle size for the center view. + */ + public Builder setIconSize(int iconSize) { + mIconSize = iconSize; + return this; + } + + /** + * Set the background color for the view. + */ + public Builder setBackgroundColor(@ColorInt int backgroundColor) { + mBackgroundColor = backgroundColor; + return this; + } + + /** + * Set the Drawable object to fill the center view. + */ + public Builder setCenterViewDrawable(Drawable drawable) { + mIconDrawable = drawable; + return this; + } + + /** + * Set the animation duration if icon is animatable. + */ + public Builder setAnimationDuration(int duration) { + mIconAnimationDuration = duration; + return this; + } + + /** + * Set the Drawable object and size for the branding view. + */ + public Builder setBrandingDrawable(Drawable branding, int width, int height) { + mBrandingDrawable = branding; + mBrandingImageWidth = width; + mBrandingImageHeight = height; + return this; + } + + /** + * Create SplashScreenWindowView object from materials. + */ + public SplashScreenView build() { + final LayoutInflater layoutInflater = LayoutInflater.from(mContext); + final SplashScreenView view = (SplashScreenView) + layoutInflater.inflate(R.layout.splash_screen_view, null, false); + view.mInitBackgroundColor = mBackgroundColor; + view.setBackgroundColor(mBackgroundColor); + view.mIconView = view.findViewById(R.id.splashscreen_icon_view); + view.mBrandingImageView = view.findViewById(R.id.splashscreen_branding_view); + // center icon + if (mIconSize != 0) { + final ViewGroup.LayoutParams params = view.mIconView.getLayoutParams(); + params.width = mIconSize; + params.height = mIconSize; + view.mIconView.setLayoutParams(params); + } + if (mIconDrawable != null) { + view.mIconView.setBackground(mIconDrawable); + view.initIconAnimation(mIconDrawable, mIconAnimationDuration); + } + view.mIconAnimationStart = mIconAnimationStart; + view.mIconAnimationDuration = mIconAnimationDuration; + if (mParceledIconBitmap != null) { + view.mParceledIconBitmap = mParceledIconBitmap; + } + // branding image + if (mBrandingImageHeight > 0 && mBrandingImageWidth > 0) { + final ViewGroup.LayoutParams params = view.mBrandingImageView.getLayoutParams(); + params.width = mBrandingImageWidth; + params.height = mBrandingImageHeight; + view.mBrandingImageView.setLayoutParams(params); + } + if (mBrandingDrawable != null) { + view.mBrandingImageView.setBackground(mBrandingDrawable); + } + if (mParceledBrandingBitmap != null) { + view.mParceledBrandingBitmap = mParceledBrandingBitmap; + } + if (DEBUG) { + Log.d(TAG, " build " + view + " Icon: view: " + view.mIconView + " drawable: " + + mIconDrawable + " size: " + mIconSize + "\n Branding: view: " + + view.mBrandingImageView + " drawable: " + mBrandingDrawable + + " size w: " + mBrandingImageWidth + " h: " + mBrandingImageHeight); + } + return view; + } + } + + /** @hide */ + public SplashScreenView(Context context) { + super(context); + } + + /** @hide */ + public SplashScreenView(Context context, AttributeSet attributeSet) { + super(context, attributeSet); + } + + /** + * Declared this view is not copyable. + * @hide + */ + public void setNotCopyable() { + mNotCopyable = true; + } + + /** + * Whether this view is copyable. + * @hide + */ + public boolean isCopyable() { + return !mNotCopyable; + } + + /** + * Returns the duration of the icon animation if icon is animatable. + * + * @see android.R.attr#windowSplashScreenAnimatedIcon + * @see android.R.attr#windowSplashScreenAnimationDuration + */ + public long getIconAnimationDurationMillis() { + return mIconAnimationDuration; + } + + /** + * If the replaced icon is animatable, return the animation start time in millisecond based on + * system. The start time is set using {@link SystemClock#uptimeMillis()}. + */ + public long getIconAnimationStartMillis() { + return mIconAnimationStart; + } + + void initIconAnimation(Drawable iconDrawable, long 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) { + mIconAnimationStart = SystemClock.uptimeMillis(); + mAnimatableIcon.start(); + } + + @Override + public void onAnimationEnd(Animator animation) { + mAnimatableIcon.stop(); + } + + @Override + public void onAnimationCancel(Animator animation) { + mAnimatableIcon.stop(); + } + + @Override + public void onAnimationRepeat(Animator animation) { + // do not repeat + mAnimatableIcon.stop(); + } + }); + } + } + + /** + * @hide + */ + public void startIntroAnimation() { + if (mAnimatableIcon != null) { + mAnimator.start(); + } + } + + /** + * <p>Remove this view and release its resource. </p> + * <p><strong>Do not</strong> invoke this method from a drawing method + * ({@link #onDraw(android.graphics.Canvas)} for instance).</p> + */ + public void remove() { + setVisibility(GONE); + if (mParceledIconBitmap != null) { + mIconView.setBackground(null); + mParceledIconBitmap.recycle(); + mParceledIconBitmap = null; + } + if (mParceledBrandingBitmap != null) { + mBrandingImageView.setBackground(null); + mParceledBrandingBitmap.recycle(); + mParceledBrandingBitmap = null; + } + if (mWindow != null) { + final DecorView decorView = (DecorView) mWindow.peekDecorView(); + if (DEBUG) { + Log.d(TAG, "remove starting view"); + } + if (decorView != null) { + decorView.removeView(this); + } + restoreSystemUIColors(); + mWindow = null; + } + } + + /** + * Cache the root window. + * @hide + */ + public void cacheRootWindow(Window window) { + mWindow = window; + } + + /** + * Called after SplashScreenView has added on the root window. + * @hide + */ + public void makeSystemUIColorsTransparent() { + if (mWindow != null) { + final WindowManager.LayoutParams attr = mWindow.getAttributes(); + mDrawBarBackground = (attr.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0; + mWindow.addFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + mStatusBarColor = mWindow.getStatusBarColor(); + mNavigationBarColor = mWindow.getNavigationBarDividerColor(); + mWindow.setStatusBarColor(Color.TRANSPARENT); + mWindow.setNavigationBarColor(Color.TRANSPARENT); + } + setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + } + + private void restoreSystemUIColors() { + if (mWindow != null) { + if (!mDrawBarBackground) { + mWindow.clearFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + } + mWindow.setStatusBarColor(mStatusBarColor); + mWindow.setNavigationBarColor(mNavigationBarColor); + } + } + + /** + * Get the view containing the Splash Screen icon and its background. + * @see android.R.attr#windowSplashScreenAnimatedIcon + */ + public @Nullable View getIconView() { + return mIconView; + } + + /** + * Get the branding image view. + * @hide + */ + @TestApi + public @Nullable View getBrandingView() { + return mBrandingImageView; + } + + /** + * Get the initial background color of this view. + * @hide + */ + @ColorInt int getInitBackgroundColor() { + return mInitBackgroundColor; + } + + /** + * Use to create {@link SplashScreenView} object across process. + * @hide + */ + public static class SplashScreenViewParcelable implements Parcelable { + private int mIconSize; + private int mBackgroundColor; + + private Bitmap mIconBitmap; + private int mBrandingWidth; + private int mBrandingHeight; + private Bitmap mBrandingBitmap; + + private long mIconAnimationStart; + private long mIconAnimationDuration; + + public SplashScreenViewParcelable(SplashScreenView view) { + ViewGroup.LayoutParams params = view.getIconView().getLayoutParams(); + mIconSize = params.height; + mBackgroundColor = view.getInitBackgroundColor(); + + mIconBitmap = copyDrawable(view.getIconView().getBackground()); + mBrandingBitmap = copyDrawable(view.getBrandingView().getBackground()); + params = view.getBrandingView().getLayoutParams(); + mBrandingWidth = params.width; + mBrandingHeight = params.height; + + mIconAnimationStart = view.getIconAnimationStartMillis(); + mIconAnimationDuration = view.getIconAnimationDurationMillis(); + } + + private Bitmap copyDrawable(Drawable drawable) { + if (drawable != null) { + final Rect initialBounds = drawable.copyBounds(); + final int width = initialBounds.width(); + final int height = initialBounds.height(); + + final Bitmap snapshot = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + final Canvas bmpCanvas = new Canvas(snapshot); + drawable.setBounds(0, 0, width, height); + drawable.draw(bmpCanvas); + final Bitmap copyBitmap = snapshot.createAshmemBitmap(); + snapshot.recycle(); + return copyBitmap; + } + return null; + } + + private SplashScreenViewParcelable(@NonNull Parcel source) { + readParcel(source); + } + + private void readParcel(@NonNull Parcel source) { + mIconSize = source.readInt(); + mBackgroundColor = source.readInt(); + mIconBitmap = source.readTypedObject(Bitmap.CREATOR); + mBrandingWidth = source.readInt(); + mBrandingHeight = source.readInt(); + mBrandingBitmap = source.readTypedObject(Bitmap.CREATOR); + mIconAnimationStart = source.readLong(); + mIconAnimationDuration = source.readLong(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mIconSize); + dest.writeInt(mBackgroundColor); + dest.writeTypedObject(mIconBitmap, flags); + dest.writeInt(mBrandingWidth); + dest.writeInt(mBrandingHeight); + dest.writeTypedObject(mBrandingBitmap, flags); + dest.writeLong(mIconAnimationStart); + dest.writeLong(mIconAnimationDuration); + } + + public static final @NonNull Parcelable.Creator<SplashScreenViewParcelable> CREATOR = + new Parcelable.Creator<SplashScreenViewParcelable>() { + public SplashScreenViewParcelable createFromParcel(@NonNull Parcel source) { + return new SplashScreenViewParcelable(source); + } + public SplashScreenViewParcelable[] newArray(int size) { + return new SplashScreenViewParcelable[size]; + } + }; + + /** + * Release the bitmap if another process cannot handle it. + */ + public void clearIfNeeded() { + if (mIconBitmap != null) { + mIconBitmap.recycle(); + mIconBitmap = null; + } + if (mBrandingBitmap != null) { + mBrandingBitmap.recycle(); + mBrandingBitmap = null; + } + } + + int getIconSize() { + return mIconSize; + } + + int getBackgroundColor() { + return mBackgroundColor; + } + } +} diff --git a/core/java/android/window/StartingWindowInfo.java b/core/java/android/window/StartingWindowInfo.java index 2282cc567936..63b9e9befb77 100644 --- a/core/java/android/window/StartingWindowInfo.java +++ b/core/java/android/window/StartingWindowInfo.java @@ -95,6 +95,12 @@ public final class StartingWindowInfo implements Parcelable { */ public int startingWindowTypeParameter; + /** + * Specifies a theme for the splash screen. + * @hide + */ + public int splashScreenThemeResId; + public StartingWindowInfo() { } @@ -115,6 +121,7 @@ public final class StartingWindowInfo implements Parcelable { dest.writeTypedObject(topOpaqueWindowInsetsState, flags); dest.writeTypedObject(topOpaqueWindowLayoutParams, flags); dest.writeTypedObject(mainWindowLayoutParams, flags); + dest.writeInt(splashScreenThemeResId); } void readFromParcel(@NonNull Parcel source) { @@ -124,6 +131,7 @@ public final class StartingWindowInfo implements Parcelable { topOpaqueWindowLayoutParams = source.readTypedObject( WindowManager.LayoutParams.CREATOR); mainWindowLayoutParams = source.readTypedObject(WindowManager.LayoutParams.CREATOR); + splashScreenThemeResId = source.readInt(); } @Override @@ -135,7 +143,8 @@ public final class StartingWindowInfo implements Parcelable { + Integer.toHexString(startingWindowTypeParameter) + " insetsState=" + topOpaqueWindowInsetsState + " topWindowLayoutParams=" + topOpaqueWindowLayoutParams - + " mainWindowLayoutParams=" + mainWindowLayoutParams; + + " mainWindowLayoutParams=" + mainWindowLayoutParams + + " splashScreenThemeResId " + Integer.toHexString(splashScreenThemeResId); } public static final @android.annotation.NonNull Creator<StartingWindowInfo> CREATOR = diff --git a/core/java/android/window/TaskOrganizer.java b/core/java/android/window/TaskOrganizer.java index cdb4762a4f0a..217ade82b336 100644 --- a/core/java/android/window/TaskOrganizer.java +++ b/core/java/android/window/TaskOrganizer.java @@ -105,6 +105,12 @@ public class TaskOrganizer extends WindowOrganizer { public void removeStartingWindow(int taskId) {} /** + * Called when the Task want to copy the splash screen. + */ + @BinderThread + public void copySplashScreenView(int taskId) {} + + /** * Called when a task with the registered windowing mode can be controlled by this task * organizer. For non-root tasks, the leash may initially be hidden so it is up to the organizer * to show this task. @@ -223,6 +229,11 @@ public class TaskOrganizer extends WindowOrganizer { } @Override + public void copySplashScreenView(int taskId) { + mExecutor.execute(() -> TaskOrganizer.this.copySplashScreenView(taskId)); + } + + @Override public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { mExecutor.execute(() -> TaskOrganizer.this.onTaskAppeared(taskInfo, leash)); } diff --git a/core/res/res/layout/splash_screen_view.xml b/core/res/res/layout/splash_screen_view.xml new file mode 100644 index 000000000000..513da5e431e5 --- /dev/null +++ b/core/res/res/layout/splash_screen_view.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<android.window.SplashScreenView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_height="match_parent" + android:layout_width="match_parent" + android:orientation="vertical"> + + <View android:id="@+id/splashscreen_icon_view" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:layout_gravity="center"/> + + <View android:id="@+id/splashscreen_branding_view" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:layout_gravity="center_horizontal|bottom" + android:layout_marginBottom="60dp"/> + +</android.window.SplashScreenView>
\ No newline at end of file diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 823e10edaad5..927a8b49c17e 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -2262,6 +2262,23 @@ --> <enum name="always" value="3" /> </attr> + + <!-- The background color for the splash screen, if not specify then system will + calculate from windowBackground. --> + <attr name="windowSplashScreenBackground" format="color"/> + + <!-- Replace an icon in the center of the starting window, if the object is animated + and drawable(e.g. AnimationDrawable, AnimatedVectorDrawable), then it will also + play the animation while showing the starting window. --> + <attr name="windowSplashScreenAnimatedIcon" format="reference"/> + <!-- The duration, in milliseconds, of the window splash screen icon animation duration + when playing the splash screen starting window. The maximum animation duration should + be limited below 1000ms. --> + <attr name="windowSplashScreenAnimationDuration" format="integer"/> + + <!-- Place an drawable image in the bottom of the starting window, it can be used to + represent the branding of the application. --> + <attr name="windowSplashScreenBrandingImage" format="reference"/> </declare-styleable> <!-- The set of attributes that describe a AlertDialog's theme. --> @@ -9260,6 +9277,7 @@ <attr name="shortcutShortLabel" format="reference" /> <attr name="shortcutLongLabel" format="reference" /> <attr name="shortcutDisabledMessage" format="reference" /> + <attr name="splashScreenTheme" format="reference"/> </declare-styleable> <declare-styleable name="ShortcutCategories"> diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index 3c97aec5d55d..278708a26d44 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -3066,6 +3066,11 @@ <public name="edgeEffectType" /> <public name="knownCerts" /> <public name="windowBackgroundBlurRadius"/> + <public name="windowSplashScreenBackground"/> + <public name="windowSplashScreenAnimatedIcon"/> + <public name="windowSplashScreenAnimationDuration"/> + <public name="windowSplashScreenBrandingImage"/> + <public name="splashScreenTheme" /> </public-group> <public-group type="drawable" first-id="0x010800b5"> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 4109d4c9f6f9..f66d33b02b69 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -1818,6 +1818,9 @@ <java-symbol type="string" name="forward_intent_to_owner" /> <java-symbol type="string" name="forward_intent_to_work" /> <java-symbol type="dimen" name="cross_profile_apps_thumbnail_size" /> + <java-symbol type="layout" name="splash_screen_view" /> + <java-symbol type="id" name="splashscreen_icon_view" /> + <java-symbol type="id" name="splashscreen_branding_view" /> <!-- From services --> <java-symbol type="anim" name="screen_rotate_0_enter" /> diff --git a/core/res/res/values/themes.xml b/core/res/res/values/themes.xml index 049ba23a1a97..87ae1623ca92 100644 --- a/core/res/res/values/themes.xml +++ b/core/res/res/values/themes.xml @@ -167,6 +167,9 @@ please see themes_device_defaults.xml. <!-- Window attributes --> <item name="windowBackground">@drawable/screen_background_selector_dark</item> <item name="windowBackgroundFallback">?attr/colorBackground</item> + <item name="windowSplashScreenBackground">@color/transparent</item> + <item name="windowSplashScreenAnimatedIcon">@null</item> + <item name="windowSplashScreenBrandingImage">@null</item> <item name="windowClipToOutline">false</item> <item name="windowFrame">@null</item> <item name="windowNoTitle">false</item> diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 69c30f3ca05c..ded4a276880f 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -889,12 +889,6 @@ "group": "WM_SHOW_TRANSACTIONS", "at": "com\/android\/server\/wm\/WindowSurfaceController.java" }, - "-1088782910": { - "message": "Translucent=%s Floating=%s ShowWallpaper=%s", - "level": "VERBOSE", - "group": "WM_DEBUG_STARTING_WINDOW", - "at": "com\/android\/server\/wm\/ActivityRecord.java" - }, "-1076978367": { "message": "thawRotation: mRotation=%d", "level": "VERBOSE", @@ -1675,6 +1669,12 @@ "group": "WM_DEBUG_STATES", "at": "com\/android\/server\/wm\/Task.java" }, + "-124316973": { + "message": "Translucent=%s Floating=%s ShowWallpaper=%s Disable=%s", + "level": "VERBOSE", + "group": "WM_DEBUG_STARTING_WINDOW", + "at": "com\/android\/server\/wm\/ActivityRecord.java" + }, "-118786523": { "message": "Resume failed; resetting state to %s: %s", "level": "VERBOSE", diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index 13f1fddfdfb6..24198659e15d 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -48,4 +48,7 @@ <!-- one handed background panel default alpha --> <item name="config_one_handed_background_alpha" format="float" type="dimen">0.5</item> + + <!-- maximum animation duration for the icon when entering the starting window --> + <integer name="max_starting_window_intro_icon_anim_duration">1000</integer> </resources> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 034e65c608a3..cb2aada2a9cf 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -173,4 +173,10 @@ <!-- The width/height of the icon view on staring surface. --> <dimen name="starting_surface_icon_size">108dp</dimen> + + <!-- The width of the brand image on staring surface. --> + <dimen name="starting_surface_brand_image_width">200dp</dimen> + + <!-- The height of the brand image on staring surface. --> + <dimen name="starting_surface_brand_image_height">80dp</dimen> </resources> 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 b22f358c0781..03724171da21 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 @@ -254,6 +253,11 @@ public class ShellTaskOrganizer extends TaskOrganizer { } @Override + public void copySplashScreenView(int taskId) { + mStartingSurfaceDrawer.copySplashScreenView(taskId); + } + + @Override public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { synchronized (mLock) { onTaskAppeared(new TaskAppearedInfo(taskInfo, leash)); 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 f3f2fc3686b6..45d551528940 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 @@ -31,23 +31,21 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.os.Build; import android.util.Slog; -import android.view.Gravity; -import android.view.View; -import android.view.WindowManager; -import android.widget.FrameLayout; +import android.view.Window; +import android.window.SplashScreenView; import com.android.internal.R; import com.android.internal.graphics.palette.Palette; import com.android.internal.graphics.palette.Quantizer; import com.android.internal.graphics.palette.VariationalKMeansQuantizer; -import com.android.internal.policy.PhoneWindow; 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; @@ -58,15 +56,24 @@ 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; + private int mBrandingImageWidth; + private int mBrandingImageHeight; - SplashscreenContentDrawer(Context context) { + SplashscreenContentDrawer(Context context, int maxIconAnimationDuration) { mContext = context; + mMaxIconAnimationDuration = maxIconAnimationDuration; } private void updateDensity() { mIconSize = mContext.getResources().getDimensionPixelSize( com.android.wm.shell.R.dimen.starting_surface_icon_size); + mBrandingImageWidth = mContext.getResources().getDimensionPixelSize( + com.android.wm.shell.R.dimen.starting_surface_brand_image_width); + mBrandingImageHeight = mContext.getResources().getDimensionPixelSize( + com.android.wm.shell.R.dimen.starting_surface_brand_image_height); } private int getSystemBGColor() { @@ -83,48 +90,92 @@ class SplashscreenContentDrawer { return new ColorDrawable(getSystemBGColor()); } - View makeSplashScreenContentView(PhoneWindow win, Context context, int iconRes, + SplashScreenView makeSplashScreenContentView(Window win, Context context, int iconRes, int splashscreenContentResId) { updateDensity(); - win.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); // splash screen content will be deprecated after S. - final View ssc = makeSplashscreenContentDrawable(win, context, splashscreenContentResId); + final SplashScreenView ssc = + makeSplashscreenContentDrawable(win, context, splashscreenContentResId); if (ssc != null) { 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(); + } + final int animationDuration; + final Drawable iconDrawable; + if (attrs.mReplaceIcon != null) { + iconDrawable = attrs.mReplaceIcon; + animationDuration = Math.max(0, + Math.min(attrs.mAnimationDuration, mMaxIconAnimationDuration)); } else { - themeBGDrawable = context.getDrawable(resId); + iconDrawable = iconRes != 0 ? context.getDrawable(iconRes) + : context.getPackageManager().getDefaultActivityIcon(); + animationDuration = 0; } - final Drawable iconDrawable = iconRes != 0 ? context.getDrawable(iconRes) - : context.getPackageManager().getDefaultActivityIcon(); // TODO (b/173975965) Tracking the performance on improved splash screen. - final StartingWindowViewBuilder builder = new StartingWindowViewBuilder(); return builder - .setPhoneWindow(win) + .setWindow(win) .setContext(context) .setThemeDrawable(themeBGDrawable) - .setIconDrawable(iconDrawable).build(); + .setIconDrawable(iconDrawable) + .setIconAnimationDuration(animationDuration) + .setBrandingDrawable(attrs.mBrandingImage).build(); + } + + private static class SplashScreenWindowAttrs { + private int mWindowBgResId = 0; + private int mWindowBgColor = Color.TRANSPARENT; + private Drawable mReplaceIcon = null; + private Drawable mBrandingImage = 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); + attrs.mBrandingImage = typedArray.getDrawable( + R.styleable.Window_windowSplashScreenBrandingImage); + typedArray.recycle(); + if (DEBUG) { + Slog.d(TAG, "window attributes color: " + + Integer.toHexString(attrs.mWindowBgColor) + + " icon " + attrs.mReplaceIcon + " duration " + attrs.mAnimationDuration + + " brandImage " + attrs.mBrandingImage); + } + return attrs; + } } private class StartingWindowViewBuilder { - // materials private Drawable mThemeBGDrawable; private Drawable mIconDrawable; - private PhoneWindow mPhoneWindow; + private Window mWindow; + private int mIconAnimationDuration; private Context mContext; + private Drawable mBrandingDrawable; // result private boolean mBuildComplete = false; - private View mCachedResult; + private SplashScreenView mCachedResult; private int mThemeColor; private Drawable mFinalIconDrawable; private float mScale = 1f; @@ -141,8 +192,20 @@ class SplashscreenContentDrawer { return this; } - StartingWindowViewBuilder setPhoneWindow(PhoneWindow window) { - mPhoneWindow = window; + StartingWindowViewBuilder setWindow(Window window) { + mWindow = window; + mBuildComplete = false; + return this; + } + + StartingWindowViewBuilder setIconAnimationDuration(int iconAnimationDuration) { + mIconAnimationDuration = iconAnimationDuration; + mBuildComplete = false; + return this; + } + + StartingWindowViewBuilder setBrandingDrawable(Drawable branding) { + mBrandingDrawable = branding; mBuildComplete = false; return this; } @@ -153,11 +216,11 @@ class SplashscreenContentDrawer { return this; } - View build() { + SplashScreenView build() { if (mBuildComplete) { return mCachedResult; } - if (mPhoneWindow == null || mContext == null) { + if (mWindow == null || mContext == null) { Slog.e(TAG, "Unable to create StartingWindowView, lack of materials!"); return null; } @@ -173,7 +236,7 @@ class SplashscreenContentDrawer { mFinalIconDrawable = mIconDrawable; } final int iconSize = mFinalIconDrawable != null ? (int) (mIconSize * mScale) : 0; - mCachedResult = fillViewWithIcon(mPhoneWindow, mContext, iconSize, mFinalIconDrawable); + mCachedResult = fillViewWithIcon(mWindow, mContext, iconSize, mFinalIconDrawable); mBuildComplete = true; return mCachedResult; } @@ -249,25 +312,26 @@ class SplashscreenContentDrawer { return true; } - private View fillViewWithIcon(PhoneWindow win, Context context, + private SplashScreenView fillViewWithIcon(Window win, Context context, int iconSize, Drawable iconDrawable) { - final StartingSurfaceWindowView surfaceWindowView = - new StartingSurfaceWindowView(context, iconSize); - surfaceWindowView.setBackground(new ColorDrawable(mThemeColor)); + final SplashScreenView.Builder builder = new SplashScreenView.Builder(context); + builder.setIconSize(iconSize).setBackgroundColor(mThemeColor); if (iconDrawable != null) { - surfaceWindowView.setIconDrawable(iconDrawable); + builder.setCenterViewDrawable(iconDrawable); } + builder.setAnimationDuration(mIconAnimationDuration); + if (mBrandingDrawable != null) { + builder.setBrandingDrawable(mBrandingDrawable, mBrandingImageWidth, + mBrandingImageHeight); + } + final SplashScreenView splashScreenView = builder.build(); if (DEBUG) { - Slog.d(TAG, "fillViewWithIcon surfaceWindowView " + surfaceWindowView); + Slog.d(TAG, "fillViewWithIcon surfaceWindowView " + splashScreenView); } - win.setContentView(surfaceWindowView); - makeSystemUIColorsTransparent(win); - return surfaceWindowView; - } - - private void makeSystemUIColorsTransparent(PhoneWindow win) { - win.setStatusBarColor(Color.TRANSPARENT); - win.setNavigationBarColor(Color.TRANSPARENT); + win.setContentView(splashScreenView); + splashScreenView.cacheRootWindow(win); + splashScreenView.makeSystemUIColorsTransparent(); + return splashScreenView; } } @@ -298,8 +362,8 @@ class SplashscreenContentDrawer { return root < 0.1; } - private static View makeSplashscreenContentDrawable(PhoneWindow win, Context ctx, - int splashscreenContentResId) { + private static SplashScreenView makeSplashscreenContentDrawable(Window win, + Context ctx, int splashscreenContentResId) { // doesn't support windowSplashscreenContent after S // TODO add an allowlist to skip some packages if needed final int targetSdkVersion = ctx.getApplicationInfo().targetSdkVersion; @@ -316,7 +380,8 @@ class SplashscreenContentDrawer { if (drawable == null) { return null; } - View view = new View(ctx); + SplashScreenView view = new SplashScreenView(ctx); + view.setNotCopyable(); view.setBackground(drawable); win.setContentView(view); return view; @@ -532,34 +597,4 @@ class SplashscreenContentDrawer { } } } - - private static class StartingSurfaceWindowView extends FrameLayout { - // TODO animate the icon view - private final View mIconView; - - StartingSurfaceWindowView(Context context, int iconSize) { - super(context); - - final boolean emptyIcon = iconSize == 0; - if (emptyIcon) { - mIconView = null; - } else { - mIconView = new View(context); - FrameLayout.LayoutParams params = - new FrameLayout.LayoutParams(iconSize, iconSize); - params.gravity = Gravity.CENTER; - addView(mIconView, params); - } - setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); - } - - // TODO support animatable icon - void setIconDrawable(Drawable icon) { - if (mIconView != null) { - mIconView.setBackground(icon); - } - } - } } 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 f3749220d4e1..5332291fd1bd 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 @@ -43,6 +43,8 @@ import android.util.SparseArray; import android.view.Display; import android.view.View; import android.view.WindowManager; +import android.window.SplashScreenView; +import android.window.SplashScreenView.SplashScreenViewParcelable; import android.window.StartingWindowInfo; import android.window.TaskOrganizer; import android.window.TaskSnapshot; @@ -63,7 +65,6 @@ import java.util.function.Consumer; * class to remove the starting window of the Task. * @hide */ - public class StartingSurfaceDrawer { static final String TAG = StartingSurfaceDrawer.class.getSimpleName(); static final boolean DEBUG_SPLASH_SCREEN = false; @@ -81,7 +82,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<StartingWindowRecord> mStartingWindowRecords = new SparseArray<>(); @@ -193,9 +197,8 @@ public class StartingSurfaceDrawer { public void addStartingWindow(StartingWindowInfo windowInfo, IBinder appToken) { final PreferredStartingTypeHelper helper = new PreferredStartingTypeHelper(windowInfo); - final RunningTaskInfo runningTaskInfo = windowInfo.taskInfo; if (helper.mPreferredType == PreferredStartingTypeHelper.STARTING_TYPE_SPLASH_SCREEN) { - addSplashScreenStartingWindow(runningTaskInfo, appToken); + addSplashScreenStartingWindow(windowInfo, appToken); } else if (helper.mPreferredType == PreferredStartingTypeHelper.STARTING_TYPE_SNAPSHOT) { final TaskSnapshot snapshot = helper.mSnapshot; makeTaskSnapshotWindow(windowInfo, appToken, snapshot); @@ -203,11 +206,13 @@ public class StartingSurfaceDrawer { // If prefer don't show, then don't show! } - private void addSplashScreenStartingWindow(RunningTaskInfo taskInfo, IBinder appToken) { + private void addSplashScreenStartingWindow(StartingWindowInfo windowInfo, IBinder appToken) { + final RunningTaskInfo taskInfo = windowInfo.taskInfo; final ActivityInfo activityInfo = taskInfo.topActivityInfo; if (activityInfo == null) { return; } + final int displayId = taskInfo.displayId; if (activityInfo.packageName == null) { return; @@ -222,11 +227,11 @@ public class StartingSurfaceDrawer { } Context context = mContext; - int theme = activityInfo.getThemeResource(); - if (theme == 0) { - // replace with the default theme if the application didn't set - theme = com.android.internal.R.style.Theme_DeviceDefault_DayNight; - } + // replace with the default theme if the application didn't set + final int theme = windowInfo.splashScreenThemeResId != 0 + ? windowInfo.splashScreenThemeResId + : activityInfo.getThemeResource() != 0 ? activityInfo.getThemeResource() + : com.android.internal.R.style.Theme_DeviceDefault_DayNight; if (DEBUG_SPLASH_SCREEN) { Slog.d(TAG, "addSplashScreen " + activityInfo.packageName + ": nonLocalizedLabel=" + nonLocalizedLabel + " theme=" @@ -352,9 +357,10 @@ public class StartingSurfaceDrawer { } params.setTitle("Splash Screen " + activityInfo.packageName); - final View contentView = mSplashscreenContentDrawer.makeSplashScreenContentView(win, - context, iconRes, splashscreenContentResId[0]); - if (contentView == null) { + final SplashScreenView splashScreenView = + mSplashscreenContentDrawer.makeSplashScreenContentView(win, context, iconRes, + splashscreenContentResId[0]); + if (splashScreenView == null) { Slog.w(TAG, "Adding splash screen window for " + activityInfo.packageName + " failed!"); return; } @@ -366,7 +372,7 @@ public class StartingSurfaceDrawer { + activityInfo.packageName + " / " + appToken + ": " + view); } final WindowManager wm = context.getSystemService(WindowManager.class); - postAddWindow(taskInfo.taskId, appToken, view, wm, params); + postAddWindow(taskInfo.taskId, appToken, view, wm, params, splashScreenView); } /** @@ -379,7 +385,7 @@ public class StartingSurfaceDrawer { snapshot, mMainExecutor, () -> removeWindowSynced(taskId) /* clearWindow */); mMainExecutor.executeDelayed(() -> removeWindowSynced(taskId), REMOVE_WHEN_TIMEOUT); final StartingWindowRecord tView = - new StartingWindowRecord(null/* decorView */, surface); + new StartingWindowRecord(null/* decorView */, surface, null /* splashScreenView */); mStartingWindowRecords.put(taskId, tView); } @@ -393,37 +399,60 @@ public class StartingSurfaceDrawer { removeWindowSynced(taskId); } + /** + * Called when the Task wants to copy the splash screen. + * @param taskId + */ + public void copySplashScreenView(int taskId) { + final StartingWindowRecord preView = mStartingWindowRecords.get(taskId); + SplashScreenViewParcelable parcelable; + if (preView != null && preView.mContentView != null + && preView.mContentView.isCopyable()) { + parcelable = new SplashScreenViewParcelable(preView.mContentView); + } else { + parcelable = null; + } + if (DEBUG_SPLASH_SCREEN) { + Slog.v(TAG, "Copying splash screen window view for task: " + taskId + + " parcelable? " + parcelable); + } + ActivityTaskManager.getInstance().onSplashScreenViewCopyFinished(taskId, parcelable); + } + protected void postAddWindow(int taskId, IBinder appToken, - View view, WindowManager wm, WindowManager.LayoutParams params) { - boolean shouldSaveView = true; - try { - wm.addView(view, params); - } catch (WindowManager.BadTokenException e) { - // ignore - Slog.w(TAG, appToken + " already running, starting window not displayed. " - + e.getMessage()); - shouldSaveView = false; - } catch (RuntimeException e) { - // don't crash if something else bad happens, for example a - // failure loading resources because we are loading from an app - // on external storage that has been unmounted. - Slog.w(TAG, appToken + " failed creating starting window", e); - shouldSaveView = false; - } finally { - if (view != null && view.getParent() == null) { - Slog.w(TAG, "view not successfully added to wm, removing view"); - wm.removeViewImmediate(view); + View view, WindowManager wm, WindowManager.LayoutParams params, + SplashScreenView splashScreenView) { + mMainExecutor.execute(() -> { + boolean shouldSaveView = true; + try { + wm.addView(view, params); + } catch (WindowManager.BadTokenException e) { + // ignore + Slog.w(TAG, appToken + " already running, starting window not displayed. " + + e.getMessage()); shouldSaveView = false; + } catch (RuntimeException e) { + // don't crash if something else bad happens, for example a + // failure loading resources because we are loading from an app + // on external storage that has been unmounted. + Slog.w(TAG, appToken + " failed creating starting window", e); + shouldSaveView = false; + } finally { + if (view != null && view.getParent() == null) { + Slog.w(TAG, "view not successfully added to wm, removing view"); + wm.removeViewImmediate(view); + shouldSaveView = false; + } } - } - - if (shouldSaveView) { - removeWindowSynced(taskId); - mMainExecutor.executeDelayed(() -> removeWindowSynced(taskId), REMOVE_WHEN_TIMEOUT); - final StartingWindowRecord tView = - new StartingWindowRecord(view, null /* TaskSnapshotWindow */); - mStartingWindowRecords.put(taskId, tView); - } + if (shouldSaveView) { + removeWindowSynced(taskId); + mMainExecutor.executeDelayed(() -> removeWindowSynced(taskId), REMOVE_WHEN_TIMEOUT); + final StartingWindowRecord tView = new StartingWindowRecord(view, + null /* TaskSnapshotWindow */, splashScreenView); + splashScreenView.startIntroAnimation(); + mStartingWindowRecords.put(taskId, tView); + } + }); } protected void removeWindowSynced(int taskId) { @@ -459,10 +488,13 @@ public class StartingSurfaceDrawer { private static class StartingWindowRecord { private final View mDecorView; private final TaskSnapshotWindow mTaskSnapshotWindow; + private final SplashScreenView mContentView; - StartingWindowRecord(View decorView, TaskSnapshotWindow taskSnapshotWindow) { + StartingWindowRecord(View decorView, TaskSnapshotWindow taskSnapshotWindow, + SplashScreenView splashScreenView) { mDecorView = decorView; mTaskSnapshotWindow = taskSnapshotWindow; + mContentView = splashScreenView; } } } 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 176b33dda020..c815e65189e6 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 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 c9537afa37ef..de7d6c74bb06 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 @@ -40,6 +40,7 @@ import android.testing.TestableContext; import android.view.View; import android.view.WindowManager; import android.view.WindowMetrics; +import android.window.SplashScreenView; import android.window.StartingWindowInfo; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -79,7 +80,8 @@ public class StartingSurfaceDrawerTests { @Override protected void postAddWindow(int taskId, IBinder appToken, - View view, WindowManager wm, WindowManager.LayoutParams params) { + View view, WindowManager wm, WindowManager.LayoutParams params, + SplashScreenView splashScreenView) { // listen for addView mAddWindowForTask = taskId; mViewThemeResId = view.getContext().getThemeResId(); @@ -125,7 +127,8 @@ public class StartingSurfaceDrawerTests { createWindowInfo(taskId, android.R.style.Theme); mStartingSurfaceDrawer.addStartingWindow(windowInfo, mBinder); waitHandlerIdle(mainLoop); - verify(mStartingSurfaceDrawer).postAddWindow(eq(taskId), eq(mBinder), any(), any(), any()); + verify(mStartingSurfaceDrawer).postAddWindow( + eq(taskId), eq(mBinder), any(), any(), any(), any()); assertEquals(mStartingSurfaceDrawer.mAddWindowForTask, taskId); mStartingSurfaceDrawer.removeStartingWindow(windowInfo.taskInfo.taskId); @@ -142,7 +145,8 @@ public class StartingSurfaceDrawerTests { createWindowInfo(taskId, 0); mStartingSurfaceDrawer.addStartingWindow(windowInfo, mBinder); waitHandlerIdle(mainLoop); - verify(mStartingSurfaceDrawer).postAddWindow(eq(taskId), eq(mBinder), any(), any(), any()); + verify(mStartingSurfaceDrawer).postAddWindow( + eq(taskId), eq(mBinder), any(), any(), any(), any()); assertNotEquals(mStartingSurfaceDrawer.mViewThemeResId, 0); } diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java index 7f638b9a55a2..915517a4b9ce 100644 --- a/services/core/java/com/android/server/UiModeManagerService.java +++ b/services/core/java/com/android/server/UiModeManagerService.java @@ -19,6 +19,7 @@ package com.android.server; import static android.app.UiModeManager.DEFAULT_PRIORITY; import static android.app.UiModeManager.MODE_NIGHT_AUTO; import static android.app.UiModeManager.MODE_NIGHT_CUSTOM; +import static android.app.UiModeManager.MODE_NIGHT_NO; import static android.app.UiModeManager.MODE_NIGHT_YES; import static android.app.UiModeManager.PROJECTION_TYPE_AUTOMOTIVE; import static android.app.UiModeManager.PROJECTION_TYPE_NONE; @@ -82,6 +83,7 @@ import com.android.internal.util.DumpUtils; import com.android.server.twilight.TwilightListener; import com.android.server.twilight.TwilightManager; import com.android.server.twilight.TwilightState; +import com.android.server.wm.ActivityTaskManagerInternal; import com.android.server.wm.WindowManagerInternal; import java.io.FileDescriptor; @@ -158,6 +160,7 @@ final class UiModeManagerService extends SystemService { private NotificationManager mNotificationManager; private StatusBarManager mStatusBarManager; private WindowManagerInternal mWindowManager; + private ActivityTaskManagerInternal mActivityTaskManager; private AlarmManager mAlarmManager; private PowerManager mPowerManager; @@ -366,6 +369,7 @@ final class UiModeManagerService extends SystemService { mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); mWakeLock = mPowerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, TAG); mWindowManager = LocalServices.getService(WindowManagerInternal.class); + mActivityTaskManager = LocalServices.getService(ActivityTaskManagerInternal.class); mAlarmManager = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE); TwilightManager twilightManager = getLocalService(TwilightManager.class); if (twilightManager != null) mTwilightManager = twilightManager; @@ -750,6 +754,39 @@ final class UiModeManagerService extends SystemService { } @Override + public void setApplicationNightMode(@UiModeManager.NightMode int mode) + throws RemoteException { + switch (mode) { + case UiModeManager.MODE_NIGHT_NO: + case UiModeManager.MODE_NIGHT_YES: + case UiModeManager.MODE_NIGHT_AUTO: + case UiModeManager.MODE_NIGHT_CUSTOM: + break; + default: + throw new IllegalArgumentException("Unknown mode: " + mode); + } + final int configNightMode; + switch (mode) { + case MODE_NIGHT_YES: + configNightMode = Configuration.UI_MODE_NIGHT_YES; + break; + case MODE_NIGHT_NO: + configNightMode = Configuration.UI_MODE_NIGHT_NO; + break; + default: + configNightMode = Configuration.UI_MODE_NIGHT_UNDEFINED; + } + try { + final ActivityTaskManagerInternal.PackageConfigurationUpdater updater = + mActivityTaskManager.createPackageConfigurationUpdater(); + updater.setNightMode(configNightMode); + updater.commit(); + } catch (RemoteException e) { + throw e; + } + } + + @Override public boolean isUiModeLocked() { synchronized (mLock) { return mUiModeLocked; diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index e91bb46657e1..b06e84d9df4e 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -16,6 +16,7 @@ package com.android.server.pm; +import static android.app.ActivityOptions.KEY_SPLASH_SCREEN_THEME; import static android.app.PendingIntent.FLAG_IMMUTABLE; import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; @@ -935,6 +936,17 @@ public class LauncherAppsService extends SystemService { intents[0].addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intents[0].setSourceBounds(sourceBounds); + // Replace theme for splash screen + final int splashScreenThemeResId = + mShortcutServiceInternal.getShortcutStartingThemeResId(getCallingUserId(), + callingPackage, packageName, shortcutId, targetUserId); + if (splashScreenThemeResId != 0) { + if (startActivityOptions == null) { + startActivityOptions = new Bundle(); + } + startActivityOptions.putInt(KEY_SPLASH_SCREEN_THEME, splashScreenThemeResId); + } + return startShortcutIntentsAsPublisher( intents, packageName, featureId, startActivityOptions, targetUserId); } diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java index 9b092c000172..bb4ec16be0a8 100644 --- a/services/core/java/com/android/server/pm/ShortcutPackage.java +++ b/services/core/java/com/android/server/pm/ShortcutPackage.java @@ -112,6 +112,7 @@ class ShortcutPackage extends ShortcutPackageItem { private static final String ATTR_BITMAP_PATH = "bitmap-path"; private static final String ATTR_ICON_URI = "icon-uri"; private static final String ATTR_LOCUS_ID = "locus-id"; + private static final String ATTR_SPLASH_SCREEN_THEME_ID = "splash-screen-theme-id"; private static final String ATTR_PERSON_NAME = "name"; private static final String ATTR_PERSON_URI = "uri"; @@ -1654,6 +1655,7 @@ class ShortcutPackage extends ShortcutPackageItem { ShortcutService.writeAttr(out, ATTR_TITLE, si.getTitle()); ShortcutService.writeAttr(out, ATTR_TITLE_RES_ID, si.getTitleResId()); ShortcutService.writeAttr(out, ATTR_TITLE_RES_NAME, si.getTitleResName()); + ShortcutService.writeAttr(out, ATTR_SPLASH_SCREEN_THEME_ID, si.getStartingThemeResId()); ShortcutService.writeAttr(out, ATTR_TEXT, si.getText()); ShortcutService.writeAttr(out, ATTR_TEXT_RES_ID, si.getTextResId()); ShortcutService.writeAttr(out, ATTR_TEXT_RES_NAME, si.getTextResName()); @@ -1861,6 +1863,7 @@ class ShortcutPackage extends ShortcutPackageItem { String bitmapPath; String iconUri; final String locusIdString; + int splashScreenThemeResId; int backupVersionCode; ArraySet<String> categories = null; ArrayList<Person> persons = new ArrayList<>(); @@ -1871,6 +1874,8 @@ class ShortcutPackage extends ShortcutPackageItem { title = ShortcutService.parseStringAttribute(parser, ATTR_TITLE); titleResId = ShortcutService.parseIntAttribute(parser, ATTR_TITLE_RES_ID); titleResName = ShortcutService.parseStringAttribute(parser, ATTR_TITLE_RES_NAME); + splashScreenThemeResId = ShortcutService.parseIntAttribute(parser, + ATTR_SPLASH_SCREEN_THEME_ID); text = ShortcutService.parseStringAttribute(parser, ATTR_TEXT); textResId = ShortcutService.parseIntAttribute(parser, ATTR_TEXT_RES_ID); textResName = ShortcutService.parseStringAttribute(parser, ATTR_TEXT_RES_NAME); @@ -1964,7 +1969,8 @@ class ShortcutPackage extends ShortcutPackageItem { intents.toArray(new Intent[intents.size()]), rank, extras, lastChangedTimestamp, flags, iconResId, iconResName, bitmapPath, iconUri, - disabledReason, persons.toArray(new Person[persons.size()]), locusId); + disabledReason, persons.toArray(new Person[persons.size()]), locusId, + splashScreenThemeResId); } private static Intent parseIntent(TypedXmlPullParser parser) diff --git a/services/core/java/com/android/server/pm/ShortcutParser.java b/services/core/java/com/android/server/pm/ShortcutParser.java index d3aace19672a..c06f01a463ad 100644 --- a/services/core/java/com/android/server/pm/ShortcutParser.java +++ b/services/core/java/com/android/server/pm/ShortcutParser.java @@ -383,6 +383,8 @@ public class ShortcutParser { final int textResId = sa.getResourceId(R.styleable.Shortcut_shortcutLongLabel, 0); final int disabledMessageResId = sa.getResourceId( R.styleable.Shortcut_shortcutDisabledMessage, 0); + final int splashScreenTheme = sa.getResourceId( + R.styleable.Shortcut_splashScreenTheme, 0); if (TextUtils.isEmpty(id)) { Log.w(TAG, "android:shortcutId must be provided. activity=" + activity); @@ -404,7 +406,8 @@ public class ShortcutParser { disabledMessageResId, rank, iconResId, - enabled); + enabled, + splashScreenTheme); } finally { sa.recycle(); } @@ -413,7 +416,7 @@ public class ShortcutParser { private static ShortcutInfo createShortcutFromManifest(ShortcutService service, @UserIdInt int userId, String id, String packageName, ComponentName activityComponent, int titleResId, int textResId, int disabledMessageResId, - int rank, int iconResId, boolean enabled) { + int rank, int iconResId, boolean enabled, int splashScreenTheme) { final int flags = (enabled ? ShortcutInfo.FLAG_MANIFEST : ShortcutInfo.FLAG_DISABLED) @@ -452,7 +455,8 @@ public class ShortcutParser { null, // icon Url disabledReason, null /* persons */, - null /* locusId */); + null /* locusId */, + splashScreenTheme); } private static String parseCategory(ShortcutService service, AttributeSet attrs) { diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index 863e3fe5c6a3..4d8abea8acd4 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -3214,6 +3214,32 @@ public class ShortcutService extends IShortcutService.Stub { } @Override + public int getShortcutStartingThemeResId(int launcherUserId, + @NonNull String callingPackage, @NonNull String packageName, + @NonNull String shortcutId, int userId) { + Objects.requireNonNull(callingPackage, "callingPackage"); + Objects.requireNonNull(packageName, "packageName"); + Objects.requireNonNull(shortcutId, "shortcutId"); + + synchronized (mLock) { + throwIfUserLockedL(userId); + throwIfUserLockedL(launcherUserId); + + getLauncherShortcutsLocked(callingPackage, userId, launcherUserId) + .attemptToRestoreIfNeededAndSave(); + + final ShortcutPackage p = getUserShortcutsLocked(userId) + .getPackageShortcutsIfExists(packageName); + if (p == null) { + return 0; + } + + final ShortcutInfo shortcutInfo = p.findShortcutById(shortcutId); + return shortcutInfo != null ? shortcutInfo.getStartingThemeResId() : 0; + } + } + + @Override public ParcelFileDescriptor getShortcutIconFd(int launcherUserId, @NonNull String callingPackage, @NonNull String packageName, @NonNull String shortcutId, int userId) { diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index 771b712cf480..c63a0f093dea 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -134,10 +134,10 @@ class ActivityClientController extends IActivityClientController.Stub { } @Override - public void activityResumed(IBinder token) { + public void activityResumed(IBinder token, boolean handleSplashScreenExit) { final long origId = Binder.clearCallingIdentity(); synchronized (mGlobalLock) { - ActivityRecord.activityResumedLocked(token); + ActivityRecord.activityResumedLocked(token, handleSplashScreenExit); } Binder.restoreCallingIdentity(origId); } @@ -692,6 +692,18 @@ class ActivityClientController extends IActivityClientController.Stub { } /** + * Splash screen view is attached to activity. + */ + @Override + public void splashScreenAttached(IBinder token) { + final long origId = Binder.clearCallingIdentity(); + synchronized (mGlobalLock) { + ActivityRecord.splashScreenAttachedLocked(token); + } + Binder.restoreCallingIdentity(origId); + } + + /** * Checks the state of the system and the activity associated with the given {@param token} to * verify that picture-in-picture is supported for that activity. * diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 68a2c5d5233c..f440e566d47e 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -42,6 +42,8 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.activityTypeToString; +import static android.app.servertransaction.TransferSplashScreenViewStateItem.ATTACH_TO; +import static android.app.servertransaction.TransferSplashScreenViewStateItem.HANDOVER_TO; import static android.content.Intent.ACTION_MAIN; import static android.content.Intent.CATEGORY_HOME; import static android.content.Intent.CATEGORY_LAUNCHER; @@ -243,6 +245,7 @@ import android.app.servertransaction.ResumeActivityItem; import android.app.servertransaction.StartActivityItem; import android.app.servertransaction.StopActivityItem; import android.app.servertransaction.TopResumedActivityChangeItem; +import android.app.servertransaction.TransferSplashScreenViewStateItem; import android.app.usage.UsageEvents.Event; import android.content.ComponentName; import android.content.Intent; @@ -300,6 +303,7 @@ import android.view.WindowManager.LayoutParams; import android.view.WindowManager.TransitionOldType; import android.view.animation.Animation; import android.window.IRemoteTransition; +import android.window.SplashScreenView.SplashScreenViewParcelable; import android.window.TaskSnapshot; import android.window.WindowContainerToken; @@ -669,6 +673,30 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A boolean startingDisplayed; boolean startingMoved; + boolean mHandleExitSplashScreen; + @TransferSplashScreenState int mTransferringSplashScreenState = + TRANSFER_SPLASH_SCREEN_IDLE; + + // Idle, can be triggered to do transfer if needed. + static final int TRANSFER_SPLASH_SCREEN_IDLE = 0; + // requesting a copy from shell. + static final int TRANSFER_SPLASH_SCREEN_COPYING = 1; + // attach the splash screen view to activity. + static final int TRANSFER_SPLASH_SCREEN_ATTACH_TO_CLIENT = 2; + // client has taken over splash screen view. + static final int TRANSFER_SPLASH_SCREEN_FINISH = 3; + + @IntDef(prefix = { "TRANSFER_SPLASH_SCREEN_" }, value = { + TRANSFER_SPLASH_SCREEN_IDLE, + TRANSFER_SPLASH_SCREEN_COPYING, + TRANSFER_SPLASH_SCREEN_ATTACH_TO_CLIENT, + TRANSFER_SPLASH_SCREEN_FINISH, + }) + @interface TransferSplashScreenState {} + + // How long we wait until giving up transfer splash screen. + private static final int TRANSFER_SPLASH_SCREEN_TIMEOUT = 2000; + // TODO: Have a WindowContainer state for tracking exiting/deferred removal. boolean mIsExiting; // Force an app transition to be ran in the case the visibility of the app did not change. @@ -1727,6 +1755,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (_createTime > 0) { createTime = _createTime; } + mAtmService.mPackageConfigPersister.updateConfigIfNeeded(this, mUserId, packageName); } /** @@ -1807,7 +1836,85 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return hasProcess() && app.hasThread(); } - boolean addStartingWindow(String pkg, int theme, CompatibilityInfo compatInfo, + /** + * Evaluate the theme for a starting window. + * @param originalTheme The original theme which read from activity or application. + * @param replaceTheme The replace theme which requested from starter. + * @return Resolved theme. + */ + private int evaluateStartingWindowTheme(String pkg, int originalTheme, int replaceTheme) { + // Skip if the package doesn't want a starting window. + if (!validateStartingWindowTheme(pkg, originalTheme)) { + return 0; + } + int selectedTheme = originalTheme; + if (replaceTheme != 0 && validateStartingWindowTheme(pkg, replaceTheme)) { + // allow to replace theme + selectedTheme = replaceTheme; + } + return selectedTheme; + } + + private boolean validateStartingWindowTheme(String pkg, int theme) { + // If this is a translucent window, then don't show a starting window -- the current + // effect (a full-screen opaque starting window that fades away to the real contents + // when it is ready) does not work for this. + ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Checking theme of starting window: 0x%x", theme); + if (theme != 0) { + AttributeCache.Entry ent = AttributeCache.instance().get(pkg, theme, + com.android.internal.R.styleable.Window, + mWmService.mCurrentUserId); + if (ent == null) { + // Whoops! App doesn't exist. Um. Okay. We'll just pretend like we didn't + // see that. + return false; + } + final boolean windowIsTranslucent = ent.array.getBoolean( + com.android.internal.R.styleable.Window_windowIsTranslucent, false); + final boolean windowIsFloating = ent.array.getBoolean( + com.android.internal.R.styleable.Window_windowIsFloating, false); + final boolean windowShowWallpaper = ent.array.getBoolean( + com.android.internal.R.styleable.Window_windowShowWallpaper, false); + final boolean windowDisableStarting = ent.array.getBoolean( + com.android.internal.R.styleable.Window_windowDisablePreview, false); + ProtoLog.v(WM_DEBUG_STARTING_WINDOW, + "Translucent=%s Floating=%s ShowWallpaper=%s Disable=%s", + windowIsTranslucent, windowIsFloating, windowShowWallpaper, + windowDisableStarting); + if (windowIsTranslucent || windowIsFloating || windowDisableStarting) { + return false; + } + if (windowShowWallpaper + && getDisplayContent().mWallpaperController.getWallpaperTarget() != null) { + return false; + } + } + return true; + } + + private void applyStartingWindowTheme(String pkg, int theme) { + if (theme != 0) { + AttributeCache.Entry ent = AttributeCache.instance().get(pkg, theme, + com.android.internal.R.styleable.Window, + mWmService.mCurrentUserId); + if (ent == null) { + return; + } + final boolean windowShowWallpaper = ent.array.getBoolean( + com.android.internal.R.styleable.Window_windowShowWallpaper, false); + if (windowShowWallpaper && getDisplayContent().mWallpaperController + .getWallpaperTarget() == null) { + // If this theme is requesting a wallpaper, and the wallpaper + // is not currently visible, then this effectively serves as + // an opaque window and our starting window transition animation + // can still work. We just need to make sure the starting window + // is also showing the wallpaper. + windowFlags |= FLAG_SHOW_WALLPAPER; + } + } + } + + boolean addStartingWindow(String pkg, int resolvedTheme, CompatibilityInfo compatInfo, CharSequence nonLocalizedLabel, int labelRes, int icon, int logo, int windowFlags, IBinder transferFrom, boolean newTask, boolean taskSwitch, boolean processRunning, boolean allowTaskSnapshot, boolean activityCreated) { @@ -1850,49 +1957,12 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return createSnapshot(snapshot, typeParameter); } - // If this is a translucent window, then don't show a starting window -- the current - // effect (a full-screen opaque starting window that fades away to the real contents - // when it is ready) does not work for this. - ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Checking theme of starting window: 0x%x", theme); - if (theme != 0) { - AttributeCache.Entry ent = AttributeCache.instance().get(pkg, theme, - com.android.internal.R.styleable.Window, - mWmService.mCurrentUserId); - if (ent == null) { - // Whoops! App doesn't exist. Um. Okay. We'll just pretend like we didn't - // see that. - return false; - } - final boolean windowIsTranslucent = ent.array.getBoolean( - com.android.internal.R.styleable.Window_windowIsTranslucent, false); - final boolean windowIsFloating = ent.array.getBoolean( - com.android.internal.R.styleable.Window_windowIsFloating, false); - final boolean windowShowWallpaper = ent.array.getBoolean( - com.android.internal.R.styleable.Window_windowShowWallpaper, false); - final boolean windowDisableStarting = ent.array.getBoolean( - com.android.internal.R.styleable.Window_windowDisablePreview, false); - ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Translucent=%s Floating=%s ShowWallpaper=%s", - windowIsTranslucent, windowIsFloating, windowShowWallpaper); - if (windowIsTranslucent) { - return false; - } - if (windowIsFloating || windowDisableStarting) { - return false; - } - if (windowShowWallpaper) { - if (getDisplayContent().mWallpaperController - .getWallpaperTarget() == null) { - // If this theme is requesting a wallpaper, and the wallpaper - // is not currently visible, then this effectively serves as - // an opaque window and our starting window transition animation - // can still work. We just need to make sure the starting window - // is also showing the wallpaper. - windowFlags |= FLAG_SHOW_WALLPAPER; - } else { - return false; - } - } + // Original theme can be 0 if developer doesn't request any theme. So if resolved theme is 0 + // but original theme is not 0, means this package doesn't want a starting window. + if (resolvedTheme == 0 && theme != 0) { + return false; } + applyStartingWindowTheme(pkg, resolvedTheme); if (transferStartingWindow(transferFrom)) { return true; @@ -1906,7 +1976,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Creating SplashScreenStartingData"); mStartingData = new SplashScreenStartingData(mWmService, pkg, - theme, compatInfo, nonLocalizedLabel, labelRes, icon, logo, windowFlags, + resolvedTheme, compatInfo, nonLocalizedLabel, labelRes, icon, logo, windowFlags, getMergedOverrideConfiguration(), typeParameter); scheduleAddStartingWindow(); return true; @@ -2031,7 +2101,118 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return snapshot.getRotation() == targetRotation; } + /** + * See {@link SplashScreen#setOnExitAnimationListener}. + */ + void setCustomizeSplashScreenExitAnimation(boolean enable) { + if (mHandleExitSplashScreen == enable) { + return; + } + mHandleExitSplashScreen = enable; + } + + private final Runnable mTransferSplashScreenTimeoutRunnable = new Runnable() { + @Override + public void run() { + synchronized (mAtmService.mGlobalLock) { + Slog.w(TAG, "Activity transferring splash screen timeout for " + + ActivityRecord.this + " state " + mTransferringSplashScreenState); + if (isTransferringSplashScreen()) { + mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_FINISH; + // TODO show default exit splash screen animation + removeStartingWindow(); + } + } + } + }; + + private void scheduleTransferSplashScreenTimeout() { + mAtmService.mH.postDelayed(mTransferSplashScreenTimeoutRunnable, + TRANSFER_SPLASH_SCREEN_TIMEOUT); + } + + private void removeTransferSplashScreenTimeout() { + mAtmService.mH.removeCallbacks(mTransferSplashScreenTimeoutRunnable); + } + + private boolean transferSplashScreenIfNeeded() { + if (!mHandleExitSplashScreen || mStartingSurface == null || mStartingWindow == null + || mTransferringSplashScreenState == TRANSFER_SPLASH_SCREEN_FINISH) { + return false; + } + if (isTransferringSplashScreen()) { + return true; + } + requestCopySplashScreen(); + return isTransferringSplashScreen(); + } + + private boolean isTransferringSplashScreen() { + return mTransferringSplashScreenState == TRANSFER_SPLASH_SCREEN_ATTACH_TO_CLIENT + || mTransferringSplashScreenState == TRANSFER_SPLASH_SCREEN_COPYING; + } + + private void requestCopySplashScreen() { + mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_COPYING; + if (!mAtmService.mTaskOrganizerController.copySplashScreenView(getTask())) { + mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_FINISH; + removeStartingWindow(); + } + scheduleTransferSplashScreenTimeout(); + } + + /** + * Receive the splash screen data from shell, sending to client. + * @param parcelable The data to reconstruct the splash screen view, null mean unable to copy. + */ + void onCopySplashScreenFinish(SplashScreenViewParcelable parcelable) { + removeTransferSplashScreenTimeout(); + // unable to copy from shell, maybe it's not a splash screen. or something went wrong. + // either way, abort and reset the sequence. + if (parcelable == null + || mTransferringSplashScreenState != TRANSFER_SPLASH_SCREEN_COPYING) { + if (parcelable != null) { + parcelable.clearIfNeeded(); + } + mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_FINISH; + removeStartingWindow(); + return; + } + // schedule attach splashScreen to client + try { + mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_ATTACH_TO_CLIENT; + mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken, + TransferSplashScreenViewStateItem.obtain(ATTACH_TO, parcelable)); + scheduleTransferSplashScreenTimeout(); + } catch (Exception e) { + Slog.w(TAG, "onCopySplashScreenComplete fail: " + this); + parcelable.clearIfNeeded(); + mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_FINISH; + } + } + + private void onSplashScreenAttachComplete() { + removeTransferSplashScreenTimeout(); + // Client has draw the splash screen, so we can remove the starting window. + if (mStartingWindow != null) { + mStartingWindow.hide(false, false); + } + try { + mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken, + TransferSplashScreenViewStateItem.obtain(HANDOVER_TO, null)); + } catch (Exception e) { + Slog.w(TAG, "onSplashScreenAttachComplete fail: " + this); + } + // no matter what, remove the starting window. + mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_FINISH; + removeStartingWindow(); + } + void removeStartingWindow() { + if (transferSplashScreenIfNeeded()) { + return; + } + mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_IDLE; if (mStartingWindow == null) { if (mStartingData != null) { // Starting window has not been added yet, but it is scheduled to be added. @@ -5092,7 +5273,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } } - static void activityResumedLocked(IBinder token) { + static void activityResumedLocked(IBinder token, boolean handleSplashScreenExit) { final ActivityRecord r = ActivityRecord.forTokenLocked(token); ProtoLog.i(WM_DEBUG_STATES, "Resumed activity; dropping state of: %s", r); if (r == null) { @@ -5100,12 +5281,22 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // been removed (e.g. destroy timeout), so the token could be null. return; } + r.setCustomizeSplashScreenExitAnimation(handleSplashScreenExit); r.setSavedState(null /* savedState */); r.mDisplayContent.handleActivitySizeCompatModeIfNeeded(r); r.mDisplayContent.mUnknownAppVisibilityController.notifyAppResumedFinished(r); } + static void splashScreenAttachedLocked(IBinder token) { + final ActivityRecord r = ActivityRecord.forTokenLocked(token); + if (r == null) { + Slog.w(TAG, "splashScreenTransferredLocked cannot find activity"); + return; + } + r.onSplashScreenAttachComplete(); + } + /** * Once we know that we have asked an application to put an activity in the resumed state * (either by launching it or explicitly telling it), this function updates the rest of our @@ -5931,7 +6122,13 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A pendingVoiceInteractionStart = false; } - void showStartingWindow(ActivityRecord prev, boolean newTask, boolean taskSwitch) { + void showStartingWindow(boolean taskSwitch) { + showStartingWindow(null /* prev */, false /* newTask */, taskSwitch, + 0 /* splashScreenTheme */); + } + + void showStartingWindow(ActivityRecord prev, boolean newTask, boolean taskSwitch, + int splashScreenTheme) { if (mTaskOverlay) { // We don't show starting window for overlay activities. return; @@ -5944,7 +6141,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A final CompatibilityInfo compatInfo = mAtmService.compatibilityInfoForPackageLocked(info.applicationInfo); - final boolean shown = addStartingWindow(packageName, theme, + + final int resolvedTheme = evaluateStartingWindowTheme(packageName, theme, + splashScreenTheme); + final boolean shown = addStartingWindow(packageName, resolvedTheme, compatInfo, nonLocalizedLabel, labelRes, icon, logo, windowFlags, prev != null ? prev.appToken : null, newTask, taskSwitch, isProcessRunning(), allowTaskSnapshot(), diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 79f8229c6162..37fda4ce217a 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -1997,8 +1997,7 @@ class ActivityStarter { if (mMovedToFront) { // We moved the task to front, use starting window to hide initial drawn delay. - targetTaskTop.showStartingWindow(null /* prev */, false /* newTask */, - true /* taskSwitch */); + targetTaskTop.showStartingWindow(true /* taskSwitch */); } else if (mDoResume) { // Make sure the root task and its belonging display are moved to topmost. mTargetRootTask.moveToFront("intentActivityFound"); diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java index 7d2075cca84d..94379b1f230e 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java @@ -574,4 +574,26 @@ public abstract class ActivityTaskManagerInternal { * @return Whether the package is the base of any locked task */ public abstract boolean isBaseOfLockedTask(String packageName); + + /** + * Create an interface to update configuration for an application. + */ + public abstract PackageConfigurationUpdater createPackageConfigurationUpdater(); + + /** + * An interface to update configuration for an application, and will persist override + * configuration for this package. + */ + public interface PackageConfigurationUpdater { + /** + * Sets the dark mode for the current application. This setting is persisted and will + * override the system configuration for this application. + */ + PackageConfigurationUpdater setNightMode(int nightMode); + + /** + * Commit changes. + */ + void commit() throws RemoteException; + } } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index f16a646d00a1..2e98c2cbc5c2 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -224,6 +224,7 @@ import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationDefinition; import android.view.WindowManager; import android.window.IWindowOrganizerController; +import android.window.SplashScreenView.SplashScreenViewParcelable; import android.window.TaskSnapshot; import android.window.WindowContainerTransaction; @@ -451,6 +452,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { /** The controller for all operations related to locktask. */ private LockTaskController mLockTaskController; private ActivityStartController mActivityStartController; + PackageConfigPersister mPackageConfigPersister; boolean mSuppressResizeConfigChanges; @@ -866,6 +868,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { setRecentTasks(new RecentTasks(this, mTaskSupervisor)); mVrController = new VrController(mGlobalLock); mKeyguardController = mTaskSupervisor.getKeyguardController(); + mPackageConfigPersister = new PackageConfigPersister(mTaskSupervisor.mPersisterQueue); } public void onActivityManagerInternalAdded() { @@ -2027,8 +2030,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { // We are reshowing a task, use a starting window to hide the initial draw delay // so the transition can start earlier. - topActivity.showStartingWindow(null /* prev */, false /* newTask */, - true /* taskSwitch */); + topActivity.showStartingWindow(true /* taskSwitch */); } } finally { Binder.restoreCallingIdentity(origId); @@ -3249,6 +3251,30 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } /** + * A splash screen view has copied, pass it to an activity. + * + * @param taskId Id of task to handle the material to reconstruct the view. + * @param parcelable Used to reconstruct the view, null means the surface is un-copyable. + * @hide + */ + @Override + public void onSplashScreenViewCopyFinished(int taskId, SplashScreenViewParcelable parcelable) + throws RemoteException { + mAmInternal.enforceCallingPermission(MANAGE_ACTIVITY_TASKS, + "copySplashScreenViewFinish()"); + synchronized (mGlobalLock) { + final Task task = mRootWindowContainer.anyTaskForId(taskId, + MATCH_ATTACHED_TASK_ONLY); + if (task != null) { + final ActivityRecord r = task.getTopWaitSplashScreenActivity(); + if (r != null) { + r.onCopySplashScreenFinish(parcelable); + } + } + } + } + + /** * Puts the given activity in picture in picture mode if possible. * * @return true if the activity is now in picture-in-picture mode, or false if it could not @@ -5433,6 +5459,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { synchronized (mGlobalLock) { mAppWarnings.onPackageUninstalled(name); mCompatModePackages.handlePackageUninstalledLocked(name); + mPackageConfigPersister.onPackageUninstall(name); } } @@ -6103,6 +6130,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { public void removeUser(int userId) { synchronized (mGlobalLock) { mRootWindowContainer.removeUser(userId); + mPackageConfigPersister.removeUser(userId); } } @@ -6200,6 +6228,8 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { public void loadRecentTasksForUser(int userId) { synchronized (mGlobalLock) { mRecentTasks.loadUserRecentsLocked(userId); + // TODO renaming the methods(?) + mPackageConfigPersister.loadUserPackages(userId); } } @@ -6308,5 +6338,54 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { return getLockTaskController().isBaseOfLockedTask(packageName); } } + + @Override + public PackageConfigurationUpdater createPackageConfigurationUpdater() { + synchronized (mGlobalLock) { + return new PackageConfigurationUpdaterImpl(Binder.getCallingPid()); + } + } + } + + final class PackageConfigurationUpdaterImpl implements + ActivityTaskManagerInternal.PackageConfigurationUpdater { + private int mPid; + private int mNightMode; + + PackageConfigurationUpdaterImpl(int pid) { + mPid = pid; + } + + @Override + public ActivityTaskManagerInternal.PackageConfigurationUpdater setNightMode(int nightMode) { + mNightMode = nightMode; + return this; + } + + @Override + public void commit() throws RemoteException { + if (mPid == 0) { + throw new RemoteException("Invalid process"); + } + synchronized (mGlobalLock) { + final WindowProcessController wpc = mProcessMap.getProcess(mPid); + if (wpc == null) { + Slog.w(TAG, "Override application configuration: cannot find application"); + return; + } + if (wpc.getNightMode() == mNightMode) { + return; + } + if (!wpc.setOverrideNightMode(mNightMode)) { + return; + } + wpc.updateNightModeForAllActivities(mNightMode); + mPackageConfigPersister.updateFromImpl(wpc.mName, wpc.mUserId, this); + } + } + + int getNightMode() { + return mNightMode; + } } } diff --git a/services/core/java/com/android/server/wm/ConfigurationContainer.java b/services/core/java/com/android/server/wm/ConfigurationContainer.java index d90e88576909..efcaaa42ec13 100644 --- a/services/core/java/com/android/server/wm/ConfigurationContainer.java +++ b/services/core/java/com/android/server/wm/ConfigurationContainer.java @@ -534,6 +534,28 @@ public abstract class ConfigurationContainer<E extends ConfigurationContainer> { return getActivityType() == ACTIVITY_TYPE_ASSISTANT; } + /** + * Overrides the night mode applied to this ConfigurationContainer. + * @return true if the nightMode has been changed. + */ + public boolean setOverrideNightMode(int nightMode) { + final int currentUiMode = mFullConfiguration.uiMode; + final int currentNightMode = getNightMode(); + final int validNightMode = nightMode & Configuration.UI_MODE_NIGHT_MASK; + if (currentNightMode == validNightMode) { + return false; + } + mRequestsTmpConfig.setTo(getRequestedOverrideConfiguration()); + mRequestsTmpConfig.uiMode = validNightMode + | (currentUiMode & ~Configuration.UI_MODE_NIGHT_MASK); + onRequestedOverrideConfigurationChanged(mRequestsTmpConfig); + return true; + } + + int getNightMode() { + return mFullConfiguration.uiMode & Configuration.UI_MODE_NIGHT_MASK; + } + public boolean isActivityTypeDream() { return getActivityType() == ACTIVITY_TYPE_DREAM; } diff --git a/services/core/java/com/android/server/wm/PackageConfigPersister.java b/services/core/java/com/android/server/wm/PackageConfigPersister.java new file mode 100644 index 000000000000..1552a96d699a --- /dev/null +++ b/services/core/java/com/android/server/wm/PackageConfigPersister.java @@ -0,0 +1,380 @@ +/* + * Copyright (C) 2021 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.UiModeManager.MODE_NIGHT_AUTO; +import static android.app.UiModeManager.MODE_NIGHT_CUSTOM; + +import android.annotation.NonNull; +import android.os.Environment; +import android.util.AtomicFile; +import android.util.Slog; +import android.util.SparseArray; +import android.util.TypedXmlPullParser; +import android.util.TypedXmlSerializer; +import android.util.Xml; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.XmlUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; + +/** + * Persist configuration for each package, only persist the change if some on attributes are + * different from the global configuration. This class only applies to packages with Activities. + */ +public class PackageConfigPersister { + private static final String TAG = PackageConfigPersister.class.getSimpleName(); + private static final boolean DEBUG = false; + + private static final String TAG_CONFIG = "config"; + private static final String ATTR_PACKAGE_NAME = "package_name"; + private static final String ATTR_NIGHT_MODE = "night_mode"; + + private static final String PACKAGE_DIRNAME = "package_configs"; + private static final String SUFFIX_FILE_NAME = "_config.xml"; + + private final PersisterQueue mPersisterQueue; + private final Object mLock = new Object(); + + @GuardedBy("mLock") + private final SparseArray<HashMap<String, PackageConfigRecord>> mPendingWrite = + new SparseArray<>(); + @GuardedBy("mLock") + private final SparseArray<HashMap<String, PackageConfigRecord>> mModified = + new SparseArray<>(); + + private static File getUserConfigsDir(int userId) { + return new File(Environment.getDataSystemCeDirectory(userId), PACKAGE_DIRNAME); + } + + PackageConfigPersister(PersisterQueue queue) { + mPersisterQueue = queue; + } + + @GuardedBy("mLock") + void loadUserPackages(int userId) { + synchronized (mLock) { + final File userConfigsDir = getUserConfigsDir(userId); + final File[] configFiles = userConfigsDir.listFiles(); + if (configFiles == null) { + Slog.v(TAG, "loadPackages: empty list files from " + userConfigsDir); + return; + } + + for (int fileIndex = 0; fileIndex < configFiles.length; ++fileIndex) { + final File configFile = configFiles[fileIndex]; + if (DEBUG) { + Slog.d(TAG, "loadPackages: userId=" + userId + + ", configFile=" + configFile.getName()); + } + if (!configFile.getName().endsWith(SUFFIX_FILE_NAME)) { + continue; + } + + try (InputStream is = new FileInputStream(configFile)) { + final TypedXmlPullParser in = Xml.resolvePullParser(is); + int event; + String packageName = null; + int nightMode = MODE_NIGHT_AUTO; + while (((event = in.next()) != XmlPullParser.END_DOCUMENT) + && event != XmlPullParser.END_TAG) { + final String name = in.getName(); + if (event == XmlPullParser.START_TAG) { + if (DEBUG) { + Slog.d(TAG, "loadPackages: START_TAG name=" + name); + } + if (TAG_CONFIG.equals(name)) { + for (int attIdx = in.getAttributeCount() - 1; attIdx >= 0; + --attIdx) { + final String attrName = in.getAttributeName(attIdx); + final String attrValue = in.getAttributeValue(attIdx); + switch (attrName) { + case ATTR_PACKAGE_NAME: + packageName = attrValue; + break; + case ATTR_NIGHT_MODE: + nightMode = Integer.parseInt(attrValue); + break; + } + } + } + } + XmlUtils.skipCurrentTag(in); + } + if (packageName != null) { + final PackageConfigRecord initRecord = + findRecordOrCreate(mModified, packageName, userId); + initRecord.mNightMode = nightMode; + if (DEBUG) { + Slog.d(TAG, "loadPackages: load one package " + initRecord); + } + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (XmlPullParserException e) { + e.printStackTrace(); + } + } + } + } + + @GuardedBy("mLock") + void updateConfigIfNeeded(@NonNull ConfigurationContainer container, int userId, + String packageName) { + synchronized (mLock) { + final PackageConfigRecord modifiedRecord = findRecord(mModified, packageName, userId); + if (DEBUG) { + Slog.d(TAG, + "updateConfigIfNeeded record " + container + " find? " + modifiedRecord); + } + if (modifiedRecord != null) { + container.setOverrideNightMode(modifiedRecord.mNightMode); + } + } + } + + @GuardedBy("mLock") + void updateFromImpl(String packageName, int userId, + ActivityTaskManagerService.PackageConfigurationUpdaterImpl impl) { + synchronized (mLock) { + PackageConfigRecord record = findRecordOrCreate(mModified, packageName, userId); + record.mNightMode = impl.getNightMode(); + + if (record.isResetNightMode()) { + removePackage(record.mName, record.mUserId); + } else { + final PackageConfigRecord pendingRecord = + findRecord(mPendingWrite, record.mName, record.mUserId); + final PackageConfigRecord writeRecord; + if (pendingRecord == null) { + writeRecord = findRecordOrCreate(mPendingWrite, record.mName, + record.mUserId); + } else { + writeRecord = pendingRecord; + } + if (writeRecord.mNightMode == record.mNightMode) { + return; + } + writeRecord.mNightMode = record.mNightMode; + if (DEBUG) { + Slog.d(TAG, "PackageConfigUpdater save config " + writeRecord); + } + mPersisterQueue.addItem(new WriteProcessItem(writeRecord), false /* flush */); + } + } + } + + @GuardedBy("mLock") + void removeUser(int userId) { + synchronized (mLock) { + final HashMap<String, PackageConfigRecord> modifyRecords = mModified.get(userId); + final HashMap<String, PackageConfigRecord> writeRecords = mPendingWrite.get(userId); + if ((modifyRecords == null || modifyRecords.size() == 0) + && (writeRecords == null || writeRecords.size() == 0)) { + return; + } + final HashMap<String, PackageConfigRecord> tempList = new HashMap<>(modifyRecords); + tempList.forEach((name, record) -> { + removePackage(record.mName, record.mUserId); + }); + } + } + + @GuardedBy("mLock") + void onPackageUninstall(String packageName) { + synchronized (mLock) { + for (int i = mModified.size() - 1; i > 0; i--) { + final int userId = mModified.keyAt(i); + removePackage(packageName, userId); + } + } + } + + private void removePackage(String packageName, int userId) { + if (DEBUG) { + Slog.d(TAG, "removePackage packageName :" + packageName + " userId " + userId); + } + final PackageConfigRecord record = findRecord(mPendingWrite, packageName, userId); + if (record != null) { + removeRecord(mPendingWrite, record); + mPersisterQueue.removeItems(item -> + item.mRecord.mName == record.mName + && item.mRecord.mUserId == record.mUserId, + WriteProcessItem.class); + } + + final PackageConfigRecord modifyRecord = findRecord(mModified, packageName, userId); + if (modifyRecord != null) { + removeRecord(mModified, modifyRecord); + mPersisterQueue.addItem(new DeletePackageItem(userId, packageName), + false /* flush */); + } + } + + // store a changed data so we don't need to get the process + static class PackageConfigRecord { + final String mName; + final int mUserId; + int mNightMode; + + PackageConfigRecord(String name, int userId) { + mName = name; + mUserId = userId; + } + + boolean isResetNightMode() { + return mNightMode == MODE_NIGHT_AUTO || mNightMode == MODE_NIGHT_CUSTOM; + } + + @Override + public String toString() { + return "PackageConfigRecord package name: " + mName + " userId " + mUserId + + " nightMode " + mNightMode; + } + } + + private PackageConfigRecord findRecordOrCreate( + SparseArray<HashMap<String, PackageConfigRecord>> list, String name, int userId) { + HashMap<String, PackageConfigRecord> records = list.get(userId); + if (records == null) { + records = new HashMap<>(); + list.put(userId, records); + } + PackageConfigRecord record = records.get(name); + if (record != null) { + return record; + } + record = new PackageConfigRecord(name, userId); + records.put(name, record); + return record; + } + + private PackageConfigRecord findRecord(SparseArray<HashMap<String, PackageConfigRecord>> list, + String name, int userId) { + HashMap<String, PackageConfigRecord> packages = list.get(userId); + if (packages == null) { + return null; + } + return packages.get(name); + } + + private void removeRecord(SparseArray<HashMap<String, PackageConfigRecord>> list, + PackageConfigRecord record) { + final HashMap<String, PackageConfigRecord> processes = list.get(record.mUserId); + if (processes != null) { + processes.remove(record.mName); + } + } + + private static class DeletePackageItem implements PersisterQueue.WriteQueueItem { + final int mUserId; + final String mPackageName; + + DeletePackageItem(int userId, String packageName) { + mUserId = userId; + mPackageName = packageName; + } + + @Override + public void process() { + File userConfigsDir = getUserConfigsDir(mUserId); + if (!userConfigsDir.isDirectory()) { + return; + } + final AtomicFile atomicFile = new AtomicFile(new File(userConfigsDir, + mPackageName + SUFFIX_FILE_NAME)); + if (atomicFile.exists()) { + atomicFile.delete(); + } + } + } + + private class WriteProcessItem implements PersisterQueue.WriteQueueItem { + final PackageConfigRecord mRecord; + + WriteProcessItem(PackageConfigRecord record) { + mRecord = record; + } + + @Override + public void process() { + // Write out one user. + byte[] data = null; + synchronized (mLock) { + try { + data = saveToXml(); + } catch (Exception e) { + } + removeRecord(mPendingWrite, mRecord); + } + if (data != null) { + // Write out xml file while not holding mService lock. + FileOutputStream file = null; + AtomicFile atomicFile = null; + try { + File userConfigsDir = getUserConfigsDir(mRecord.mUserId); + if (!userConfigsDir.isDirectory() && !userConfigsDir.mkdirs()) { + Slog.e(TAG, "Failure creating tasks directory for user " + mRecord.mUserId + + ": " + userConfigsDir); + return; + } + atomicFile = new AtomicFile(new File(userConfigsDir, + mRecord.mName + SUFFIX_FILE_NAME)); + file = atomicFile.startWrite(); + file.write(data); + atomicFile.finishWrite(file); + } catch (IOException e) { + if (file != null) { + atomicFile.failWrite(file); + } + Slog.e(TAG, "Unable to open " + atomicFile + " for persisting. " + e); + } + } + } + + private byte[] saveToXml() throws IOException { + final ByteArrayOutputStream os = new ByteArrayOutputStream(); + final TypedXmlSerializer xmlSerializer = Xml.resolveSerializer(os); + + xmlSerializer.startDocument(null, true); + if (DEBUG) { + Slog.d(TAG, "Writing package configuration=" + mRecord); + } + xmlSerializer.startTag(null, TAG_CONFIG); + xmlSerializer.attribute(null, ATTR_PACKAGE_NAME, mRecord.mName); + xmlSerializer.attributeInt(null, ATTR_NIGHT_MODE, mRecord.mNightMode); + xmlSerializer.endTag(null, TAG_CONFIG); + xmlSerializer.endDocument(); + xmlSerializer.flush(); + + return os.toByteArray(); + } + } +} diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index ceebe9550846..6fbeaa4e944e 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -2656,7 +2656,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent> void addStartingWindowsForVisibleActivities() { forAllActivities((r) -> { if (r.mVisibleRequested) { - r.showStartingWindow(null /* prev */, false /* newTask */, true /*taskSwitch*/); + r.showStartingWindow(true /*taskSwitch*/); } }); } diff --git a/services/core/java/com/android/server/wm/StartingSurfaceController.java b/services/core/java/com/android/server/wm/StartingSurfaceController.java index 94e14dd0a6b8..ef4a40f4837c 100644 --- a/services/core/java/com/android/server/wm/StartingSurfaceController.java +++ b/services/core/java/com/android/server/wm/StartingSurfaceController.java @@ -60,7 +60,7 @@ public class StartingSurfaceController { final Task task = activity.getTask(); if (task != null && mService.mAtmService.mTaskOrganizerController.addStartingWindow(task, - activity.token)) { + activity.token, theme)) { return new ShellStartingSurface(task); } return null; @@ -125,7 +125,8 @@ public class StartingSurfaceController { return mService.mTaskSnapshotController .createStartingSurface(activity, taskSnapshot); } - mService.mAtmService.mTaskOrganizerController.addStartingWindow(task, activity.token); + mService.mAtmService.mTaskOrganizerController.addStartingWindow(task, activity.token, + 0 /* launchTheme */); return new ShellStartingSurface(task); } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 9c8a997ec098..8bd4dfd054b6 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -82,6 +82,7 @@ import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_RECENTS_ANIMA import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_TASKS; import static com.android.server.wm.ActivityRecord.STARTING_WINDOW_SHOWN; +import static com.android.server.wm.ActivityRecord.TRANSFER_SPLASH_SCREEN_COPYING; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RECENTS; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RESULTS; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_SWITCH; @@ -3881,6 +3882,13 @@ class Task extends WindowContainer<WindowContainer> { }); } + ActivityRecord getTopWaitSplashScreenActivity() { + return getActivity((r) -> { + return r.mHandleExitSplashScreen + && r.mTransferringSplashScreenState == TRANSFER_SPLASH_SCREEN_COPYING; + }); + } + boolean isTopActivityFocusable() { final ActivityRecord r = topRunningActivity(); return r != null ? r.isFocusable() @@ -4267,6 +4275,9 @@ class Task extends WindowContainer<WindowContainer> { if (mainWindow != null) { info.mainWindowLayoutParams = mainWindow.getAttrs(); } + // If the developer has persist a different configuration, we need to override it to the + // starting window because persisted configuration does not effect to Task. + info.taskInfo.configuration.setTo(topActivity.getConfiguration()); } final ActivityRecord topFullscreenActivity = getTopFullscreenActivity(); if (topFullscreenActivity != null) { @@ -6558,10 +6569,9 @@ class Task extends WindowContainer<WindowContainer> { Slog.i(TAG, "Restarting because process died: " + next); if (!next.hasBeenLaunched) { next.hasBeenLaunched = true; - } else if (SHOW_APP_STARTING_PREVIEW && lastFocusedRootTask != null + } else if (SHOW_APP_STARTING_PREVIEW && lastFocusedRootTask != null && lastFocusedRootTask.isTopRootTaskInDisplayArea()) { - next.showStartingWindow(null /* prev */, false /* newTask */, - false /* taskSwitch */); + next.showStartingWindow(false /* taskSwitch */); } mTaskSupervisor.startSpecificActivity(next, true, false); return true; @@ -6584,8 +6594,7 @@ class Task extends WindowContainer<WindowContainer> { next.hasBeenLaunched = true; } else { if (SHOW_APP_STARTING_PREVIEW) { - next.showStartingWindow(null /* prev */, false /* newTask */, - false /* taskSwich */); + next.showStartingWindow(false /* taskSwich */); } if (DEBUG_SWITCH) Slog.v(TAG_SWITCH, "Restarting: " + next); } @@ -6746,7 +6755,10 @@ class Task extends WindowContainer<WindowContainer> { prev = null; } } - r.showStartingWindow(prev, newTask, isTaskSwitch(r, focusedTopActivity)); + final int splashScreenThemeResId = options != null + ? options.getSplashScreenThemeResId() : 0; + r.showStartingWindow(prev, newTask, isTaskSwitch(r, focusedTopActivity), + splashScreenThemeResId); } } else { // If this is the first activity, don't do any fancy animations, diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java index 9fac3f003f70..286d31ed7d5f 100644 --- a/services/core/java/com/android/server/wm/TaskOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java @@ -117,8 +117,11 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { return mTaskOrganizer.asBinder(); } - void addStartingWindow(Task task, IBinder appToken) { + void addStartingWindow(Task task, IBinder appToken, int launchTheme) { final StartingWindowInfo info = task.getStartingWindowInfo(); + if (launchTheme != 0) { + info.splashScreenThemeResId = launchTheme; + } mDeferTaskOrgCallbacksConsumer.accept(() -> { try { mTaskOrganizer.addStartingWindow(info, appToken); @@ -138,6 +141,16 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { }); } + void copySplashScreenView(Task task) { + mDeferTaskOrgCallbacksConsumer.accept(() -> { + try { + mTaskOrganizer.copySplashScreenView(task.mTaskId); + } catch (RemoteException e) { + Slog.e(TAG, "Exception sending copyStartingWindowView callback", e); + } + }); + } + SurfaceControl prepareLeash(Task task, boolean visible, String reason) { SurfaceControl outSurfaceControl = new SurfaceControl(task.getSurfaceControl(), reason); if (!task.mCreatedByOrganizer && !visible) { @@ -232,14 +245,18 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { mUid = uid; } - void addStartingWindow(Task t, IBinder appToken) { - mOrganizer.addStartingWindow(t, appToken); + void addStartingWindow(Task t, IBinder appToken, int launchTheme) { + mOrganizer.addStartingWindow(t, appToken, launchTheme); } void removeStartingWindow(Task t) { mOrganizer.removeStartingWindow(t); } + void copySplashScreenView(Task t) { + mOrganizer.copySplashScreenView(t); + } + /** * Register this task with this state, but doesn't trigger the task appeared callback to * the organizer. @@ -465,14 +482,14 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { return !ArrayUtils.contains(UNSUPPORTED_WINDOWING_MODES, winMode); } - boolean addStartingWindow(Task task, IBinder appToken) { + boolean addStartingWindow(Task task, IBinder appToken, int launchTheme) { final Task rootTask = task.getRootTask(); if (rootTask == null || rootTask.mTaskOrganizer == null) { return false; } final TaskOrganizerState state = mTaskOrganizerStates.get(rootTask.mTaskOrganizer.asBinder()); - state.addStartingWindow(task, appToken); + state.addStartingWindow(task, appToken, launchTheme); return true; } @@ -486,6 +503,17 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { state.removeStartingWindow(task); } + boolean copySplashScreenView(Task task) { + final Task rootTask = task.getRootTask(); + if (rootTask == null || rootTask.mTaskOrganizer == null) { + return false; + } + final TaskOrganizerState state = + mTaskOrganizerStates.get(rootTask.mTaskOrganizer.asBinder()); + state.copySplashScreenView(task); + return true; + } + void onTaskAppeared(ITaskOrganizer organizer, Task task) { final TaskOrganizerState state = mTaskOrganizerStates.get(organizer.asBinder()); if (state != null && state.addTask(task)) { diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java index 264a3b4edfa6..c3620235d3df 100644 --- a/services/core/java/com/android/server/wm/WindowProcessController.java +++ b/services/core/java/com/android/server/wm/WindowProcessController.java @@ -256,6 +256,7 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio } onConfigurationChanged(atm.getGlobalConfiguration()); + mAtm.mPackageConfigPersister.updateConfigIfNeeded(this, mUserId, mName); } public void setPid(int pid) { @@ -802,6 +803,13 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio return false; } + void updateNightModeForAllActivities(int nightMode) { + for (int i = mActivities.size() - 1; i >= 0; --i) { + final ActivityRecord r = mActivities.get(i); + r.setOverrideNightMode(nightMode); + } + } + public void clearPackagePreferredForHomeActivities() { synchronized (mAtm.mGlobalLock) { for (int i = mActivities.size() - 1; i >= 0; --i) { diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java index be489c3bcd12..5614aa2a165d 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java @@ -394,7 +394,7 @@ public class NotificationListenerServiceTest extends UiServiceTestCase { "disabledMessage", 0, "disabledMessageResName", null, null, 0, null, 0, 0, 0, "iconResName", "bitmapPath", null, 0, - null, null); + null, null, 0); return si; } diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 72b84396e985..aa1110cd55a7 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -719,7 +719,7 @@ public class ActivityRecordTests extends WindowTestsBase { final ActivityRecord activity = createActivityWithTask(); assertTrue(activity.hasSavedState()); - ActivityRecord.activityResumedLocked(activity.appToken); + ActivityRecord.activityResumedLocked(activity.appToken, false /* handleSplashScreenExit */); assertFalse(activity.hasSavedState()); assertNull(activity.getSavedState()); } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java index 77fca3d2fdeb..9c1614393554 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java @@ -549,6 +549,9 @@ public class WindowOrganizerTests extends WindowTestsBase { public void removeStartingWindow(int taskId) { } @Override + public void copySplashScreenView(int taskId) { } + + @Override public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { } @Override @@ -609,10 +612,10 @@ public class WindowOrganizerTests extends WindowTestsBase { public void addStartingWindow(StartingWindowInfo info, IBinder appToken) { } - @Override public void removeStartingWindow(int taskId) { } - + @Override + public void copySplashScreenView(int taskId) { } @Override public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { } @@ -685,7 +688,8 @@ public class WindowOrganizerTests extends WindowTestsBase { @Override public void removeStartingWindow(int taskId) { } - + @Override + public void copySplashScreenView(int taskId) { } @Override public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) { } @@ -829,6 +833,8 @@ public class WindowOrganizerTests extends WindowTestsBase { @Override public void removeStartingWindow(int taskId) { } @Override + public void copySplashScreenView(int taskId) { } + @Override public void onTaskAppeared(RunningTaskInfo info, SurfaceControl leash) { mInfo = info; } 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 83b30a9747d7..c13d6b19bf1d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -1144,6 +1144,9 @@ class WindowTestsBase extends SystemServiceTestsBase { public void removeStartingWindow(int taskId) { } @Override + public void copySplashScreenView(int taskId) { + } + @Override public void onTaskAppeared(ActivityManager.RunningTaskInfo info, SurfaceControl leash) { } @Override |