• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2014 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.base;
6 
7 import android.annotation.SuppressLint;
8 import android.app.Activity;
9 import android.app.Application;
10 import android.app.Application.ActivityLifecycleCallbacks;
11 import android.os.Bundle;
12 import android.view.Window;
13 
14 import androidx.annotation.AnyThread;
15 import androidx.annotation.MainThread;
16 import androidx.annotation.Nullable;
17 import androidx.annotation.VisibleForTesting;
18 
19 import org.chromium.base.annotations.CalledByNative;
20 import org.chromium.base.annotations.JNINamespace;
21 import org.chromium.base.annotations.NativeMethods;
22 import org.chromium.build.BuildConfig;
23 
24 import java.lang.reflect.Field;
25 import java.lang.reflect.InvocationHandler;
26 import java.lang.reflect.InvocationTargetException;
27 import java.lang.reflect.Method;
28 import java.lang.reflect.Proxy;
29 import java.util.ArrayList;
30 import java.util.Collections;
31 import java.util.HashMap;
32 import java.util.HashSet;
33 import java.util.List;
34 import java.util.Map;
35 
36 import javax.annotation.concurrent.GuardedBy;
37 
38 /**
39  * Provides information about the current activity's status, and a way
40  * to register / unregister listeners for state changes.
41  * TODO(https://crbug.com/470582): ApplicationStatus will not work on WebView/WebLayer, and
42  * should be moved out of base and into //chrome. It should not be relied upon for //components.
43  */
44 @JNINamespace("base::android")
45 public class ApplicationStatus {
46     private static final String TOOLBAR_CALLBACK_WRAPPER_CLASS =
47             "androidx.appcompat.app.ToolbarActionBar$ToolbarCallbackWrapper";
48 
49     private static class ActivityInfo {
50         private int mStatus = ActivityState.DESTROYED;
51         private ObserverList<ActivityStateListener> mListeners = new ObserverList<>();
52 
53         /**
54          * @return The current {@link ActivityState} of the activity.
55          */
56         @ActivityState
getStatus()57         public int getStatus() {
58             return mStatus;
59         }
60 
61         /**
62          * @param status The new {@link ActivityState} of the activity.
63          */
setStatus(@ctivityState int status)64         public void setStatus(@ActivityState int status) {
65             mStatus = status;
66         }
67 
68         /**
69          * @return A list of {@link ActivityStateListener}s listening to this activity.
70          */
getListeners()71         public ObserverList<ActivityStateListener> getListeners() {
72             return mListeners;
73         }
74     }
75 
76     /**
77      * A map of which observers listen to state changes from which {@link Activity}.
78      */
79     private static final Map<Activity, ActivityInfo> sActivityInfo =
80             Collections.synchronizedMap(new HashMap<Activity, ActivityInfo>());
81 
82     @SuppressLint("SupportAnnotationUsage")
83     @ApplicationState
84     @GuardedBy("sActivityInfo")
85     // The getStateForApplication() historically returned ApplicationState.HAS_DESTROYED_ACTIVITIES
86     // when no activity has been observed.
87     private static int sCurrentApplicationState = ApplicationState.UNKNOWN;
88 
89     /**
90      * Last activity that was shown (or null if none or it was destroyed).
91      */
92     @SuppressLint("StaticFieldLeak")
93     private static Activity sActivity;
94 
95     /**
96      * A lazily initialized listener that forwards application state changes to native.
97      */
98     private static ApplicationStateListener sNativeApplicationStateListener;
99 
100     /**
101      * A list of observers to be notified when any {@link Activity} has a state change.
102      */
103     private static ObserverList<ActivityStateListener> sGeneralActivityStateListeners;
104 
105     /**
106      * A list of observers to be notified when the visibility state of this {@link Application}
107      * changes.  See {@link #getStateForApplication()}.
108      */
109     private static ObserverList<ApplicationStateListener> sApplicationStateListeners;
110 
111     /**
112      * A list of observers to be notified when the window focus changes.
113      * See {@link #registerWindowFocusChangedListener}.
114      */
115     private static ObserverList<WindowFocusChangedListener> sWindowFocusListeners;
116 
117     /**
118      * A list of observers to be notified when the visibility of any task changes.
119      */
120     private static ObserverList<TaskVisibilityListener> sTaskVisibilityListeners;
121 
122     /**
123      * Interface to be implemented by listeners.
124      */
125     public interface ApplicationStateListener {
126         /**
127          * Called when the application's state changes.
128          *
129          * @param newState The application state.
130          */
onApplicationStateChange(@pplicationState int newState)131         void onApplicationStateChange(@ApplicationState int newState);
132     }
133 
134     /**
135      * Interface to be implemented by listeners.
136      */
137     public interface ActivityStateListener {
138         /**
139          * Called when the activity's state changes.
140          *
141          * @param activity The activity that had a state change.
142          * @param newState New activity state.
143          */
onActivityStateChange(Activity activity, @ActivityState int newState)144         void onActivityStateChange(Activity activity, @ActivityState int newState);
145     }
146 
147     /**
148      * Interface to be implemented by listeners for window focus events.
149      */
150     public interface WindowFocusChangedListener {
151         /**
152          * Called when the window focus changes for {@code activity}.
153          *
154          * @param activity The {@link Activity} that has a window focus changed event.
155          * @param hasFocus Whether or not {@code activity} gained or lost focus.
156          */
onWindowFocusChanged(Activity activity, boolean hasFocus)157         public void onWindowFocusChanged(Activity activity, boolean hasFocus);
158     }
159 
160     /**
161      * Interface to be implemented by listeners for task visibility changes.
162      */
163     public interface TaskVisibilityListener {
164         /**
165          * Called when the visibility of a task changes.
166          *
167          * @param taskId    The unique Id of the task that changed visibility.
168          * @param isVisible The new visibility state of the task.
169          */
onTaskVisibilityChanged(int taskId, boolean isVisible)170         void onTaskVisibilityChanged(int taskId, boolean isVisible);
171     }
172 
ApplicationStatus()173     private ApplicationStatus() {}
174 
175     /**
176      * Registers a listener to receive window focus updates on activities in this application.
177      *
178      * @param listener Listener to receive window focus events.
179      */
180     @MainThread
registerWindowFocusChangedListener(WindowFocusChangedListener listener)181     public static void registerWindowFocusChangedListener(WindowFocusChangedListener listener) {
182         assert isInitialized();
183         if (sWindowFocusListeners == null) sWindowFocusListeners = new ObserverList<>();
184         sWindowFocusListeners.addObserver(listener);
185     }
186 
187     /**
188      * Unregisters a listener from receiving window focus updates on activities in this application.
189      *
190      * @param listener Listener that doesn't want to receive window focus events.
191      */
192     @MainThread
unregisterWindowFocusChangedListener(WindowFocusChangedListener listener)193     public static void unregisterWindowFocusChangedListener(WindowFocusChangedListener listener) {
194         if (sWindowFocusListeners == null) return;
195         sWindowFocusListeners.removeObserver(listener);
196     }
197 
198     /**
199      * Register a listener to receive task visibility updates.
200      *
201      * @param listener Listener to receive task visibility events.
202      */
203     @MainThread
registerTaskVisibilityListener(TaskVisibilityListener listener)204     public static void registerTaskVisibilityListener(TaskVisibilityListener listener) {
205         assert isInitialized();
206         if (sTaskVisibilityListeners == null) sTaskVisibilityListeners = new ObserverList<>();
207         sTaskVisibilityListeners.addObserver(listener);
208     }
209 
210     /**
211      * Unregisters a listener from receiving task visibility updates.
212      *
213      * @param listener Listener that doesn't want to receive task visibility events.
214      */
215     @MainThread
unregisterTaskVisibilityListener(TaskVisibilityListener listener)216     public static void unregisterTaskVisibilityListener(TaskVisibilityListener listener) {
217         if (sTaskVisibilityListeners == null) return;
218         sTaskVisibilityListeners.removeObserver(listener);
219     }
220 
221     /**
222      * Intercepts calls to an existing Window.Callback. Most invocations are passed on directly
223      * to the composed Window.Callback but enables intercepting/manipulating others.
224      * <p>
225      * This is used to relay window focus changes throughout the app and remedy a bug in the
226      * appcompat library.
227      */
228     @VisibleForTesting
229     static class WindowCallbackProxy implements InvocationHandler {
230         private final Window.Callback mCallback;
231         private final Activity mActivity;
232 
WindowCallbackProxy(Activity activity, Window.Callback callback)233         public WindowCallbackProxy(Activity activity, Window.Callback callback) {
234             mCallback = callback;
235             mActivity = activity;
236         }
237 
238         @Override
invoke(Object proxy, Method method, Object[] args)239         public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
240             if (method.getName().equals("onWindowFocusChanged") && args.length == 1
241                     && args[0] instanceof Boolean) {
242                 onWindowFocusChanged((boolean) args[0]);
243                 return null;
244             } else {
245                 try {
246                     return method.invoke(mCallback, args);
247                 } catch (InvocationTargetException e) {
248                     // Special-case for when a method is not defined on the underlying
249                     // Window.Callback object. Because we're using a Proxy to forward all method
250                     // calls, this breaks the Android framework's handling for apps built against
251                     // an older SDK. The framework expects an AbstractMethodError but due to
252                     // reflection it becomes wrapped inside an InvocationTargetException. Undo the
253                     // wrapping to signal the framework accordingly.
254                     if (e.getCause() instanceof AbstractMethodError) {
255                         throw e.getCause();
256                     }
257                     throw e;
258                 }
259             }
260         }
261 
onWindowFocusChanged(boolean hasFocus)262         public void onWindowFocusChanged(boolean hasFocus) {
263             mCallback.onWindowFocusChanged(hasFocus);
264 
265             if (sWindowFocusListeners != null) {
266                 for (WindowFocusChangedListener listener : sWindowFocusListeners) {
267                     listener.onWindowFocusChanged(mActivity, hasFocus);
268                 }
269             }
270         }
271     }
272 
isInitialized()273     public static boolean isInitialized() {
274         synchronized (sActivityInfo) {
275             return sCurrentApplicationState != ApplicationState.UNKNOWN;
276         }
277     }
278 
279     /**
280      * Initializes the activity status for a specified application.
281      *
282      * @param application The application whose status you wish to monitor.
283      */
284     @MainThread
initialize(Application application)285     public static void initialize(Application application) {
286         assert !isInitialized();
287         synchronized (sActivityInfo) {
288             sCurrentApplicationState = ApplicationState.HAS_DESTROYED_ACTIVITIES;
289         }
290 
291         registerWindowFocusChangedListener(new WindowFocusChangedListener() {
292             @Override
293             public void onWindowFocusChanged(Activity activity, boolean hasFocus) {
294                 if (!hasFocus || activity == sActivity) return;
295 
296                 int state = getStateForActivity(activity);
297 
298                 if (state != ActivityState.DESTROYED && state != ActivityState.STOPPED) {
299                     sActivity = activity;
300                 }
301 
302                 // TODO(dtrainor): Notify of active activity change?
303             }
304         });
305 
306         application.registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
307             @Override
308             public void onActivityCreated(final Activity activity, Bundle savedInstanceState) {
309                 onStateChange(activity, ActivityState.CREATED);
310                 Window.Callback callback = activity.getWindow().getCallback();
311                 activity.getWindow().setCallback(createWindowCallbackProxy(activity, callback));
312             }
313 
314             @Override
315             public void onActivityDestroyed(Activity activity) {
316                 onStateChange(activity, ActivityState.DESTROYED);
317                 checkCallback(activity);
318             }
319 
320             @Override
321             public void onActivityPaused(Activity activity) {
322                 onStateChange(activity, ActivityState.PAUSED);
323                 checkCallback(activity);
324             }
325 
326             @Override
327             public void onActivityResumed(Activity activity) {
328                 onStateChange(activity, ActivityState.RESUMED);
329                 checkCallback(activity);
330             }
331 
332             @Override
333             public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
334                 checkCallback(activity);
335             }
336 
337             @Override
338             public void onActivityStarted(Activity activity) {
339                 onStateChange(activity, ActivityState.STARTED);
340                 checkCallback(activity);
341             }
342 
343             @Override
344             public void onActivityStopped(Activity activity) {
345                 onStateChange(activity, ActivityState.STOPPED);
346                 checkCallback(activity);
347             }
348 
349             private void checkCallback(Activity activity) {
350                 if (BuildConfig.ENABLE_ASSERTS) {
351                     assert reachesWindowCallback(activity.getWindow().getCallback());
352                 }
353             }
354         });
355     }
356 
357     @VisibleForTesting
createWindowCallbackProxy(Activity activity, Window.Callback callback)358     static Window.Callback createWindowCallbackProxy(Activity activity, Window.Callback callback) {
359         return (Window.Callback) Proxy.newProxyInstance(Window.Callback.class.getClassLoader(),
360                 new Class[] {Window.Callback.class},
361                 new ApplicationStatus.WindowCallbackProxy(activity, callback));
362     }
363 
364     /**
365      * Tries to trace down to our WindowCallbackProxy from the given callback.
366      * Since the callback can be overwritten by embedder code we try to ensure
367      * that there at least seem to be a reference back to our callback by
368      * checking the declared fields of the given callback using reflection.
369      */
370     @VisibleForTesting
reachesWindowCallback(@ullable Window.Callback callback)371     static boolean reachesWindowCallback(@Nullable Window.Callback callback) {
372         if (callback == null) return false;
373         if (callback.getClass().getName().equals(TOOLBAR_CALLBACK_WRAPPER_CLASS)) {
374             // We're actually not going to get called, see AndroidX report here:
375             // https://issuetracker.google.com/issues/155165145.
376             // But this was accepted in the old code as well so mimic that until
377             // AndroidX is fixed and updated.
378             return true;
379         }
380         if (Proxy.isProxyClass(callback.getClass())) {
381             return Proxy.getInvocationHandler(callback)
382                            instanceof ApplicationStatus.WindowCallbackProxy;
383         }
384         for (Class<?> c = callback.getClass(); c != Object.class; c = c.getSuperclass()) {
385             for (Field f : c.getDeclaredFields()) {
386                 if (f.getType().isAssignableFrom(Window.Callback.class)) {
387                     boolean isAccessible = f.isAccessible();
388                     f.setAccessible(true);
389                     Window.Callback fieldCb;
390                     try {
391                         fieldCb = (Window.Callback) f.get(callback);
392                     } catch (IllegalAccessException ex) {
393                         continue;
394                     } finally {
395                         f.setAccessible(isAccessible);
396                     }
397                     if (reachesWindowCallback(fieldCb)) {
398                         return true;
399                     }
400                 }
401             }
402         }
403         return false;
404     }
405 
406     /**
407      * Must be called by the main activity when it changes state.
408      *
409      * @param activity Current activity.
410      * @param newState New state value.
411      */
onStateChange(Activity activity, @ActivityState int newState)412     private static void onStateChange(Activity activity, @ActivityState int newState) {
413         if (activity == null) throw new IllegalArgumentException("null activity is not supported");
414 
415         if (sActivity == null
416                 || newState == ActivityState.CREATED
417                 || newState == ActivityState.RESUMED
418                 || newState == ActivityState.STARTED) {
419             sActivity = activity;
420         }
421 
422         int oldApplicationState = getStateForApplication();
423         boolean oldTaskVisibility = isTaskVisible(activity.getTaskId());
424         ActivityInfo info;
425 
426         synchronized (sActivityInfo) {
427             if (newState == ActivityState.CREATED) {
428                 assert !sActivityInfo.containsKey(activity);
429                 sActivityInfo.put(activity, new ActivityInfo());
430             }
431 
432             info = sActivityInfo.get(activity);
433             info.setStatus(newState);
434 
435             // Remove before calling listeners so that isEveryActivityDestroyed() returns false when
436             // this was the last activity.
437             if (newState == ActivityState.DESTROYED) {
438                 sActivityInfo.remove(activity);
439                 if (activity == sActivity) sActivity = null;
440             }
441 
442             sCurrentApplicationState = determineApplicationStateLocked();
443         }
444 
445         // Notify all state observers that are specifically listening to this activity.
446         for (ActivityStateListener listener : info.getListeners()) {
447             listener.onActivityStateChange(activity, newState);
448         }
449 
450         // Notify all state observers that are listening globally for all activity state
451         // changes.
452         if (sGeneralActivityStateListeners != null) {
453             for (ActivityStateListener listener : sGeneralActivityStateListeners) {
454                 listener.onActivityStateChange(activity, newState);
455             }
456         }
457 
458         boolean taskVisibility = isTaskVisible(activity.getTaskId());
459         if (taskVisibility != oldTaskVisibility && sTaskVisibilityListeners != null) {
460             for (TaskVisibilityListener listener : sTaskVisibilityListeners) {
461                 listener.onTaskVisibilityChanged(activity.getTaskId(), taskVisibility);
462             }
463         }
464 
465         int applicationState = getStateForApplication();
466         if (applicationState != oldApplicationState && sApplicationStateListeners != null) {
467             for (ApplicationStateListener listener : sApplicationStateListeners) {
468                 listener.onApplicationStateChange(applicationState);
469             }
470         }
471     }
472 
473     /**
474      * Testing method to update the state of the specified activity.
475      */
476     @VisibleForTesting
477     @MainThread
onStateChangeForTesting(Activity activity, int newState)478     public static void onStateChangeForTesting(Activity activity, int newState) {
479         onStateChange(activity, newState);
480     }
481 
482     /**
483      * @return The most recent focused {@link Activity} tracked by this class.  Being focused means
484      * out of all the activities tracked here, it has most recently gained window focus.
485      */
486     @MainThread
getLastTrackedFocusedActivity()487     public static Activity getLastTrackedFocusedActivity() {
488         return sActivity;
489     }
490 
491     /**
492      * @return A {@link List} of all non-destroyed {@link Activity}s.
493      */
494     @AnyThread
getRunningActivities()495     public static List<Activity> getRunningActivities() {
496         assert isInitialized();
497         synchronized (sActivityInfo) {
498             return new ArrayList<>(sActivityInfo.keySet());
499         }
500     }
501 
502     /**
503      * Query the state for a given activity.  If the activity is not being tracked, this will
504      * return {@link ActivityState#DESTROYED}.
505      *
506      * <p>
507      * Please note that Chrome can have multiple activities running simultaneously.  Please also
508      * look at {@link #getStateForApplication()} for more details.
509      *
510      * <p>
511      * When relying on this method, be familiar with the expected life cycle state
512      * transitions:
513      * <a href="http://developer.android.com/guide/components/activities.html#Lifecycle">
514      * Activity Lifecycle
515      * </a>
516      *
517      * <p>
518      * During activity transitions (activity B launching in front of activity A), A will completely
519      * paused before the creation of activity B begins.
520      *
521      * <p>
522      * A basic flow for activity A starting, followed by activity B being opened and then closed:
523      * <ul>
524      *   <li> -- Starting Activity A --
525      *   <li> Activity A - ActivityState.CREATED
526      *   <li> Activity A - ActivityState.STARTED
527      *   <li> Activity A - ActivityState.RESUMED
528      *   <li> -- Starting Activity B --
529      *   <li> Activity A - ActivityState.PAUSED
530      *   <li> Activity B - ActivityState.CREATED
531      *   <li> Activity B - ActivityState.STARTED
532      *   <li> Activity B - ActivityState.RESUMED
533      *   <li> Activity A - ActivityState.STOPPED
534      *   <li> -- Closing Activity B, Activity A regaining focus --
535      *   <li> Activity B - ActivityState.PAUSED
536      *   <li> Activity A - ActivityState.STARTED
537      *   <li> Activity A - ActivityState.RESUMED
538      *   <li> Activity B - ActivityState.STOPPED
539      *   <li> Activity B - ActivityState.DESTROYED
540      * </ul>
541      *
542      * @param activity The activity whose state is to be returned.
543      * @return The state of the specified activity (see {@link ActivityState}).
544      */
545     @ActivityState
546     @AnyThread
getStateForActivity(@ullable Activity activity)547     public static int getStateForActivity(@Nullable Activity activity) {
548         assert isInitialized();
549         if (activity == null) return ActivityState.DESTROYED;
550         ActivityInfo info = sActivityInfo.get(activity);
551         return info != null ? info.getStatus() : ActivityState.DESTROYED;
552     }
553 
554     /**
555      * @return The state of the application (see {@link ApplicationState}).
556      */
557     @AnyThread
558     @ApplicationState
559     @CalledByNative
getStateForApplication()560     public static int getStateForApplication() {
561         synchronized (sActivityInfo) {
562             return sCurrentApplicationState;
563         }
564     }
565 
566     /**
567      * Checks whether or not any Activity in this Application is visible to the user.  Note that
568      * this includes the PAUSED state, which can happen when the Activity is temporarily covered
569      * by another Activity's Fragment (e.g.).
570      *
571      * @return Whether any Activity under this Application is visible.
572      */
573     @AnyThread
574     @CalledByNative
hasVisibleActivities()575     public static boolean hasVisibleActivities() {
576         assert isInitialized();
577         int state = getStateForApplication();
578         return state == ApplicationState.HAS_RUNNING_ACTIVITIES
579                 || state == ApplicationState.HAS_PAUSED_ACTIVITIES;
580     }
581 
582     /**
583      * Checks to see if there are any active Activity instances being watched by ApplicationStatus.
584      *
585      * @return True if all Activities have been destroyed.
586      */
587     @AnyThread
isEveryActivityDestroyed()588     public static boolean isEveryActivityDestroyed() {
589         assert isInitialized();
590         return sActivityInfo.isEmpty();
591     }
592 
593     /**
594      * Returns the visibility of the task with the given taskId. A task is visible if any of its
595      * Activities are in RESUMED or PAUSED state.
596      *
597      * @param taskId The id of the task whose visibility needs to be checked.
598      * @return Whether the task is visible or not.
599      */
600     @AnyThread
isTaskVisible(int taskId)601     public static boolean isTaskVisible(int taskId) {
602         assert isInitialized();
603         for (Map.Entry<Activity, ActivityInfo> entry : sActivityInfo.entrySet()) {
604             if (entry.getKey().getTaskId() == taskId) {
605                 @ActivityState
606                 int state = entry.getValue().getStatus();
607                 if (state == ActivityState.RESUMED || state == ActivityState.PAUSED) {
608                     return true;
609                 }
610             }
611         }
612         return false;
613     }
614 
615     /**
616      * Registers the given listener to receive state changes for all activities.
617      *
618      * @param listener Listener to receive state changes.
619      */
620     @MainThread
registerStateListenerForAllActivities(ActivityStateListener listener)621     public static void registerStateListenerForAllActivities(ActivityStateListener listener) {
622         assert isInitialized();
623         if (sGeneralActivityStateListeners == null) {
624             sGeneralActivityStateListeners = new ObserverList<>();
625         }
626         sGeneralActivityStateListeners.addObserver(listener);
627     }
628 
629     /**
630      * Registers the given listener to receive state changes for {@code activity}.  After a call to
631      * {@link ActivityStateListener#onActivityStateChange(Activity, int)} with
632      * {@link ActivityState#DESTROYED} all listeners associated with that particular
633      * {@link Activity} are removed.
634      *
635      * @param listener Listener to receive state changes.
636      * @param activity Activity to track or {@code null} to track all activities.
637      */
638     @MainThread
639     @SuppressLint("NewApi")
registerStateListenerForActivity( ActivityStateListener listener, Activity activity)640     public static void registerStateListenerForActivity(
641             ActivityStateListener listener, Activity activity) {
642         assert isInitialized();
643         assert activity != null;
644 
645         ActivityInfo info = sActivityInfo.get(activity);
646         assert info.getStatus() != ActivityState.DESTROYED;
647         info.getListeners().addObserver(listener);
648     }
649 
650     /**
651      * Unregisters the given listener from receiving activity state changes.
652      *
653      * @param listener Listener that doesn't want to receive state changes.
654      */
655     @MainThread
unregisterActivityStateListener(ActivityStateListener listener)656     public static void unregisterActivityStateListener(ActivityStateListener listener) {
657         if (sGeneralActivityStateListeners != null) {
658             sGeneralActivityStateListeners.removeObserver(listener);
659         }
660 
661         // Loop through all observer lists for all activities and remove the listener.
662         synchronized (sActivityInfo) {
663             for (ActivityInfo info : sActivityInfo.values()) {
664                 info.getListeners().removeObserver(listener);
665             }
666         }
667     }
668 
669     /**
670      * Registers the given listener to receive state changes for the application.
671      *
672      * @param listener Listener to receive state state changes.
673      */
674     @MainThread
registerApplicationStateListener(ApplicationStateListener listener)675     public static void registerApplicationStateListener(ApplicationStateListener listener) {
676         if (sApplicationStateListeners == null) {
677             sApplicationStateListeners = new ObserverList<>();
678         }
679         sApplicationStateListeners.addObserver(listener);
680     }
681 
682     /**
683      * Unregisters the given listener from receiving state changes.
684      *
685      * @param listener Listener that doesn't want to receive state changes.
686      */
687     @MainThread
unregisterApplicationStateListener(ApplicationStateListener listener)688     public static void unregisterApplicationStateListener(ApplicationStateListener listener) {
689         if (sApplicationStateListeners == null) return;
690         sApplicationStateListeners.removeObserver(listener);
691     }
692 
693     /**
694      * Robolectric JUnit tests create a new application between each test, while all the context
695      * in static classes isn't reset. This function allows to reset the application status to avoid
696      * being in a dirty state.
697      */
698     @MainThread
destroyForJUnitTests()699     public static void destroyForJUnitTests() {
700         synchronized (sActivityInfo) {
701             if (sApplicationStateListeners != null) sApplicationStateListeners.clear();
702             if (sGeneralActivityStateListeners != null) sGeneralActivityStateListeners.clear();
703             if (sTaskVisibilityListeners != null) sTaskVisibilityListeners.clear();
704             sActivityInfo.clear();
705             if (sWindowFocusListeners != null) sWindowFocusListeners.clear();
706             sCurrentApplicationState = ApplicationState.UNKNOWN;
707             sActivity = null;
708             sNativeApplicationStateListener = null;
709         }
710     }
711 
712     /**
713      * Mark all Activities as destroyed to avoid side-effects in future test.
714      */
715     @MainThread
resetActivitiesForInstrumentationTests()716     public static void resetActivitiesForInstrumentationTests() {
717         assert ThreadUtils.runningOnUiThread();
718 
719         synchronized (sActivityInfo) {
720             // Copy the set to avoid concurrent modifications to the underlying set.
721             for (Activity activity : new HashSet<>(sActivityInfo.keySet())) {
722                 assert activity.getApplication()
723                         == null : "Real activities that are launched should be closed by test code "
724                                   + "and not rely on this cleanup of mocks.";
725                 onStateChangeForTesting(activity, ActivityState.DESTROYED);
726             }
727         }
728     }
729 
730     /**
731      * Registers the single thread-safe native activity status listener.
732      * This handles the case where the caller is not on the main thread.
733      * Note that this is used by a leaky singleton object from the native
734      * side, hence lifecycle management is greatly simplified.
735      */
736     @CalledByNative
registerThreadSafeNativeApplicationStateListener()737     private static void registerThreadSafeNativeApplicationStateListener() {
738         ThreadUtils.runOnUiThread(new Runnable() {
739             @Override
740             public void run() {
741                 if (sNativeApplicationStateListener != null) return;
742 
743                 sNativeApplicationStateListener = new ApplicationStateListener() {
744                     @Override
745                     public void onApplicationStateChange(int newState) {
746                         ApplicationStatusJni.get().onApplicationStateChange(newState);
747                     }
748                 };
749                 registerApplicationStateListener(sNativeApplicationStateListener);
750             }
751         });
752     }
753 
754     /**
755      * Determines the current application state as defined by {@link ApplicationState}.  This will
756      * loop over all the activities and check their state to determine what the general application
757      * state should be.
758      *
759      * @return HAS_RUNNING_ACTIVITIES if any activity is not paused, stopped, or destroyed.
760      * HAS_PAUSED_ACTIVITIES if none are running and one is paused.
761      * HAS_STOPPED_ACTIVITIES if none are running/paused and one is stopped.
762      * HAS_DESTROYED_ACTIVITIES if none are running/paused/stopped.
763      */
764     @ApplicationState
765     @GuardedBy("sActivityInfo")
determineApplicationStateLocked()766     private static int determineApplicationStateLocked() {
767         boolean hasPausedActivity = false;
768         boolean hasStoppedActivity = false;
769 
770         for (ActivityInfo info : sActivityInfo.values()) {
771             int state = info.getStatus();
772             if (state != ActivityState.PAUSED && state != ActivityState.STOPPED
773                     && state != ActivityState.DESTROYED) {
774                 return ApplicationState.HAS_RUNNING_ACTIVITIES;
775             } else if (state == ActivityState.PAUSED) {
776                 hasPausedActivity = true;
777             } else if (state == ActivityState.STOPPED) {
778                 hasStoppedActivity = true;
779             }
780         }
781 
782         if (hasPausedActivity) return ApplicationState.HAS_PAUSED_ACTIVITIES;
783         if (hasStoppedActivity) return ApplicationState.HAS_STOPPED_ACTIVITIES;
784         return ApplicationState.HAS_DESTROYED_ACTIVITIES;
785     }
786 
787     @NativeMethods
788     interface Natives {
789         // Called to notify the native side of state changes.
790         // IMPORTANT: This is always called on the main thread!
onApplicationStateChange(@pplicationState int newState)791         void onApplicationStateChange(@ApplicationState int newState);
792     }
793 }
794