• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2014 The Chromium Authors. All rights reserved.
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.app.Activity;
8 import android.app.Application;
9 import android.app.Application.ActivityLifecycleCallbacks;
10 import android.os.Bundle;
11 
12 import org.chromium.base.annotations.CalledByNative;
13 import org.chromium.base.annotations.JNINamespace;
14 import org.chromium.base.annotations.MainDex;
15 
16 import java.lang.ref.WeakReference;
17 import java.util.ArrayList;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.concurrent.ConcurrentHashMap;
21 
22 /**
23  * Provides information about the current activity's status, and a way
24  * to register / unregister listeners for state changes.
25  */
26 @JNINamespace("base::android")
27 @MainDex
28 public class ApplicationStatus {
29     private static class ActivityInfo {
30         private int mStatus = ActivityState.DESTROYED;
31         private ObserverList<ActivityStateListener> mListeners =
32                 new ObserverList<ActivityStateListener>();
33 
34         /**
35          * @return The current {@link ActivityState} of the activity.
36          */
getStatus()37         public int getStatus() {
38             return mStatus;
39         }
40 
41         /**
42          * @param status The new {@link ActivityState} of the activity.
43          */
setStatus(int status)44         public void setStatus(int status) {
45             mStatus = status;
46         }
47 
48         /**
49          * @return A list of {@link ActivityStateListener}s listening to this activity.
50          */
getListeners()51         public ObserverList<ActivityStateListener> getListeners() {
52             return mListeners;
53         }
54     }
55 
56     private static Object sCachedApplicationStateLock = new Object();
57     private static Integer sCachedApplicationState;
58 
59     /** Last activity that was shown (or null if none or it was destroyed). */
60     private static Activity sActivity;
61 
62     /** A lazily initialized listener that forwards application state changes to native. */
63     private static ApplicationStateListener sNativeApplicationStateListener;
64 
65     /**
66      * A map of which observers listen to state changes from which {@link Activity}.
67      */
68     private static final Map<Activity, ActivityInfo> sActivityInfo =
69             new ConcurrentHashMap<Activity, ActivityInfo>();
70 
71     /**
72      * A list of observers to be notified when any {@link Activity} has a state change.
73      */
74     private static final ObserverList<ActivityStateListener> sGeneralActivityStateListeners =
75             new ObserverList<ActivityStateListener>();
76 
77     /**
78      * A list of observers to be notified when the visibility state of this {@link Application}
79      * changes.  See {@link #getStateForApplication()}.
80      */
81     private static final ObserverList<ApplicationStateListener> sApplicationStateListeners =
82             new ObserverList<ApplicationStateListener>();
83 
84     /**
85      * Interface to be implemented by listeners.
86      */
87     public interface ApplicationStateListener {
88         /**
89          * Called when the application's state changes.
90          * @param newState The application state.
91          */
onApplicationStateChange(int newState)92         public void onApplicationStateChange(int newState);
93     }
94 
95     /**
96      * Interface to be implemented by listeners.
97      */
98     public interface ActivityStateListener {
99         /**
100          * Called when the activity's state changes.
101          * @param activity The activity that had a state change.
102          * @param newState New activity state.
103          */
onActivityStateChange(Activity activity, int newState)104         public void onActivityStateChange(Activity activity, int newState);
105     }
106 
ApplicationStatus()107     private ApplicationStatus() {}
108 
109     /**
110      * Initializes the activity status for a specified application.
111      *
112      * @param application The application whose status you wish to monitor.
113      */
initialize(BaseChromiumApplication application)114     public static void initialize(BaseChromiumApplication application) {
115         application.registerWindowFocusChangedListener(
116                 new BaseChromiumApplication.WindowFocusChangedListener() {
117                     @Override
118                     public void onWindowFocusChanged(Activity activity, boolean hasFocus) {
119                         if (!hasFocus || activity == sActivity) return;
120 
121                         int state = getStateForActivity(activity);
122 
123                         if (state != ActivityState.DESTROYED && state != ActivityState.STOPPED) {
124                             sActivity = activity;
125                         }
126 
127                         // TODO(dtrainor): Notify of active activity change?
128                     }
129                 });
130 
131         application.registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
132             @Override
133             public void onActivityCreated(final Activity activity, Bundle savedInstanceState) {
134                 onStateChange(activity, ActivityState.CREATED);
135             }
136 
137             @Override
138             public void onActivityDestroyed(Activity activity) {
139                 onStateChange(activity, ActivityState.DESTROYED);
140             }
141 
142             @Override
143             public void onActivityPaused(Activity activity) {
144                 onStateChange(activity, ActivityState.PAUSED);
145             }
146 
147             @Override
148             public void onActivityResumed(Activity activity) {
149                 onStateChange(activity, ActivityState.RESUMED);
150             }
151 
152             @Override
153             public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
154 
155             @Override
156             public void onActivityStarted(Activity activity) {
157                 onStateChange(activity, ActivityState.STARTED);
158             }
159 
160             @Override
161             public void onActivityStopped(Activity activity) {
162                 onStateChange(activity, ActivityState.STOPPED);
163             }
164         });
165     }
166 
167     /**
168      * Must be called by the main activity when it changes state.
169      *
170      * @param activity Current activity.
171      * @param newState New state value.
172      */
onStateChange(Activity activity, int newState)173     private static void onStateChange(Activity activity, int newState) {
174         if (activity == null) throw new IllegalArgumentException("null activity is not supported");
175 
176         if (sActivity == null
177                 || newState == ActivityState.CREATED
178                 || newState == ActivityState.RESUMED
179                 || newState == ActivityState.STARTED) {
180             sActivity = activity;
181         }
182 
183         int oldApplicationState = getStateForApplication();
184 
185         if (newState == ActivityState.CREATED) {
186             assert !sActivityInfo.containsKey(activity);
187             sActivityInfo.put(activity, new ActivityInfo());
188         }
189 
190         // Invalidate the cached application state.
191         synchronized (sCachedApplicationStateLock) {
192             sCachedApplicationState = null;
193         }
194 
195         ActivityInfo info = sActivityInfo.get(activity);
196         info.setStatus(newState);
197 
198         // Notify all state observers that are specifically listening to this activity.
199         for (ActivityStateListener listener : info.getListeners()) {
200             listener.onActivityStateChange(activity, newState);
201         }
202 
203         // Notify all state observers that are listening globally for all activity state
204         // changes.
205         for (ActivityStateListener listener : sGeneralActivityStateListeners) {
206             listener.onActivityStateChange(activity, newState);
207         }
208 
209         int applicationState = getStateForApplication();
210         if (applicationState != oldApplicationState) {
211             for (ApplicationStateListener listener : sApplicationStateListeners) {
212                 listener.onApplicationStateChange(applicationState);
213             }
214         }
215 
216         if (newState == ActivityState.DESTROYED) {
217             sActivityInfo.remove(activity);
218             if (activity == sActivity) sActivity = null;
219         }
220     }
221 
222     /**
223      * Testing method to update the state of the specified activity.
224      */
225     @VisibleForTesting
onStateChangeForTesting(Activity activity, int newState)226     public static void onStateChangeForTesting(Activity activity, int newState) {
227         onStateChange(activity, newState);
228     }
229 
230     /**
231      * @return The most recent focused {@link Activity} tracked by this class.  Being focused means
232      *         out of all the activities tracked here, it has most recently gained window focus.
233      */
getLastTrackedFocusedActivity()234     public static Activity getLastTrackedFocusedActivity() {
235         return sActivity;
236     }
237 
238     /**
239      * @return A {@link List} of all non-destroyed {@link Activity}s.
240      */
getRunningActivities()241     public static List<WeakReference<Activity>> getRunningActivities() {
242         List<WeakReference<Activity>> activities = new ArrayList<WeakReference<Activity>>();
243         for (Activity activity : sActivityInfo.keySet()) {
244             activities.add(new WeakReference<Activity>(activity));
245         }
246         return activities;
247     }
248 
249     /**
250      * Query the state for a given activity.  If the activity is not being tracked, this will
251      * return {@link ActivityState#DESTROYED}.
252      *
253      * <p>
254      * Please note that Chrome can have multiple activities running simultaneously.  Please also
255      * look at {@link #getStateForApplication()} for more details.
256      *
257      * <p>
258      * When relying on this method, be familiar with the expected life cycle state
259      * transitions:
260      * <a href="http://developer.android.com/guide/components/activities.html#Lifecycle">
261      *   Activity Lifecycle
262      * </a>
263      *
264      * <p>
265      * During activity transitions (activity B launching in front of activity A), A will completely
266      * paused before the creation of activity B begins.
267      *
268      * <p>
269      * A basic flow for activity A starting, followed by activity B being opened and then closed:
270      * <ul>
271      *   <li> -- Starting Activity A --
272      *   <li> Activity A - ActivityState.CREATED
273      *   <li> Activity A - ActivityState.STARTED
274      *   <li> Activity A - ActivityState.RESUMED
275      *   <li> -- Starting Activity B --
276      *   <li> Activity A - ActivityState.PAUSED
277      *   <li> Activity B - ActivityState.CREATED
278      *   <li> Activity B - ActivityState.STARTED
279      *   <li> Activity B - ActivityState.RESUMED
280      *   <li> Activity A - ActivityState.STOPPED
281      *   <li> -- Closing Activity B, Activity A regaining focus --
282      *   <li> Activity B - ActivityState.PAUSED
283      *   <li> Activity A - ActivityState.STARTED
284      *   <li> Activity A - ActivityState.RESUMED
285      *   <li> Activity B - ActivityState.STOPPED
286      *   <li> Activity B - ActivityState.DESTROYED
287      * </ul>
288      *
289      * @param activity The activity whose state is to be returned.
290      * @return The state of the specified activity (see {@link ActivityState}).
291      */
getStateForActivity(Activity activity)292     public static int getStateForActivity(Activity activity) {
293         ActivityInfo info = sActivityInfo.get(activity);
294         return info != null ? info.getStatus() : ActivityState.DESTROYED;
295     }
296 
297     /**
298      * @return The state of the application (see {@link ApplicationState}).
299      */
300     @CalledByNative
getStateForApplication()301     public static int getStateForApplication() {
302         synchronized (sCachedApplicationStateLock) {
303             if (sCachedApplicationState == null) {
304                 sCachedApplicationState = determineApplicationState();
305             }
306             return sCachedApplicationState.intValue();
307         }
308     }
309 
310     /**
311      * Checks whether or not any Activity in this Application is visible to the user.  Note that
312      * this includes the PAUSED state, which can happen when the Activity is temporarily covered
313      * by another Activity's Fragment (e.g.).
314      * @return Whether any Activity under this Application is visible.
315      */
hasVisibleActivities()316     public static boolean hasVisibleActivities() {
317         int state = getStateForApplication();
318         return state == ApplicationState.HAS_RUNNING_ACTIVITIES
319                 || state == ApplicationState.HAS_PAUSED_ACTIVITIES;
320     }
321 
322     /**
323      * Checks to see if there are any active Activity instances being watched by ApplicationStatus.
324      * @return True if all Activities have been destroyed.
325      */
isEveryActivityDestroyed()326     public static boolean isEveryActivityDestroyed() {
327         return sActivityInfo.isEmpty();
328     }
329 
330     /**
331      * Registers the given listener to receive state changes for all activities.
332      * @param listener Listener to receive state changes.
333      */
registerStateListenerForAllActivities(ActivityStateListener listener)334     public static void registerStateListenerForAllActivities(ActivityStateListener listener) {
335         sGeneralActivityStateListeners.addObserver(listener);
336     }
337 
338     /**
339      * Registers the given listener to receive state changes for {@code activity}.  After a call to
340      * {@link ActivityStateListener#onActivityStateChange(Activity, int)} with
341      * {@link ActivityState#DESTROYED} all listeners associated with that particular
342      * {@link Activity} are removed.
343      * @param listener Listener to receive state changes.
344      * @param activity Activity to track or {@code null} to track all activities.
345      */
registerStateListenerForActivity(ActivityStateListener listener, Activity activity)346     public static void registerStateListenerForActivity(ActivityStateListener listener,
347             Activity activity) {
348         assert activity != null;
349 
350         ActivityInfo info = sActivityInfo.get(activity);
351         assert info != null && info.getStatus() != ActivityState.DESTROYED;
352         info.getListeners().addObserver(listener);
353     }
354 
355     /**
356      * Unregisters the given listener from receiving activity state changes.
357      * @param listener Listener that doesn't want to receive state changes.
358      */
unregisterActivityStateListener(ActivityStateListener listener)359     public static void unregisterActivityStateListener(ActivityStateListener listener) {
360         sGeneralActivityStateListeners.removeObserver(listener);
361 
362         // Loop through all observer lists for all activities and remove the listener.
363         for (ActivityInfo info : sActivityInfo.values()) {
364             info.getListeners().removeObserver(listener);
365         }
366     }
367 
368     /**
369      * Registers the given listener to receive state changes for the application.
370      * @param listener Listener to receive state state changes.
371      */
registerApplicationStateListener(ApplicationStateListener listener)372     public static void registerApplicationStateListener(ApplicationStateListener listener) {
373         sApplicationStateListeners.addObserver(listener);
374     }
375 
376     /**
377      * Unregisters the given listener from receiving state changes.
378      * @param listener Listener that doesn't want to receive state changes.
379      */
unregisterApplicationStateListener(ApplicationStateListener listener)380     public static void unregisterApplicationStateListener(ApplicationStateListener listener) {
381         sApplicationStateListeners.removeObserver(listener);
382     }
383 
384     /**
385      * Robolectric JUnit tests create a new application between each test, while all the context
386      * in static classes isn't reset. This function allows to reset the application status to avoid
387      * being in a dirty state.
388      */
destroyForJUnitTests()389     public static void destroyForJUnitTests() {
390         sApplicationStateListeners.clear();
391         sGeneralActivityStateListeners.clear();
392         sActivityInfo.clear();
393         synchronized (sCachedApplicationStateLock) {
394             sCachedApplicationState = null;
395         }
396         sActivity = null;
397         sNativeApplicationStateListener = null;
398     }
399 
400     /**
401      * Registers the single thread-safe native activity status listener.
402      * This handles the case where the caller is not on the main thread.
403      * Note that this is used by a leaky singleton object from the native
404      * side, hence lifecycle management is greatly simplified.
405      */
406     @CalledByNative
registerThreadSafeNativeApplicationStateListener()407     private static void registerThreadSafeNativeApplicationStateListener() {
408         ThreadUtils.runOnUiThread(new Runnable() {
409             @Override
410             public void run() {
411                 if (sNativeApplicationStateListener != null) return;
412 
413                 sNativeApplicationStateListener = new ApplicationStateListener() {
414                     @Override
415                     public void onApplicationStateChange(int newState) {
416                         nativeOnApplicationStateChange(newState);
417                     }
418                 };
419                 registerApplicationStateListener(sNativeApplicationStateListener);
420             }
421         });
422     }
423 
424     /**
425      * Determines the current application state as defined by {@link ApplicationState}.  This will
426      * loop over all the activities and check their state to determine what the general application
427      * state should be.
428      * @return HAS_RUNNING_ACTIVITIES if any activity is not paused, stopped, or destroyed.
429      *         HAS_PAUSED_ACTIVITIES if none are running and one is paused.
430      *         HAS_STOPPED_ACTIVITIES if none are running/paused and one is stopped.
431      *         HAS_DESTROYED_ACTIVITIES if none are running/paused/stopped.
432      */
determineApplicationState()433     private static int determineApplicationState() {
434         boolean hasPausedActivity = false;
435         boolean hasStoppedActivity = false;
436 
437         for (ActivityInfo info : sActivityInfo.values()) {
438             int state = info.getStatus();
439             if (state != ActivityState.PAUSED
440                     && state != ActivityState.STOPPED
441                     && state != ActivityState.DESTROYED) {
442                 return ApplicationState.HAS_RUNNING_ACTIVITIES;
443             } else if (state == ActivityState.PAUSED) {
444                 hasPausedActivity = true;
445             } else if (state == ActivityState.STOPPED) {
446                 hasStoppedActivity = true;
447             }
448         }
449 
450         if (hasPausedActivity) return ApplicationState.HAS_PAUSED_ACTIVITIES;
451         if (hasStoppedActivity) return ApplicationState.HAS_STOPPED_ACTIVITIES;
452         return ApplicationState.HAS_DESTROYED_ACTIVITIES;
453     }
454 
455     // Called to notify the native side of state changes.
456     // IMPORTANT: This is always called on the main thread!
nativeOnApplicationStateChange(int newState)457     private static native void nativeOnApplicationStateChange(int newState);
458 }
459