Index: base/android/java/src/org/chromium/base/ApplicationStatus.java |
diff --git a/base/android/java/src/org/chromium/base/ApplicationStatus.java b/base/android/java/src/org/chromium/base/ApplicationStatus.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..9930682f15e9fc3ceabc7e8847caffa2b5f0d962 |
--- /dev/null |
+++ b/base/android/java/src/org/chromium/base/ApplicationStatus.java |
@@ -0,0 +1,421 @@ |
+// Copyright 2012 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+package org.chromium.base; |
+ |
+import android.app.Activity; |
+import android.app.Application; |
+import android.app.Application.ActivityLifecycleCallbacks; |
+import android.content.Context; |
+import android.os.Bundle; |
+ |
+import java.util.HashMap; |
+import java.util.Map; |
+ |
+/** |
+ * Provides information about the current activity's status, and a way |
+ * to register / unregister listeners for state changes. |
+ */ |
+@JNINamespace("base::android") |
+public class ApplicationStatus { |
+ private static class ActivityInfo { |
+ private int mStatus = ActivityState.DESTROYED; |
+ private ObserverList<ActivityStateListener> mListeners = |
+ new ObserverList<ActivityStateListener>(); |
+ |
+ /** |
+ * @return The current {@link ActivityState} of the activity. |
+ */ |
+ public int getStatus() { |
+ return mStatus; |
+ } |
+ |
+ /** |
+ * @param status The new {@link ActivityState} of the activity. |
+ */ |
+ public void setStatus(int status) { |
+ mStatus = status; |
+ } |
+ |
+ /** |
+ * @return A list of {@link ActivityStateListener}s listening to this activity. |
+ */ |
+ public ObserverList<ActivityStateListener> getListeners() { |
+ return mListeners; |
+ } |
+ } |
+ |
+ private static Application sApplication; |
+ |
+ private static Integer sCachedApplicationState; |
+ |
+ /** Last activity that was shown (or null if none or it was destroyed). */ |
+ private static Activity sActivity; |
+ |
+ /** |
+ * |
+ */ |
+ private static final Map<Activity, ActivityInfo> sActivityInfo = |
+ new HashMap<Activity, ActivityInfo>(); |
+ |
+ /** |
+ * |
+ */ |
+ private static final ObserverList<ActivityStateListener> sGeneralActivityStateListeners = |
+ new ObserverList<ActivityStateListener>(); |
+ |
+ /** |
+ * A list of observers to be notified when the visibility state of this {@link Application} |
+ * changes. See {@link #getStateForApplication()}. |
+ */ |
+ private static final ObserverList<ApplicationStateListener> sApplicationStateListeners = |
+ new ObserverList<ApplicationStateListener>(); |
+ |
+ /** |
+ * Interface to be implemented by listeners. |
+ */ |
+ public interface ApplicationStateListener { |
+ /** |
+ * Called when the application's state changes. |
+ * @param newState The application state. |
+ */ |
+ public void onApplicationStateChange(int newState); |
+ } |
+ |
+ /** |
+ * Interface to be implemented by listeners. |
+ */ |
+ public interface ActivityStateListener { |
+ /** |
+ * Called when the activity's state changes. |
+ * @param activity The activity that had a state change. |
+ * @param newState New activity state. |
+ */ |
+ public void onActivityStateChange(Activity activity, int newState); |
+ } |
+ |
+ private ApplicationStatus() {} |
+ |
+ /** |
+ * Initializes the activity status for a specified application. |
+ * |
+ * @param application The application whose status you wish to monitor. |
+ */ |
+ public static void initialize(BaseChromiumApplication application) { |
+ sApplication = application; |
+ |
+ application.registerWindowFocusChangedListener( |
+ new BaseChromiumApplication.WindowFocusChangedListener() { |
+ @Override |
+ public void onWindowFocusChanged(Activity activity, boolean hasFocus) { |
+ if (!hasFocus || activity == sActivity) return; |
+ |
+ int state = getStateForActivity(activity); |
+ |
+ if (state != ActivityState.DESTROYED && state != ActivityState.STOPPED) { |
+ sActivity = activity; |
+ } |
+ |
+ // TODO(dtrainor): Notify of active activity change? |
+ } |
+ }); |
+ |
+ application.registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { |
+ @Override |
+ public void onActivityCreated(final Activity activity, Bundle savedInstanceState) { |
+ onStateChange(activity, ActivityState.CREATED); |
+ } |
+ |
+ @Override |
+ public void onActivityDestroyed(Activity activity) { |
+ onStateChange(activity, ActivityState.DESTROYED); |
+ } |
+ |
+ @Override |
+ public void onActivityPaused(Activity activity) { |
+ onStateChange(activity, ActivityState.PAUSED); |
+ } |
+ |
+ @Override |
+ public void onActivityResumed(Activity activity) { |
+ onStateChange(activity, ActivityState.RESUMED); |
+ } |
+ |
+ @Override |
+ public void onActivitySaveInstanceState(Activity activity, Bundle outState) {} |
+ |
+ @Override |
+ public void onActivityStarted(Activity activity) { |
+ onStateChange(activity, ActivityState.STARTED); |
+ } |
+ |
+ @Override |
+ public void onActivityStopped(Activity activity) { |
+ onStateChange(activity, ActivityState.STOPPED); |
+ } |
+ }); |
+ } |
+ |
+ /** |
+ * Must be called by the main activity when it changes state. |
+ * |
+ * @param activity Current activity. |
+ * @param newState New state value. |
+ */ |
+ private static void onStateChange(Activity activity, int newState) { |
+ if (activity == null) throw new IllegalArgumentException("null activity is not supported"); |
+ |
+ if (sActivity == null |
+ || newState == ActivityState.CREATED |
+ || newState == ActivityState.RESUMED |
+ || newState == ActivityState.STARTED) { |
+ sActivity = activity; |
+ } |
+ |
+ int oldApplicationState = getStateForApplication(); |
+ |
+ if (newState == ActivityState.CREATED) { |
+ assert !sActivityInfo.containsKey(activity); |
+ sActivityInfo.put(activity, new ActivityInfo()); |
+ } |
+ |
+ // Invalidate the cached application state. |
+ sCachedApplicationState = null; |
+ |
+ ActivityInfo info = sActivityInfo.get(activity); |
+ info.setStatus(newState); |
+ |
+ // Notify all state observers that are specifically listening to this activity. |
+ for (ActivityStateListener listener : info.getListeners()) { |
+ listener.onActivityStateChange(activity, newState); |
+ } |
+ |
+ // Notify all state observers that are listening globally for all activity state |
+ // changes. |
+ for (ActivityStateListener listener : sGeneralActivityStateListeners) { |
+ listener.onActivityStateChange(activity, newState); |
+ } |
+ |
+ int applicationState = getStateForApplication(); |
+ if (applicationState != oldApplicationState) { |
+ for (ApplicationStateListener listener : sApplicationStateListeners) { |
+ listener.onApplicationStateChange(applicationState); |
+ } |
+ } |
+ |
+ if (newState == ActivityState.DESTROYED) { |
+ sActivityInfo.remove(activity); |
+ if (activity == sActivity) sActivity = null; |
+ } |
+ } |
+ |
+ /** |
+ * Testing method to update the state of the specified activity. |
+ */ |
+ public static void onStateChangeForTesting(Activity activity, int newState) { |
+ onStateChange(activity, newState); |
+ } |
+ |
+ /** |
+ * @return The most recent focused {@link Activity} tracked by this class. Being focused means |
+ * out of all the activities tracked here, it has most recently gained window focus. |
+ */ |
+ public static Activity getLastTrackedFocusedActivity() { |
+ return sActivity; |
+ } |
+ |
+ /** |
+ * @return The {@link Context} for the {@link Application}. |
+ */ |
+ public static Context getApplicationContext() { |
+ return sApplication != null ? sApplication.getApplicationContext() : null; |
+ } |
+ |
+ /** |
+ * Query the state for a given activity. If the activity is not being tracked, this will |
+ * return {@link ActivityState#DESTROYED}. |
+ * |
+ * <p> |
+ * Please note that Chrome can have multiple activities running simultaneously. Please also |
+ * look at {@link #getStateForApplication()} for more details. |
+ * |
+ * <p> |
+ * When relying on this method, be familiar with the expected life cycle state |
+ * transitions: |
+ * <a href="http://developer.android.com/guide/components/activities.html#Lifecycle"> |
+ * Activity Lifecycle |
+ * </a> |
+ * |
+ * <p> |
+ * During activity transitions (activity B launching in front of activity A), A will completely |
+ * paused before the creation of activity B begins. |
+ * |
+ * <p> |
+ * A basic flow for activity A starting, followed by activity B being opened and then closed: |
+ * <ul> |
+ * <li> -- Starting Activity A -- |
+ * <li> Activity A - ActivityState.CREATED |
+ * <li> Activity A - ActivityState.STARTED |
+ * <li> Activity A - ActivityState.RESUMED |
+ * <li> -- Starting Activity B -- |
+ * <li> Activity A - ActivityState.PAUSED |
+ * <li> Activity B - ActivityState.CREATED |
+ * <li> Activity B - ActivityState.STARTED |
+ * <li> Activity B - ActivityState.RESUMED |
+ * <li> Activity A - ActivityState.STOPPED |
+ * <li> -- Closing Activity B, Activity A regaining focus -- |
+ * <li> Activity B - ActivityState.PAUSED |
+ * <li> Activity A - ActivityState.STARTED |
+ * <li> Activity A - ActivityState.RESUMED |
+ * <li> Activity B - ActivityState.STOPPED |
+ * <li> Activity B - ActivityState.DESTROYED |
+ * </ul> |
+ * |
+ * @param activity The activity whose state is to be returned. |
+ * @return The state of the specified activity (see {@link ActivityState}). |
+ */ |
+ public static int getStateForActivity(Activity activity) { |
+ ActivityInfo info = sActivityInfo.get(activity); |
+ return info != null ? info.getStatus() : ActivityState.DESTROYED; |
+ } |
+ |
+ /** |
+ * @return The state of the application (see {@link ApplicationState}). |
+ */ |
+ public static int getStateForApplication() { |
+ if (sCachedApplicationState == null) sCachedApplicationState = determineApplicationState(); |
+ |
+ return sCachedApplicationState.intValue(); |
+ } |
+ |
+ /** |
+ * Checks whether or not any Activity in this Application is visible to the user. Note that |
+ * this includes the PAUSED state, which can happen when the Activity is temporarily covered |
+ * by another Activity's Fragment (e.g.). |
+ * @return Whether any Activity under this Application is visible. |
+ */ |
+ public static boolean hasVisibleActivities() { |
+ int state = getStateForApplication(); |
+ return state == ApplicationState.HAS_RUNNING_ACTIVITIES |
+ || state == ApplicationState.HAS_PAUSED_ACTIVITIES; |
+ } |
+ |
+ /** |
+ * Checks to see if there are any active Activity instances being watched by ApplicationStatus. |
+ * @return True if all Activities have been destroyed. |
+ */ |
+ public static boolean isEveryActivityDestroyed() { |
+ return sActivityInfo.isEmpty(); |
+ } |
+ |
+ /** |
+ * Registers the given listener to receive state changes for all activities. |
+ * @param listener Listener to receive state changes. |
+ */ |
+ public static void registerStateListenerForAllActivities(ActivityStateListener listener) { |
+ sGeneralActivityStateListeners.addObserver(listener); |
+ } |
+ |
+ /** |
+ * Registers the given listener to receive state changes for {@code activity}. After a call to |
+ * {@link ActivityStateListener#onActivityStateChange(Activity, int)} with |
+ * {@link ActivityState#DESTROYED} all listeners associated with that particular |
+ * {@link Activity} are removed. |
+ * @param listener Listener to receive state changes. |
+ * @param activity Activity to track or {@code null} to track all activities. |
+ */ |
+ public static void registerStateListenerForActivity(ActivityStateListener listener, |
+ Activity activity) { |
+ assert activity != null; |
+ |
+ ActivityInfo info = sActivityInfo.get(activity); |
+ assert info != null && info.getStatus() != ActivityState.DESTROYED; |
+ info.getListeners().addObserver(listener); |
+ } |
+ |
+ /** |
+ * Unregisters the given listener from receiving activity state changes. |
+ * @param listener Listener that doesn't want to receive state changes. |
+ */ |
+ public static void unregisterActivityStateListener(ActivityStateListener listener) { |
+ sGeneralActivityStateListeners.removeObserver(listener); |
+ |
+ // Loop through all observer lists for all activities and remove the listener. |
+ for (ActivityInfo info : sActivityInfo.values()) { |
+ info.getListeners().removeObserver(listener); |
+ } |
+ } |
+ |
+ /** |
+ * Registers the given listener to receive state changes for the application. |
+ * @param listener Listener to receive state state changes. |
+ */ |
+ public static void registerApplicationStateListener(ApplicationStateListener listener) { |
+ sApplicationStateListeners.addObserver(listener); |
+ } |
+ |
+ /** |
+ * Unregisters the given listener from receiving state changes. |
+ * @param listener Listener that doesn't want to receive state changes. |
+ */ |
+ public static void unregisterApplicationStateListener(ApplicationStateListener listener) { |
+ sApplicationStateListeners.removeObserver(listener); |
+ } |
+ |
+ /** |
+ * Registers the single thread-safe native activity status listener. |
+ * This handles the case where the caller is not on the main thread. |
+ * Note that this is used by a leaky singleton object from the native |
+ * side, hence lifecycle management is greatly simplified. |
+ */ |
+ @CalledByNative |
+ private static void registerThreadSafeNativeApplicationStateListener() { |
+ ThreadUtils.runOnUiThread(new Runnable () { |
+ @Override |
+ public void run() { |
+ registerApplicationStateListener(new ApplicationStateListener() { |
+ @Override |
+ public void onApplicationStateChange(int newState) { |
+ nativeOnApplicationStateChange(newState); |
+ } |
+ }); |
+ } |
+ }); |
+ } |
+ |
+ /** |
+ * Determines the current application state as defined by {@link ApplicationState}. This will |
+ * loop over all the activities and check their state to determine what the general application |
+ * state should be. |
+ * @return HAS_RUNNING_ACTIVITIES if any activity is not paused, stopped, or destroyed. |
+ * HAS_PAUSED_ACTIVITIES if none are running and one is paused. |
+ * HAS_STOPPED_ACTIVITIES if none are running/paused and one is stopped. |
+ * HAS_DESTROYED_ACTIVITIES if none are running/paused/stopped. |
+ */ |
+ private static int determineApplicationState() { |
+ boolean hasPausedActivity = false; |
+ boolean hasStoppedActivity = false; |
+ |
+ for (ActivityInfo info : sActivityInfo.values()) { |
+ int state = info.getStatus(); |
+ if (state != ActivityState.PAUSED |
+ && state != ActivityState.STOPPED |
+ && state != ActivityState.DESTROYED) { |
+ return ApplicationState.HAS_RUNNING_ACTIVITIES; |
+ } else if (state == ActivityState.PAUSED) { |
+ hasPausedActivity = true; |
+ } else if (state == ActivityState.STOPPED) { |
+ hasStoppedActivity = true; |
+ } |
+ } |
+ |
+ if (hasPausedActivity) return ApplicationState.HAS_PAUSED_ACTIVITIES; |
+ if (hasStoppedActivity) return ApplicationState.HAS_STOPPED_ACTIVITIES; |
+ return ApplicationState.HAS_DESTROYED_ACTIVITIES; |
+ } |
+ |
+ // Called to notify the native side of state changes. |
+ // IMPORTANT: This is always called on the main thread! |
+ private static native void nativeOnApplicationStateChange(int newState); |
+} |