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