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