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
5package org.chromium.base;
6
7import android.app.Activity;
8import android.app.Application;
9import android.app.Application.ActivityLifecycleCallbacks;
10import android.content.Context;
11import android.os.Bundle;
12
13import java.lang.ref.WeakReference;
14import java.util.ArrayList;
15import java.util.List;
16import java.util.Map;
17import java.util.concurrent.ConcurrentHashMap;
18
19/**
20 * Provides information about the current activity's status, and a way
21 * to register / unregister listeners for state changes.
22 */
23@JNINamespace("base::android")
24public class ApplicationStatus {
25    private static class ActivityInfo {
26        private int mStatus = ActivityState.DESTROYED;
27        private ObserverList<ActivityStateListener> mListeners =
28                new ObserverList<ActivityStateListener>();
29
30        /**
31         * @return The current {@link ActivityState} of the activity.
32         */
33        public int getStatus() {
34            return mStatus;
35        }
36
37        /**
38         * @param status The new {@link ActivityState} of the activity.
39         */
40        public void setStatus(int status) {
41            mStatus = status;
42        }
43
44        /**
45         * @return A list of {@link ActivityStateListener}s listening to this activity.
46         */
47        public ObserverList<ActivityStateListener> getListeners() {
48            return mListeners;
49        }
50    }
51
52    private static Application sApplication;
53
54    private static Object sCachedApplicationStateLock = new Object();
55    private static Integer sCachedApplicationState;
56
57    /** Last activity that was shown (or null if none or it was destroyed). */
58    private static Activity sActivity;
59
60    /** A lazily initialized listener that forwards application state changes to native. */
61    private static ApplicationStateListener sNativeApplicationStateListener;
62
63    /**
64     * A map of which observers listen to state changes from which {@link Activity}.
65     */
66    private static final Map<Activity, ActivityInfo> sActivityInfo =
67            new ConcurrentHashMap<Activity, ActivityInfo>();
68
69    /**
70     * A list of observers to be notified when any {@link Activity} has a state change.
71     */
72    private static final ObserverList<ActivityStateListener> sGeneralActivityStateListeners =
73            new ObserverList<ActivityStateListener>();
74
75    /**
76     * A list of observers to be notified when the visibility state of this {@link Application}
77     * changes.  See {@link #getStateForApplication()}.
78     */
79    private static final ObserverList<ApplicationStateListener> sApplicationStateListeners =
80            new ObserverList<ApplicationStateListener>();
81
82    /**
83     * Interface to be implemented by listeners.
84     */
85    public interface ApplicationStateListener {
86        /**
87         * Called when the application's state changes.
88         * @param newState The application state.
89         */
90        public void onApplicationStateChange(int newState);
91    }
92
93    /**
94     * Interface to be implemented by listeners.
95     */
96    public interface ActivityStateListener {
97        /**
98         * Called when the activity's state changes.
99         * @param activity The activity that had a state change.
100         * @param newState New activity state.
101         */
102        public void onActivityStateChange(Activity activity, int newState);
103    }
104
105    private ApplicationStatus() {}
106
107    /**
108     * Initializes the activity status for a specified application.
109     *
110     * @param application The application whose status you wish to monitor.
111     */
112    public static void initialize(BaseChromiumApplication application) {
113        sApplication = application;
114
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     */
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
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     */
234    public static Activity getLastTrackedFocusedActivity() {
235        return sActivity;
236    }
237
238    /**
239     * @return A {@link List} of all non-destroyed {@link Activity}s.
240     */
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     * @return The {@link Context} for the {@link Application}.
251     */
252    public static Context getApplicationContext() {
253        return sApplication != null ? sApplication.getApplicationContext() : null;
254    }
255
256    /**
257     * Query the state for a given activity.  If the activity is not being tracked, this will
258     * return {@link ActivityState#DESTROYED}.
259     *
260     * <p>
261     * Please note that Chrome can have multiple activities running simultaneously.  Please also
262     * look at {@link #getStateForApplication()} for more details.
263     *
264     * <p>
265     * When relying on this method, be familiar with the expected life cycle state
266     * transitions:
267     * <a href="http://developer.android.com/guide/components/activities.html#Lifecycle">
268     *   Activity Lifecycle
269     * </a>
270     *
271     * <p>
272     * During activity transitions (activity B launching in front of activity A), A will completely
273     * paused before the creation of activity B begins.
274     *
275     * <p>
276     * A basic flow for activity A starting, followed by activity B being opened and then closed:
277     * <ul>
278     *   <li> -- Starting Activity A --
279     *   <li> Activity A - ActivityState.CREATED
280     *   <li> Activity A - ActivityState.STARTED
281     *   <li> Activity A - ActivityState.RESUMED
282     *   <li> -- Starting Activity B --
283     *   <li> Activity A - ActivityState.PAUSED
284     *   <li> Activity B - ActivityState.CREATED
285     *   <li> Activity B - ActivityState.STARTED
286     *   <li> Activity B - ActivityState.RESUMED
287     *   <li> Activity A - ActivityState.STOPPED
288     *   <li> -- Closing Activity B, Activity A regaining focus --
289     *   <li> Activity B - ActivityState.PAUSED
290     *   <li> Activity A - ActivityState.STARTED
291     *   <li> Activity A - ActivityState.RESUMED
292     *   <li> Activity B - ActivityState.STOPPED
293     *   <li> Activity B - ActivityState.DESTROYED
294     * </ul>
295     *
296     * @param activity The activity whose state is to be returned.
297     * @return The state of the specified activity (see {@link ActivityState}).
298     */
299    public static int getStateForActivity(Activity activity) {
300        ActivityInfo info = sActivityInfo.get(activity);
301        return info != null ? info.getStatus() : ActivityState.DESTROYED;
302    }
303
304    /**
305     * @return The state of the application (see {@link ApplicationState}).
306     */
307    public static int getStateForApplication() {
308        synchronized (sCachedApplicationStateLock) {
309            if (sCachedApplicationState == null) {
310                sCachedApplicationState = determineApplicationState();
311            }
312        }
313
314        return sCachedApplicationState.intValue();
315    }
316
317    /**
318     * Checks whether or not any Activity in this Application is visible to the user.  Note that
319     * this includes the PAUSED state, which can happen when the Activity is temporarily covered
320     * by another Activity's Fragment (e.g.).
321     * @return Whether any Activity under this Application is visible.
322     */
323    public static boolean hasVisibleActivities() {
324        int state = getStateForApplication();
325        return state == ApplicationState.HAS_RUNNING_ACTIVITIES
326                || state == ApplicationState.HAS_PAUSED_ACTIVITIES;
327    }
328
329    /**
330     * Checks to see if there are any active Activity instances being watched by ApplicationStatus.
331     * @return True if all Activities have been destroyed.
332     */
333    public static boolean isEveryActivityDestroyed() {
334        return sActivityInfo.isEmpty();
335    }
336
337    /**
338     * Registers the given listener to receive state changes for all activities.
339     * @param listener Listener to receive state changes.
340     */
341    public static void registerStateListenerForAllActivities(ActivityStateListener listener) {
342        sGeneralActivityStateListeners.addObserver(listener);
343    }
344
345    /**
346     * Registers the given listener to receive state changes for {@code activity}.  After a call to
347     * {@link ActivityStateListener#onActivityStateChange(Activity, int)} with
348     * {@link ActivityState#DESTROYED} all listeners associated with that particular
349     * {@link Activity} are removed.
350     * @param listener Listener to receive state changes.
351     * @param activity Activity to track or {@code null} to track all activities.
352     */
353    public static void registerStateListenerForActivity(ActivityStateListener listener,
354            Activity activity) {
355        assert activity != null;
356
357        ActivityInfo info = sActivityInfo.get(activity);
358        assert info != null && info.getStatus() != ActivityState.DESTROYED;
359        info.getListeners().addObserver(listener);
360    }
361
362    /**
363     * Unregisters the given listener from receiving activity state changes.
364     * @param listener Listener that doesn't want to receive state changes.
365     */
366    public static void unregisterActivityStateListener(ActivityStateListener listener) {
367        sGeneralActivityStateListeners.removeObserver(listener);
368
369        // Loop through all observer lists for all activities and remove the listener.
370        for (ActivityInfo info : sActivityInfo.values()) {
371            info.getListeners().removeObserver(listener);
372        }
373    }
374
375    /**
376     * Registers the given listener to receive state changes for the application.
377     * @param listener Listener to receive state state changes.
378     */
379    public static void registerApplicationStateListener(ApplicationStateListener listener) {
380        sApplicationStateListeners.addObserver(listener);
381    }
382
383    /**
384     * Unregisters the given listener from receiving state changes.
385     * @param listener Listener that doesn't want to receive state changes.
386     */
387    public static void unregisterApplicationStateListener(ApplicationStateListener listener) {
388        sApplicationStateListeners.removeObserver(listener);
389    }
390
391    /**
392     * Registers the single thread-safe native activity status listener.
393     * This handles the case where the caller is not on the main thread.
394     * Note that this is used by a leaky singleton object from the native
395     * side, hence lifecycle management is greatly simplified.
396     */
397    @CalledByNative
398    private static void registerThreadSafeNativeApplicationStateListener() {
399        ThreadUtils.runOnUiThread(new Runnable () {
400            @Override
401            public void run() {
402                if (sNativeApplicationStateListener != null) return;
403
404                sNativeApplicationStateListener = new ApplicationStateListener() {
405                    @Override
406                    public void onApplicationStateChange(int newState) {
407                        nativeOnApplicationStateChange(newState);
408                    }
409                };
410                registerApplicationStateListener(sNativeApplicationStateListener);
411            }
412        });
413    }
414
415    /**
416     * Determines the current application state as defined by {@link ApplicationState}.  This will
417     * loop over all the activities and check their state to determine what the general application
418     * state should be.
419     * @return HAS_RUNNING_ACTIVITIES if any activity is not paused, stopped, or destroyed.
420     *         HAS_PAUSED_ACTIVITIES if none are running and one is paused.
421     *         HAS_STOPPED_ACTIVITIES if none are running/paused and one is stopped.
422     *         HAS_DESTROYED_ACTIVITIES if none are running/paused/stopped.
423     */
424    private static int determineApplicationState() {
425        boolean hasPausedActivity = false;
426        boolean hasStoppedActivity = false;
427
428        for (ActivityInfo info : sActivityInfo.values()) {
429            int state = info.getStatus();
430            if (state != ActivityState.PAUSED
431                    && state != ActivityState.STOPPED
432                    && state != ActivityState.DESTROYED) {
433                return ApplicationState.HAS_RUNNING_ACTIVITIES;
434            } else if (state == ActivityState.PAUSED) {
435                hasPausedActivity = true;
436            } else if (state == ActivityState.STOPPED) {
437                hasStoppedActivity = true;
438            }
439        }
440
441        if (hasPausedActivity) return ApplicationState.HAS_PAUSED_ACTIVITIES;
442        if (hasStoppedActivity) return ApplicationState.HAS_STOPPED_ACTIVITIES;
443        return ApplicationState.HAS_DESTROYED_ACTIVITIES;
444    }
445
446    // Called to notify the native side of state changes.
447    // IMPORTANT: This is always called on the main thread!
448    private static native void nativeOnApplicationStateChange(int newState);
449}
450