• 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.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.support.annotation.Nullable;
13 import android.view.Window;
14 
15 import org.chromium.base.annotations.CalledByNative;
16 import org.chromium.base.annotations.JNINamespace;
17 
18 import java.lang.ref.WeakReference;
19 import java.lang.reflect.InvocationHandler;
20 import java.lang.reflect.InvocationTargetException;
21 import java.lang.reflect.Method;
22 import java.lang.reflect.Proxy;
23 import java.util.ArrayList;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.concurrent.ConcurrentHashMap;
27 
28 /**
29  * Provides information about the current activity's status, and a way
30  * to register / unregister listeners for state changes.
31  */
32 @JNINamespace("base::android")
33 public class ApplicationStatus {
34     private static final String TOOLBAR_CALLBACK_INTERNAL_WRAPPER_CLASS =
35             "android.support.v7.internal.app.ToolbarActionBar$ToolbarCallbackWrapper";
36     // In builds using the --use_unpublished_apis flag, the ToolbarActionBar class name does not
37     // include the "internal" package.
38     private static final String TOOLBAR_CALLBACK_WRAPPER_CLASS =
39             "android.support.v7.app.ToolbarActionBar$ToolbarCallbackWrapper";
40     private static final String WINDOW_PROFILER_CALLBACK =
41             "com.android.tools.profiler.support.event.WindowProfilerCallback";
42 
43     private static class ActivityInfo {
44         private int mStatus = ActivityState.DESTROYED;
45         private ObserverList<ActivityStateListener> mListeners = new ObserverList<>();
46 
47         /**
48          * @return The current {@link ActivityState} of the activity.
49          */
50         @ActivityState
getStatus()51         public int getStatus() {
52             return mStatus;
53         }
54 
55         /**
56          * @param status The new {@link ActivityState} of the activity.
57          */
setStatus(@ctivityState int status)58         public void setStatus(@ActivityState int status) {
59             mStatus = status;
60         }
61 
62         /**
63          * @return A list of {@link ActivityStateListener}s listening to this activity.
64          */
getListeners()65         public ObserverList<ActivityStateListener> getListeners() {
66             return mListeners;
67         }
68     }
69 
70     static {
71         // Chrome initializes this only for the main process. This assert aims to try and catch
72         // usages from GPU / renderers, while still allowing tests.
73         assert ContextUtils.isMainProcess()
74                 || ContextUtils.getProcessName().contains(":test")
75             : "Cannot use ApplicationState from process: "
ContextUtils.getProcessName()76                         + ContextUtils.getProcessName();
77     }
78 
79     private static final Object sCurrentApplicationStateLock = new Object();
80 
81     @SuppressLint("SupportAnnotationUsage")
82     @ApplicationState
83     // The getStateForApplication() historically returned ApplicationState.HAS_DESTROYED_ACTIVITIES
84     // when no activity has been observed.
85     private static Integer sCurrentApplicationState = ApplicationState.HAS_DESTROYED_ACTIVITIES;
86 
87     /** Last activity that was shown (or null if none or it was destroyed). */
88     @SuppressLint("StaticFieldLeak")
89     private static Activity sActivity;
90 
91     /** A lazily initialized listener that forwards application state changes to native. */
92     private static ApplicationStateListener sNativeApplicationStateListener;
93 
94     private static boolean sIsInitialized;
95 
96     /**
97      * A map of which observers listen to state changes from which {@link Activity}.
98      */
99     private static final Map<Activity, ActivityInfo> sActivityInfo = new ConcurrentHashMap<>();
100 
101     /**
102      * A list of observers to be notified when any {@link Activity} has a state change.
103      */
104     private static final ObserverList<ActivityStateListener> sGeneralActivityStateListeners =
105             new ObserverList<>();
106 
107     /**
108      * A list of observers to be notified when the visibility state of this {@link Application}
109      * changes.  See {@link #getStateForApplication()}.
110      */
111     private static final ObserverList<ApplicationStateListener> sApplicationStateListeners =
112             new ObserverList<>();
113 
114     /**
115      * A list of observers to be notified when the window focus changes.
116      * See {@link #registerWindowFocusChangedListener}.
117      */
118     private static final ObserverList<WindowFocusChangedListener> sWindowFocusListeners =
119             new ObserverList<>();
120 
121     /**
122      * Interface to be implemented by listeners.
123      */
124     public interface ApplicationStateListener {
125         /**
126          * Called when the application's state changes.
127          * @param newState The application state.
128          */
onApplicationStateChange(@pplicationState int newState)129         void onApplicationStateChange(@ApplicationState int newState);
130     }
131 
132     /**
133      * Interface to be implemented by listeners.
134      */
135     public interface ActivityStateListener {
136         /**
137          * Called when the activity's state changes.
138          * @param activity The activity that had a state change.
139          * @param newState New activity state.
140          */
onActivityStateChange(Activity activity, @ActivityState int newState)141         void onActivityStateChange(Activity activity, @ActivityState int newState);
142     }
143 
144     /**
145      * Interface to be implemented by listeners for window focus events.
146      */
147     public interface WindowFocusChangedListener {
148         /**
149          * Called when the window focus changes for {@code activity}.
150          * @param activity The {@link Activity} that has a window focus changed event.
151          * @param hasFocus Whether or not {@code activity} gained or lost focus.
152          */
onWindowFocusChanged(Activity activity, boolean hasFocus)153         public void onWindowFocusChanged(Activity activity, boolean hasFocus);
154     }
155 
ApplicationStatus()156     private ApplicationStatus() {}
157 
158     /**
159      * Registers a listener to receive window focus updates on activities in this application.
160      * @param listener Listener to receive window focus events.
161      */
registerWindowFocusChangedListener(WindowFocusChangedListener listener)162     public static void registerWindowFocusChangedListener(WindowFocusChangedListener listener) {
163         sWindowFocusListeners.addObserver(listener);
164     }
165 
166     /**
167      * Unregisters a listener from receiving window focus updates on activities in this application.
168      * @param listener Listener that doesn't want to receive window focus events.
169      */
unregisterWindowFocusChangedListener(WindowFocusChangedListener listener)170     public static void unregisterWindowFocusChangedListener(WindowFocusChangedListener listener) {
171         sWindowFocusListeners.removeObserver(listener);
172     }
173 
174     /**
175      * Intercepts calls to an existing Window.Callback. Most invocations are passed on directly
176      * to the composed Window.Callback but enables intercepting/manipulating others.
177      *
178      * This is used to relay window focus changes throughout the app and remedy a bug in the
179      * appcompat library.
180      */
181     private static class WindowCallbackProxy implements InvocationHandler {
182         private final Window.Callback mCallback;
183         private final Activity mActivity;
184 
WindowCallbackProxy(Activity activity, Window.Callback callback)185         public WindowCallbackProxy(Activity activity, Window.Callback callback) {
186             mCallback = callback;
187             mActivity = activity;
188         }
189 
190         @Override
invoke(Object proxy, Method method, Object[] args)191         public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
192             if (method.getName().equals("onWindowFocusChanged") && args.length == 1
193                     && args[0] instanceof Boolean) {
194                 onWindowFocusChanged((boolean) args[0]);
195                 return null;
196             } else {
197                 try {
198                     return method.invoke(mCallback, args);
199                 } catch (InvocationTargetException e) {
200                     // Special-case for when a method is not defined on the underlying
201                     // Window.Callback object. Because we're using a Proxy to forward all method
202                     // calls, this breaks the Android framework's handling for apps built against
203                     // an older SDK. The framework expects an AbstractMethodError but due to
204                     // reflection it becomes wrapped inside an InvocationTargetException. Undo the
205                     // wrapping to signal the framework accordingly.
206                     if (e.getCause() instanceof AbstractMethodError) {
207                         throw e.getCause();
208                     }
209                     throw e;
210                 }
211             }
212         }
213 
onWindowFocusChanged(boolean hasFocus)214         public void onWindowFocusChanged(boolean hasFocus) {
215             mCallback.onWindowFocusChanged(hasFocus);
216 
217             for (WindowFocusChangedListener listener : sWindowFocusListeners) {
218                 listener.onWindowFocusChanged(mActivity, hasFocus);
219             }
220         }
221     }
222 
223     /**
224      * Initializes the activity status for a specified application.
225      *
226      * @param application The application whose status you wish to monitor.
227      */
initialize(Application application)228     public static void initialize(Application application) {
229         if (sIsInitialized) return;
230         sIsInitialized = true;
231 
232         registerWindowFocusChangedListener(new WindowFocusChangedListener() {
233             @Override
234             public void onWindowFocusChanged(Activity activity, boolean hasFocus) {
235                 if (!hasFocus || activity == sActivity) return;
236 
237                 int state = getStateForActivity(activity);
238 
239                 if (state != ActivityState.DESTROYED && state != ActivityState.STOPPED) {
240                     sActivity = activity;
241                 }
242 
243                 // TODO(dtrainor): Notify of active activity change?
244             }
245         });
246 
247         application.registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
248             @Override
249             public void onActivityCreated(final Activity activity, Bundle savedInstanceState) {
250                 onStateChange(activity, ActivityState.CREATED);
251                 Window.Callback callback = activity.getWindow().getCallback();
252                 activity.getWindow().setCallback((Window.Callback) Proxy.newProxyInstance(
253                         Window.Callback.class.getClassLoader(), new Class[] {Window.Callback.class},
254                         new ApplicationStatus.WindowCallbackProxy(activity, callback)));
255             }
256 
257             @Override
258             public void onActivityDestroyed(Activity activity) {
259                 onStateChange(activity, ActivityState.DESTROYED);
260                 checkCallback(activity);
261             }
262 
263             @Override
264             public void onActivityPaused(Activity activity) {
265                 onStateChange(activity, ActivityState.PAUSED);
266                 checkCallback(activity);
267             }
268 
269             @Override
270             public void onActivityResumed(Activity activity) {
271                 onStateChange(activity, ActivityState.RESUMED);
272                 checkCallback(activity);
273             }
274 
275             @Override
276             public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
277                 checkCallback(activity);
278             }
279 
280             @Override
281             public void onActivityStarted(Activity activity) {
282                 onStateChange(activity, ActivityState.STARTED);
283                 checkCallback(activity);
284             }
285 
286             @Override
287             public void onActivityStopped(Activity activity) {
288                 onStateChange(activity, ActivityState.STOPPED);
289                 checkCallback(activity);
290             }
291 
292             private void checkCallback(Activity activity) {
293                 if (BuildConfig.DCHECK_IS_ON) {
294                     Class<? extends Window.Callback> callback =
295                             activity.getWindow().getCallback().getClass();
296                     assert(Proxy.isProxyClass(callback)
297                             || callback.getName().equals(TOOLBAR_CALLBACK_WRAPPER_CLASS)
298                             || callback.getName().equals(TOOLBAR_CALLBACK_INTERNAL_WRAPPER_CLASS)
299                             || callback.getName().equals(WINDOW_PROFILER_CALLBACK));
300                 }
301             }
302         });
303     }
304 
305     /**
306      * Asserts that initialize method has been called.
307      */
assertInitialized()308     private static void assertInitialized() {
309         if (!sIsInitialized) {
310             throw new IllegalStateException("ApplicationStatus has not been initialized yet.");
311         }
312     }
313 
314     /**
315      * Must be called by the main activity when it changes state.
316      *
317      * @param activity Current activity.
318      * @param newState New state value.
319      */
onStateChange(Activity activity, @ActivityState int newState)320     private static void onStateChange(Activity activity, @ActivityState int newState) {
321         if (activity == null) throw new IllegalArgumentException("null activity is not supported");
322 
323         if (sActivity == null
324                 || newState == ActivityState.CREATED
325                 || newState == ActivityState.RESUMED
326                 || newState == ActivityState.STARTED) {
327             sActivity = activity;
328         }
329 
330         int oldApplicationState = getStateForApplication();
331         ActivityInfo info;
332 
333         synchronized (sCurrentApplicationStateLock) {
334             if (newState == ActivityState.CREATED) {
335                 assert !sActivityInfo.containsKey(activity);
336                 sActivityInfo.put(activity, new ActivityInfo());
337             }
338 
339             info = sActivityInfo.get(activity);
340             info.setStatus(newState);
341 
342             // Remove before calling listeners so that isEveryActivityDestroyed() returns false when
343             // this was the last activity.
344             if (newState == ActivityState.DESTROYED) {
345                 sActivityInfo.remove(activity);
346                 if (activity == sActivity) sActivity = null;
347             }
348 
349             sCurrentApplicationState = determineApplicationState();
350         }
351 
352         // Notify all state observers that are specifically listening to this activity.
353         for (ActivityStateListener listener : info.getListeners()) {
354             listener.onActivityStateChange(activity, newState);
355         }
356 
357         // Notify all state observers that are listening globally for all activity state
358         // changes.
359         for (ActivityStateListener listener : sGeneralActivityStateListeners) {
360             listener.onActivityStateChange(activity, newState);
361         }
362 
363         int applicationState = getStateForApplication();
364         if (applicationState != oldApplicationState) {
365             for (ApplicationStateListener listener : sApplicationStateListeners) {
366                 listener.onApplicationStateChange(applicationState);
367             }
368         }
369     }
370 
371     /**
372      * Testing method to update the state of the specified activity.
373      */
374     @VisibleForTesting
onStateChangeForTesting(Activity activity, int newState)375     public static void onStateChangeForTesting(Activity activity, int newState) {
376         onStateChange(activity, newState);
377     }
378 
379     /**
380      * @return The most recent focused {@link Activity} tracked by this class.  Being focused means
381      *         out of all the activities tracked here, it has most recently gained window focus.
382      */
getLastTrackedFocusedActivity()383     public static Activity getLastTrackedFocusedActivity() {
384         return sActivity;
385     }
386 
387     /**
388      * @return A {@link List} of all non-destroyed {@link Activity}s.
389      */
getRunningActivities()390     public static List<WeakReference<Activity>> getRunningActivities() {
391         assertInitialized();
392         List<WeakReference<Activity>> activities = new ArrayList<>();
393         for (Activity activity : sActivityInfo.keySet()) {
394             activities.add(new WeakReference<>(activity));
395         }
396         return activities;
397     }
398 
399     /**
400      * Query the state for a given activity.  If the activity is not being tracked, this will
401      * return {@link ActivityState#DESTROYED}.
402      *
403      * <p>
404      * Please note that Chrome can have multiple activities running simultaneously.  Please also
405      * look at {@link #getStateForApplication()} for more details.
406      *
407      * <p>
408      * When relying on this method, be familiar with the expected life cycle state
409      * transitions:
410      * <a href="http://developer.android.com/guide/components/activities.html#Lifecycle">
411      *   Activity Lifecycle
412      * </a>
413      *
414      * <p>
415      * During activity transitions (activity B launching in front of activity A), A will completely
416      * paused before the creation of activity B begins.
417      *
418      * <p>
419      * A basic flow for activity A starting, followed by activity B being opened and then closed:
420      * <ul>
421      *   <li> -- Starting Activity A --
422      *   <li> Activity A - ActivityState.CREATED
423      *   <li> Activity A - ActivityState.STARTED
424      *   <li> Activity A - ActivityState.RESUMED
425      *   <li> -- Starting Activity B --
426      *   <li> Activity A - ActivityState.PAUSED
427      *   <li> Activity B - ActivityState.CREATED
428      *   <li> Activity B - ActivityState.STARTED
429      *   <li> Activity B - ActivityState.RESUMED
430      *   <li> Activity A - ActivityState.STOPPED
431      *   <li> -- Closing Activity B, Activity A regaining focus --
432      *   <li> Activity B - ActivityState.PAUSED
433      *   <li> Activity A - ActivityState.STARTED
434      *   <li> Activity A - ActivityState.RESUMED
435      *   <li> Activity B - ActivityState.STOPPED
436      *   <li> Activity B - ActivityState.DESTROYED
437      * </ul>
438      *
439      * @param activity The activity whose state is to be returned.
440      * @return The state of the specified activity (see {@link ActivityState}).
441      */
442     @ActivityState
getStateForActivity(@ullable Activity activity)443     public static int getStateForActivity(@Nullable Activity activity) {
444         ApplicationStatus.assertInitialized();
445         if (activity == null) return ActivityState.DESTROYED;
446         ActivityInfo info = sActivityInfo.get(activity);
447         return info != null ? info.getStatus() : ActivityState.DESTROYED;
448     }
449 
450     /**
451      * @return The state of the application (see {@link ApplicationState}).
452      */
453     @ApplicationState
454     @CalledByNative
getStateForApplication()455     public static int getStateForApplication() {
456         synchronized (sCurrentApplicationStateLock) {
457             return sCurrentApplicationState;
458         }
459     }
460 
461     /**
462      * Checks whether or not any Activity in this Application is visible to the user.  Note that
463      * this includes the PAUSED state, which can happen when the Activity is temporarily covered
464      * by another Activity's Fragment (e.g.).
465      * @return Whether any Activity under this Application is visible.
466      */
hasVisibleActivities()467     public static boolean hasVisibleActivities() {
468         int state = getStateForApplication();
469         return state == ApplicationState.HAS_RUNNING_ACTIVITIES
470                 || state == ApplicationState.HAS_PAUSED_ACTIVITIES;
471     }
472 
473     /**
474      * Checks to see if there are any active Activity instances being watched by ApplicationStatus.
475      * @return True if all Activities have been destroyed.
476      */
isEveryActivityDestroyed()477     public static boolean isEveryActivityDestroyed() {
478         return sActivityInfo.isEmpty();
479     }
480 
481     /**
482      * Registers the given listener to receive state changes for all activities.
483      * @param listener Listener to receive state changes.
484      */
registerStateListenerForAllActivities(ActivityStateListener listener)485     public static void registerStateListenerForAllActivities(ActivityStateListener listener) {
486         sGeneralActivityStateListeners.addObserver(listener);
487     }
488 
489     /**
490      * Registers the given listener to receive state changes for {@code activity}.  After a call to
491      * {@link ActivityStateListener#onActivityStateChange(Activity, int)} with
492      * {@link ActivityState#DESTROYED} all listeners associated with that particular
493      * {@link Activity} are removed.
494      * @param listener Listener to receive state changes.
495      * @param activity Activity to track or {@code null} to track all activities.
496      */
497     @SuppressLint("NewApi")
registerStateListenerForActivity(ActivityStateListener listener, Activity activity)498     public static void registerStateListenerForActivity(ActivityStateListener listener,
499             Activity activity) {
500         if (activity == null) {
501             throw new IllegalStateException("Attempting to register listener on a null activity.");
502         }
503         ApplicationStatus.assertInitialized();
504 
505         ActivityInfo info = sActivityInfo.get(activity);
506         if (info == null) {
507             throw new IllegalStateException(
508                     "Attempting to register listener on an untracked activity.");
509         }
510         assert info.getStatus() != ActivityState.DESTROYED;
511         info.getListeners().addObserver(listener);
512     }
513 
514     /**
515      * Unregisters the given listener from receiving activity state changes.
516      * @param listener Listener that doesn't want to receive state changes.
517      */
unregisterActivityStateListener(ActivityStateListener listener)518     public static void unregisterActivityStateListener(ActivityStateListener listener) {
519         sGeneralActivityStateListeners.removeObserver(listener);
520 
521         // Loop through all observer lists for all activities and remove the listener.
522         for (ActivityInfo info : sActivityInfo.values()) {
523             info.getListeners().removeObserver(listener);
524         }
525     }
526 
527     /**
528      * Registers the given listener to receive state changes for the application.
529      * @param listener Listener to receive state state changes.
530      */
registerApplicationStateListener(ApplicationStateListener listener)531     public static void registerApplicationStateListener(ApplicationStateListener listener) {
532         sApplicationStateListeners.addObserver(listener);
533     }
534 
535     /**
536      * Unregisters the given listener from receiving state changes.
537      * @param listener Listener that doesn't want to receive state changes.
538      */
unregisterApplicationStateListener(ApplicationStateListener listener)539     public static void unregisterApplicationStateListener(ApplicationStateListener listener) {
540         sApplicationStateListeners.removeObserver(listener);
541     }
542 
543     /**
544      * Robolectric JUnit tests create a new application between each test, while all the context
545      * in static classes isn't reset. This function allows to reset the application status to avoid
546      * being in a dirty state.
547      */
destroyForJUnitTests()548     public static void destroyForJUnitTests() {
549         sApplicationStateListeners.clear();
550         sGeneralActivityStateListeners.clear();
551         sActivityInfo.clear();
552         sWindowFocusListeners.clear();
553         sIsInitialized = false;
554         synchronized (sCurrentApplicationStateLock) {
555             sCurrentApplicationState = determineApplicationState();
556         }
557         sActivity = null;
558         sNativeApplicationStateListener = null;
559     }
560 
561     /**
562      * Registers the single thread-safe native activity status listener.
563      * This handles the case where the caller is not on the main thread.
564      * Note that this is used by a leaky singleton object from the native
565      * side, hence lifecycle management is greatly simplified.
566      */
567     @CalledByNative
registerThreadSafeNativeApplicationStateListener()568     private static void registerThreadSafeNativeApplicationStateListener() {
569         ThreadUtils.runOnUiThread(new Runnable() {
570             @Override
571             public void run() {
572                 if (sNativeApplicationStateListener != null) return;
573 
574                 sNativeApplicationStateListener = new ApplicationStateListener() {
575                     @Override
576                     public void onApplicationStateChange(int newState) {
577                         nativeOnApplicationStateChange(newState);
578                     }
579                 };
580                 registerApplicationStateListener(sNativeApplicationStateListener);
581             }
582         });
583     }
584 
585     /**
586      * Determines the current application state as defined by {@link ApplicationState}.  This will
587      * loop over all the activities and check their state to determine what the general application
588      * state should be.
589      * @return HAS_RUNNING_ACTIVITIES if any activity is not paused, stopped, or destroyed.
590      *         HAS_PAUSED_ACTIVITIES if none are running and one is paused.
591      *         HAS_STOPPED_ACTIVITIES if none are running/paused and one is stopped.
592      *         HAS_DESTROYED_ACTIVITIES if none are running/paused/stopped.
593      */
594     @ApplicationState
determineApplicationState()595     private static int determineApplicationState() {
596         boolean hasPausedActivity = false;
597         boolean hasStoppedActivity = false;
598 
599         for (ActivityInfo info : sActivityInfo.values()) {
600             int state = info.getStatus();
601             if (state != ActivityState.PAUSED
602                     && state != ActivityState.STOPPED
603                     && state != ActivityState.DESTROYED) {
604                 return ApplicationState.HAS_RUNNING_ACTIVITIES;
605             } else if (state == ActivityState.PAUSED) {
606                 hasPausedActivity = true;
607             } else if (state == ActivityState.STOPPED) {
608                 hasStoppedActivity = true;
609             }
610         }
611 
612         if (hasPausedActivity) return ApplicationState.HAS_PAUSED_ACTIVITIES;
613         if (hasStoppedActivity) return ApplicationState.HAS_STOPPED_ACTIVITIES;
614         return ApplicationState.HAS_DESTROYED_ACTIVITIES;
615     }
616 
617     // Called to notify the native side of state changes.
618     // IMPORTANT: This is always called on the main thread!
nativeOnApplicationStateChange(@pplicationState int newState)619     private static native void nativeOnApplicationStateChange(@ApplicationState int newState);
620 }
621