blob: 8c36b61bd60fe47c512c8ca6d27e0db860f41238 [file] [log] [blame]
// Copyright 2014 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.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 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 Object sCachedApplicationStateLock = new Object();
private static Integer sCachedApplicationState;
/** Last activity that was shown (or null if none or it was destroyed). */
private static Activity sActivity;
/** A lazily initialized listener that forwards application state changes to native. */
private static ApplicationStateListener sNativeApplicationStateListener;
/**
* A map of which observers listen to state changes from which {@link Activity}.
*/
private static final Map<Activity, ActivityInfo> sActivityInfo =
new ConcurrentHashMap<Activity, ActivityInfo>();
/**
* A list of observers to be notified when any {@link Activity} has a state change.
*/
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.
synchronized (sCachedApplicationStateLock) {
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.
*/
@VisibleForTesting
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 A {@link List} of all non-destroyed {@link Activity}s.
*/
public static List<WeakReference<Activity>> getRunningActivities() {
List<WeakReference<Activity>> activities = new ArrayList<WeakReference<Activity>>();
for (Activity activity : sActivityInfo.keySet()) {
activities.add(new WeakReference<Activity>(activity));
}
return activities;
}
/**
* @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() {
synchronized (sCachedApplicationStateLock) {
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() {
if (sNativeApplicationStateListener != null) return;
sNativeApplicationStateListener = new ApplicationStateListener() {
@Override
public void onApplicationStateChange(int newState) {
nativeOnApplicationStateChange(newState);
}
};
registerApplicationStateListener(sNativeApplicationStateListener);
}
});
}
/**
* 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);
}