1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.window;
18 
19 import android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.StyleRes;
22 import android.annotation.SuppressLint;
23 import android.annotation.UiThread;
24 import android.app.Activity;
25 import android.app.ActivityThread;
26 import android.app.AppGlobals;
27 import android.content.Context;
28 import android.content.res.Resources;
29 import android.os.IBinder;
30 import android.os.RemoteException;
31 import android.util.Log;
32 import android.util.Singleton;
33 import android.util.Slog;
34 
35 import java.util.ArrayList;
36 
37 /**
38  * The interface that apps use to talk to the splash screen.
39  * <p>
40  * Each splash screen instance is bound to a particular {@link Activity}.
41  * To obtain a {@link SplashScreen} for an Activity, use
42  * <code>Activity.getSplashScreen()</code> to get the SplashScreen.</p>
43  */
44 public interface SplashScreen {
45     /**
46      * The splash screen style is not defined.
47      * @hide
48      */
49     int SPLASH_SCREEN_STYLE_UNDEFINED = -1;
50     /**
51      * Force splash screen to be empty.
52      * @hide
53      */
54     int SPLASH_SCREEN_STYLE_EMPTY = 0;
55     /**
56      * Force splash screen to show icon.
57      * @hide
58      */
59     int SPLASH_SCREEN_STYLE_ICON = 1;
60 
61     /** @hide */
62     @IntDef(prefix = { "SPLASH_SCREEN_STYLE_" }, value = {
63             SPLASH_SCREEN_STYLE_UNDEFINED,
64             SPLASH_SCREEN_STYLE_EMPTY,
65             SPLASH_SCREEN_STYLE_ICON
66     })
67     @interface SplashScreenStyle {}
68 
69     /**
70      * <p>Specifies whether an {@link Activity} wants to handle the splash screen animation on its
71      * own. Normally the splash screen will show on screen before the content of the activity has
72      * been drawn, and disappear when the activity is showing on the screen. With this listener set,
73      * the activity will receive {@link OnExitAnimationListener#onSplashScreenExit} callback if
74      * splash screen is showed, then the activity can create its own exit animation based on the
75      * SplashScreenView.</p>
76      *
77      * <p> Note that this method must be called before splash screen leave, so it only takes effect
78      * during or before {@link Activity#onResume}.</p>
79      *
80      * @param listener the listener for receive the splash screen with
81      *
82      * @see OnExitAnimationListener#onSplashScreenExit(SplashScreenView)
83      */
84     @SuppressLint("ExecutorRegistration")
setOnExitAnimationListener(@onNull SplashScreen.OnExitAnimationListener listener)85     void setOnExitAnimationListener(@NonNull SplashScreen.OnExitAnimationListener listener);
86 
87     /**
88      * Clear exist listener
89      * @see #setOnExitAnimationListener
90      */
clearOnExitAnimationListener()91     void clearOnExitAnimationListener();
92 
93 
94     /**
95      * Overrides the theme used for the {@link SplashScreen}s of this application.
96      * <p>
97      * By default, the {@link SplashScreen} uses the theme set in the manifest. This method
98      * overrides and persists the theme used for the {@link SplashScreen} of this application.
99      * <p>
100      * To reset to the default theme, set this the themeId to {@link Resources#ID_NULL}.
101      * <p>
102      * <b>Note:</b> The theme name must be stable across versions, otherwise it won't be found
103      * after your application is updated.
104      */
setSplashScreenTheme(@tyleRes int themeId)105     void setSplashScreenTheme(@StyleRes int themeId);
106 
107     /**
108      * Listens for the splash screen exit event.
109      */
110     interface OnExitAnimationListener {
111         /**
112          * When receiving this callback, the {@link SplashScreenView} object will be drawing on top
113          * of the activity. The {@link SplashScreenView} represents the splash screen view
114          * object, developer can make an exit animation based on this view.</p>
115          *
116          * <p>This method is never invoked if your activity clear the listener by
117          * {@link #clearOnExitAnimationListener}.
118          *
119          * @param view The view object which on top of this Activity.
120          * @see #setOnExitAnimationListener
121          * @see #clearOnExitAnimationListener
122          */
123         @UiThread
onSplashScreenExit(@onNull SplashScreenView view)124         void onSplashScreenExit(@NonNull SplashScreenView view);
125     }
126 
127     /**
128      * @hide
129      */
130     class SplashScreenImpl implements SplashScreen {
131         private static final String TAG = "SplashScreenImpl";
132 
133         private OnExitAnimationListener mExitAnimationListener;
134         private final IBinder mActivityToken;
135         private final SplashScreenManagerGlobal mGlobal;
136 
SplashScreenImpl(Context context)137         public SplashScreenImpl(Context context) {
138             mActivityToken = context.getActivityToken();
139             mGlobal = SplashScreenManagerGlobal.getInstance();
140         }
141 
142         @Override
setOnExitAnimationListener( @onNull SplashScreen.OnExitAnimationListener listener)143         public void setOnExitAnimationListener(
144                 @NonNull SplashScreen.OnExitAnimationListener listener) {
145             if (mActivityToken == null) {
146                 // This is not an activity.
147                 return;
148             }
149             synchronized (mGlobal.mGlobalLock) {
150                 if (listener != null) {
151                     mExitAnimationListener = listener;
152                     mGlobal.addImpl(this);
153                 }
154             }
155         }
156 
157         @Override
clearOnExitAnimationListener()158         public void clearOnExitAnimationListener() {
159             if (mActivityToken == null) {
160                 // This is not an activity.
161                 return;
162             }
163             synchronized (mGlobal.mGlobalLock) {
164                 mExitAnimationListener = null;
165                 mGlobal.removeImpl(this);
166             }
167         }
168 
setSplashScreenTheme(@tyleRes int themeId)169         public void setSplashScreenTheme(@StyleRes int themeId) {
170             if (mActivityToken == null) {
171                 Log.w(TAG, "Couldn't persist the starting theme. This instance is not an Activity");
172                 return;
173             }
174 
175             Activity activity = ActivityThread.currentActivityThread().getActivity(
176                     mActivityToken);
177             if (activity == null) {
178                 return;
179             }
180             String themeName = themeId != Resources.ID_NULL
181                     ? activity.getResources().getResourceName(themeId) : null;
182 
183             try {
184                 AppGlobals.getPackageManager().setSplashScreenTheme(
185                         activity.getComponentName().getPackageName(),
186                         themeName, activity.getUserId());
187             } catch (RemoteException e) {
188                 Log.w(TAG, "Couldn't persist the starting theme", e);
189             }
190         }
191     }
192 
193     /**
194      * This class is only used internally to manage the activities for this process.
195      *
196      * @hide
197      */
198     class SplashScreenManagerGlobal {
199         private static final String TAG = SplashScreen.class.getSimpleName();
200         private final Object mGlobalLock = new Object();
201         private final ArrayList<SplashScreenImpl> mImpls = new ArrayList<>();
202 
SplashScreenManagerGlobal()203         private SplashScreenManagerGlobal() {
204             ActivityThread.currentActivityThread().registerSplashScreenManager(this);
205         }
206 
getInstance()207         public static SplashScreenManagerGlobal getInstance() {
208             return sInstance.get();
209         }
210 
211         private static final Singleton<SplashScreenManagerGlobal> sInstance =
212                 new Singleton<SplashScreenManagerGlobal>() {
213                     @Override
214                     protected SplashScreenManagerGlobal create() {
215                         return new SplashScreenManagerGlobal();
216                     }
217                 };
218 
addImpl(SplashScreenImpl impl)219         private void addImpl(SplashScreenImpl impl) {
220             synchronized (mGlobalLock) {
221                 mImpls.add(impl);
222             }
223         }
224 
removeImpl(SplashScreenImpl impl)225         private void removeImpl(SplashScreenImpl impl) {
226             synchronized (mGlobalLock) {
227                 mImpls.remove(impl);
228             }
229         }
230 
findImpl(IBinder token)231         private SplashScreenImpl findImpl(IBinder token) {
232             synchronized (mGlobalLock) {
233                 for (SplashScreenImpl impl : mImpls) {
234                     if (impl.mActivityToken == token) {
235                         return impl;
236                     }
237                 }
238             }
239             return null;
240         }
241 
tokenDestroyed(IBinder token)242         public void tokenDestroyed(IBinder token) {
243             synchronized (mGlobalLock) {
244                 final SplashScreenImpl impl = findImpl(token);
245                 if (impl != null) {
246                     removeImpl(impl);
247                 }
248             }
249         }
250 
handOverSplashScreenView(@onNull IBinder token, @NonNull SplashScreenView splashScreenView)251         public void handOverSplashScreenView(@NonNull IBinder token,
252                 @NonNull SplashScreenView splashScreenView) {
253             dispatchOnExitAnimation(token, splashScreenView);
254         }
255 
dispatchOnExitAnimation(IBinder token, SplashScreenView view)256         private void dispatchOnExitAnimation(IBinder token, SplashScreenView view) {
257             synchronized (mGlobalLock) {
258                 final SplashScreenImpl impl = findImpl(token);
259                 if (impl == null) {
260                     return;
261                 }
262                 if (impl.mExitAnimationListener == null) {
263                     Slog.e(TAG, "cannot dispatch onExitAnimation to listener " + token);
264                     return;
265                 }
266                 impl.mExitAnimationListener.onSplashScreenExit(view);
267             }
268         }
269 
containsExitListener(IBinder token)270         public boolean containsExitListener(IBinder token) {
271             synchronized (mGlobalLock) {
272                 final SplashScreenImpl impl = findImpl(token);
273                 return impl != null && impl.mExitAnimationListener != null;
274             }
275         }
276     }
277 }
278