821 lines
32 KiB
Java
821 lines
32 KiB
Java
// Copyright 2014 The Chromium Authors
|
|
// 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.annotation.SuppressLint;
|
|
import android.app.Activity;
|
|
import android.app.Application;
|
|
import android.app.Application.ActivityLifecycleCallbacks;
|
|
import android.content.SharedPreferences;
|
|
import android.os.Bundle;
|
|
import android.view.Window;
|
|
|
|
import androidx.annotation.AnyThread;
|
|
import androidx.annotation.MainThread;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.annotation.VisibleForTesting;
|
|
|
|
import org.jni_zero.CalledByNative;
|
|
import org.jni_zero.JNINamespace;
|
|
import org.jni_zero.NativeMethods;
|
|
|
|
import org.chromium.build.BuildConfig;
|
|
|
|
import java.lang.reflect.Field;
|
|
import java.lang.reflect.InvocationHandler;
|
|
import java.lang.reflect.InvocationTargetException;
|
|
import java.lang.reflect.Method;
|
|
import java.lang.reflect.Proxy;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
import javax.annotation.concurrent.GuardedBy;
|
|
|
|
/**
|
|
* Provides information about the current activity's status, and a way
|
|
* to register / unregister listeners for state changes.
|
|
* TODO(https://crbug.com/470582): ApplicationStatus will not work on WebView/WebLayer, and
|
|
* should be moved out of base and into //chrome. It should not be relied upon for //components.
|
|
*/
|
|
@JNINamespace("base::android")
|
|
public class ApplicationStatus {
|
|
private static final String TOOLBAR_CALLBACK_WRAPPER_CLASS =
|
|
"androidx.appcompat.app.ToolbarActionBar$ToolbarCallbackWrapper";
|
|
|
|
private static class ActivityInfo {
|
|
private int mStatus = ActivityState.DESTROYED;
|
|
private ObserverList<ActivityStateListener> mListeners = new ObserverList<>();
|
|
|
|
/**
|
|
* @return The current {@link ActivityState} of the activity.
|
|
*/
|
|
@ActivityState
|
|
public int getStatus() {
|
|
return mStatus;
|
|
}
|
|
|
|
/**
|
|
* @param status The new {@link ActivityState} of the activity.
|
|
*/
|
|
public void setStatus(@ActivityState int status) {
|
|
mStatus = status;
|
|
}
|
|
|
|
/**
|
|
* @return A list of {@link ActivityStateListener}s listening to this activity.
|
|
*/
|
|
public ObserverList<ActivityStateListener> getListeners() {
|
|
return mListeners;
|
|
}
|
|
}
|
|
|
|
/** A map of which observers listen to state changes from which {@link Activity}. */
|
|
private static final Map<Activity, ActivityInfo> sActivityInfo =
|
|
Collections.synchronizedMap(new HashMap<Activity, ActivityInfo>());
|
|
|
|
/** A map to cache TaskId for each {@link Activity}. */
|
|
public static final Map<Activity, Integer> sActivityTaskId =
|
|
Collections.synchronizedMap(new HashMap<Activity, Integer>());
|
|
|
|
// Shared preferences key for TaskId caching of an activity.
|
|
private static final String CACHE_ACTIVITY_TASKID_KEY = "cache_activity_taskid_enabled";
|
|
|
|
@SuppressLint("SupportAnnotationUsage")
|
|
@ApplicationState
|
|
@GuardedBy("sActivityInfo")
|
|
// The getStateForApplication() historically returned ApplicationState.HAS_DESTROYED_ACTIVITIES
|
|
// when no activity has been observed.
|
|
private static int sCurrentApplicationState = ApplicationState.UNKNOWN;
|
|
|
|
/** Last activity that was shown (or null if none or it was destroyed). */
|
|
@SuppressLint("StaticFieldLeak")
|
|
private static Activity sActivity;
|
|
|
|
/** A lazily initialized listener that forwards application state changes to native. */
|
|
private static ApplicationStateListener sNativeApplicationStateListener;
|
|
|
|
/** A list of observers to be notified when any {@link Activity} has a state change. */
|
|
private static ObserverList<ActivityStateListener> sGeneralActivityStateListeners;
|
|
|
|
/**
|
|
* A list of observers to be notified when the visibility state of this {@link Application}
|
|
* changes. See {@link #getStateForApplication()}.
|
|
*/
|
|
private static ObserverList<ApplicationStateListener> sApplicationStateListeners;
|
|
|
|
/**
|
|
* A list of observers to be notified when the window focus changes.
|
|
* See {@link #registerWindowFocusChangedListener}.
|
|
*/
|
|
private static ObserverList<WindowFocusChangedListener> sWindowFocusListeners;
|
|
|
|
/** A list of observers to be notified when the visibility of any task changes. */
|
|
private static ObserverList<TaskVisibilityListener> sTaskVisibilityListeners;
|
|
|
|
/** Interface to be implemented by listeners. */
|
|
public interface ApplicationStateListener {
|
|
/**
|
|
* Called when the application's state changes.
|
|
*
|
|
* @param newState The application state.
|
|
*/
|
|
void onApplicationStateChange(@ApplicationState 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.
|
|
*/
|
|
void onActivityStateChange(Activity activity, @ActivityState int newState);
|
|
}
|
|
|
|
/** Interface to be implemented by listeners for window focus events. */
|
|
public interface WindowFocusChangedListener {
|
|
/**
|
|
* Called when the window focus changes for {@code activity}.
|
|
*
|
|
* @param activity The {@link Activity} that has a window focus changed event.
|
|
* @param hasFocus Whether or not {@code activity} gained or lost focus.
|
|
*/
|
|
public void onWindowFocusChanged(Activity activity, boolean hasFocus);
|
|
}
|
|
|
|
/** Interface to be implemented by listeners for task visibility changes. */
|
|
public interface TaskVisibilityListener {
|
|
/**
|
|
* Called when the visibility of a task changes.
|
|
*
|
|
* @param taskId The unique Id of the task that changed visibility.
|
|
* @param isVisible The new visibility state of the task.
|
|
*/
|
|
void onTaskVisibilityChanged(int taskId, boolean isVisible);
|
|
}
|
|
|
|
private ApplicationStatus() {}
|
|
|
|
/**
|
|
* Registers a listener to receive window focus updates on activities in this application.
|
|
*
|
|
* @param listener Listener to receive window focus events.
|
|
*/
|
|
@MainThread
|
|
public static void registerWindowFocusChangedListener(WindowFocusChangedListener listener) {
|
|
assert isInitialized();
|
|
if (sWindowFocusListeners == null) sWindowFocusListeners = new ObserverList<>();
|
|
sWindowFocusListeners.addObserver(listener);
|
|
}
|
|
|
|
/**
|
|
* Unregisters a listener from receiving window focus updates on activities in this application.
|
|
*
|
|
* @param listener Listener that doesn't want to receive window focus events.
|
|
*/
|
|
@MainThread
|
|
public static void unregisterWindowFocusChangedListener(WindowFocusChangedListener listener) {
|
|
if (sWindowFocusListeners == null) return;
|
|
sWindowFocusListeners.removeObserver(listener);
|
|
}
|
|
|
|
/**
|
|
* Register a listener to receive task visibility updates.
|
|
*
|
|
* @param listener Listener to receive task visibility events.
|
|
*/
|
|
@MainThread
|
|
public static void registerTaskVisibilityListener(TaskVisibilityListener listener) {
|
|
assert isInitialized();
|
|
if (sTaskVisibilityListeners == null) sTaskVisibilityListeners = new ObserverList<>();
|
|
sTaskVisibilityListeners.addObserver(listener);
|
|
}
|
|
|
|
/**
|
|
* Unregisters a listener from receiving task visibility updates.
|
|
*
|
|
* @param listener Listener that doesn't want to receive task visibility events.
|
|
*/
|
|
@MainThread
|
|
public static void unregisterTaskVisibilityListener(TaskVisibilityListener listener) {
|
|
if (sTaskVisibilityListeners == null) return;
|
|
sTaskVisibilityListeners.removeObserver(listener);
|
|
}
|
|
|
|
public static void setCachingEnabled(boolean enabled) {
|
|
SharedPreferences.Editor editor = ContextUtils.getAppSharedPreferences().edit();
|
|
editor.putBoolean(CACHE_ACTIVITY_TASKID_KEY, enabled).apply();
|
|
}
|
|
|
|
public static boolean isCachingEnabled() {
|
|
try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
|
|
return ContextUtils.getAppSharedPreferences()
|
|
.getBoolean(CACHE_ACTIVITY_TASKID_KEY, false);
|
|
}
|
|
}
|
|
|
|
public static int getTaskId(Activity activity) {
|
|
if (!isCachingEnabled()) return activity.getTaskId();
|
|
|
|
if (!sActivityTaskId.containsKey(activity)) {
|
|
synchronized (sActivityTaskId) {
|
|
sActivityTaskId.put(activity, activity.getTaskId());
|
|
}
|
|
}
|
|
return sActivityTaskId.get(activity);
|
|
}
|
|
|
|
/**
|
|
* Intercepts calls to an existing Window.Callback. Most invocations are passed on directly
|
|
* to the composed Window.Callback but enables intercepting/manipulating others.
|
|
* <p>
|
|
* This is used to relay window focus changes throughout the app and remedy a bug in the
|
|
* appcompat library.
|
|
*/
|
|
@VisibleForTesting
|
|
static class WindowCallbackProxy implements InvocationHandler {
|
|
private final Window.Callback mCallback;
|
|
private final Activity mActivity;
|
|
|
|
public WindowCallbackProxy(Activity activity, Window.Callback callback) {
|
|
mCallback = callback;
|
|
mActivity = activity;
|
|
}
|
|
|
|
@Override
|
|
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
|
if (method.getName().equals("onWindowFocusChanged")
|
|
&& args.length == 1
|
|
&& args[0] instanceof Boolean) {
|
|
onWindowFocusChanged((boolean) args[0]);
|
|
return null;
|
|
} else {
|
|
try {
|
|
return method.invoke(mCallback, args);
|
|
} catch (InvocationTargetException e) {
|
|
// Special-case for when a method is not defined on the underlying
|
|
// Window.Callback object. Because we're using a Proxy to forward all method
|
|
// calls, this breaks the Android framework's handling for apps built against
|
|
// an older SDK. The framework expects an AbstractMethodError but due to
|
|
// reflection it becomes wrapped inside an InvocationTargetException. Undo the
|
|
// wrapping to signal the framework accordingly.
|
|
if (e.getCause() instanceof AbstractMethodError) {
|
|
throw e.getCause();
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void onWindowFocusChanged(boolean hasFocus) {
|
|
mCallback.onWindowFocusChanged(hasFocus);
|
|
|
|
if (sWindowFocusListeners != null) {
|
|
for (WindowFocusChangedListener listener : sWindowFocusListeners) {
|
|
listener.onWindowFocusChanged(mActivity, hasFocus);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public static boolean isInitialized() {
|
|
synchronized (sActivityInfo) {
|
|
return sCurrentApplicationState != ApplicationState.UNKNOWN;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initializes the activity status for a specified application.
|
|
*
|
|
* @param application The application whose status you wish to monitor.
|
|
*/
|
|
@MainThread
|
|
public static void initialize(Application application) {
|
|
assert !isInitialized();
|
|
synchronized (sActivityInfo) {
|
|
sCurrentApplicationState = ApplicationState.HAS_DESTROYED_ACTIVITIES;
|
|
}
|
|
|
|
registerWindowFocusChangedListener(
|
|
new 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);
|
|
Window.Callback callback = activity.getWindow().getCallback();
|
|
activity.getWindow()
|
|
.setCallback(createWindowCallbackProxy(activity, callback));
|
|
}
|
|
|
|
@Override
|
|
public void onActivityDestroyed(Activity activity) {
|
|
onStateChange(activity, ActivityState.DESTROYED);
|
|
checkCallback(activity);
|
|
}
|
|
|
|
@Override
|
|
public void onActivityPaused(Activity activity) {
|
|
onStateChange(activity, ActivityState.PAUSED);
|
|
checkCallback(activity);
|
|
}
|
|
|
|
@Override
|
|
public void onActivityResumed(Activity activity) {
|
|
onStateChange(activity, ActivityState.RESUMED);
|
|
checkCallback(activity);
|
|
}
|
|
|
|
@Override
|
|
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
|
|
checkCallback(activity);
|
|
}
|
|
|
|
@Override
|
|
public void onActivityStarted(Activity activity) {
|
|
onStateChange(activity, ActivityState.STARTED);
|
|
checkCallback(activity);
|
|
}
|
|
|
|
@Override
|
|
public void onActivityStopped(Activity activity) {
|
|
onStateChange(activity, ActivityState.STOPPED);
|
|
checkCallback(activity);
|
|
}
|
|
|
|
private void checkCallback(Activity activity) {
|
|
if (BuildConfig.ENABLE_ASSERTS) {
|
|
assert reachesWindowCallback(activity.getWindow().getCallback());
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
@VisibleForTesting
|
|
static Window.Callback createWindowCallbackProxy(Activity activity, Window.Callback callback) {
|
|
return (Window.Callback)
|
|
Proxy.newProxyInstance(
|
|
Window.Callback.class.getClassLoader(),
|
|
new Class[] {Window.Callback.class},
|
|
new ApplicationStatus.WindowCallbackProxy(activity, callback));
|
|
}
|
|
|
|
/**
|
|
* Tries to trace down to our WindowCallbackProxy from the given callback.
|
|
* Since the callback can be overwritten by embedder code we try to ensure
|
|
* that there at least seem to be a reference back to our callback by
|
|
* checking the declared fields of the given callback using reflection.
|
|
*/
|
|
@VisibleForTesting
|
|
static boolean reachesWindowCallback(@Nullable Window.Callback callback) {
|
|
if (callback == null) return false;
|
|
if (callback.getClass().getName().equals(TOOLBAR_CALLBACK_WRAPPER_CLASS)) {
|
|
// We're actually not going to get called, see AndroidX report here:
|
|
// https://issuetracker.google.com/issues/155165145.
|
|
// But this was accepted in the old code as well so mimic that until
|
|
// AndroidX is fixed and updated.
|
|
return true;
|
|
}
|
|
if (Proxy.isProxyClass(callback.getClass())) {
|
|
return Proxy.getInvocationHandler(callback)
|
|
instanceof ApplicationStatus.WindowCallbackProxy;
|
|
}
|
|
for (Class<?> c = callback.getClass(); c != Object.class; c = c.getSuperclass()) {
|
|
for (Field f : c.getDeclaredFields()) {
|
|
if (f.getType().isAssignableFrom(Window.Callback.class)) {
|
|
boolean isAccessible = f.isAccessible();
|
|
f.setAccessible(true);
|
|
Window.Callback fieldCb;
|
|
try {
|
|
fieldCb = (Window.Callback) f.get(callback);
|
|
} catch (IllegalAccessException ex) {
|
|
continue;
|
|
} finally {
|
|
f.setAccessible(isAccessible);
|
|
}
|
|
if (reachesWindowCallback(fieldCb)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 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, @ActivityState int newState) {
|
|
assert activity != null;
|
|
|
|
if (sActivity == null
|
|
|| newState == ActivityState.CREATED
|
|
|| newState == ActivityState.RESUMED
|
|
|| newState == ActivityState.STARTED) {
|
|
sActivity = activity;
|
|
}
|
|
|
|
int oldApplicationState = getStateForApplication();
|
|
boolean oldTaskVisibility = isTaskVisible(getTaskId(activity));
|
|
ActivityInfo info;
|
|
|
|
synchronized (sActivityInfo) {
|
|
if (newState == ActivityState.CREATED) {
|
|
assert !sActivityInfo.containsKey(activity);
|
|
sActivityInfo.put(activity, new ActivityInfo());
|
|
}
|
|
|
|
info = sActivityInfo.get(activity);
|
|
info.setStatus(newState);
|
|
|
|
// Remove before calling listeners so that isEveryActivityDestroyed() returns false when
|
|
// this was the last activity.
|
|
if (newState == ActivityState.DESTROYED) {
|
|
sActivityInfo.remove(activity);
|
|
if (activity == sActivity) sActivity = null;
|
|
}
|
|
|
|
sCurrentApplicationState = determineApplicationStateLocked();
|
|
}
|
|
|
|
// 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.
|
|
if (sGeneralActivityStateListeners != null) {
|
|
for (ActivityStateListener listener : sGeneralActivityStateListeners) {
|
|
listener.onActivityStateChange(activity, newState);
|
|
}
|
|
}
|
|
|
|
boolean taskVisibility = isTaskVisible(getTaskId(activity));
|
|
if (taskVisibility != oldTaskVisibility && sTaskVisibilityListeners != null) {
|
|
for (TaskVisibilityListener listener : sTaskVisibilityListeners) {
|
|
listener.onTaskVisibilityChanged(getTaskId(activity), taskVisibility);
|
|
}
|
|
}
|
|
|
|
int applicationState = getStateForApplication();
|
|
if (applicationState != oldApplicationState && sApplicationStateListeners != null) {
|
|
for (ApplicationStateListener listener : sApplicationStateListeners) {
|
|
listener.onApplicationStateChange(applicationState);
|
|
}
|
|
}
|
|
synchronized (sActivityTaskId) {
|
|
if (newState == ActivityState.DESTROYED) {
|
|
sActivityTaskId.remove(activity);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Testing method to update the state of the specified activity. */
|
|
@VisibleForTesting
|
|
@MainThread
|
|
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.
|
|
*/
|
|
@MainThread
|
|
public static Activity getLastTrackedFocusedActivity() {
|
|
return sActivity;
|
|
}
|
|
|
|
/**
|
|
* @return A {@link List} of all non-destroyed {@link Activity}s.
|
|
*/
|
|
@AnyThread
|
|
public static List<Activity> getRunningActivities() {
|
|
assert isInitialized();
|
|
synchronized (sActivityInfo) {
|
|
return new ArrayList<>(sActivityInfo.keySet());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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}).
|
|
*/
|
|
@ActivityState
|
|
@AnyThread
|
|
public static int getStateForActivity(@Nullable Activity activity) {
|
|
assert isInitialized();
|
|
if (activity == null) return ActivityState.DESTROYED;
|
|
ActivityInfo info = sActivityInfo.get(activity);
|
|
return info != null ? info.getStatus() : ActivityState.DESTROYED;
|
|
}
|
|
|
|
/**
|
|
* @return The state of the application (see {@link ApplicationState}).
|
|
*/
|
|
@AnyThread
|
|
@ApplicationState
|
|
@CalledByNative
|
|
public static int getStateForApplication() {
|
|
synchronized (sActivityInfo) {
|
|
return sCurrentApplicationState;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
@AnyThread
|
|
@CalledByNative
|
|
public static boolean hasVisibleActivities() {
|
|
assert isInitialized();
|
|
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.
|
|
*/
|
|
@AnyThread
|
|
public static boolean isEveryActivityDestroyed() {
|
|
assert isInitialized();
|
|
return sActivityInfo.isEmpty();
|
|
}
|
|
|
|
/**
|
|
* Returns the visibility of the task with the given taskId. A task is visible if any of its
|
|
* Activities are in RESUMED or PAUSED state.
|
|
*
|
|
* @param taskId The id of the task whose visibility needs to be checked.
|
|
* @return Whether the task is visible or not.
|
|
*/
|
|
@AnyThread
|
|
public static boolean isTaskVisible(int taskId) {
|
|
assert isInitialized();
|
|
for (Map.Entry<Activity, ActivityInfo> entry : sActivityInfo.entrySet()) {
|
|
if (getTaskId(entry.getKey()) == taskId) {
|
|
@ActivityState int state = entry.getValue().getStatus();
|
|
if (state == ActivityState.RESUMED || state == ActivityState.PAUSED) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Registers the given listener to receive state changes for all activities.
|
|
*
|
|
* @param listener Listener to receive state changes.
|
|
*/
|
|
@MainThread
|
|
public static void registerStateListenerForAllActivities(ActivityStateListener listener) {
|
|
assert isInitialized();
|
|
if (sGeneralActivityStateListeners == null) {
|
|
sGeneralActivityStateListeners = new ObserverList<>();
|
|
}
|
|
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.
|
|
*/
|
|
@MainThread
|
|
@SuppressLint("NewApi")
|
|
public static void registerStateListenerForActivity(
|
|
ActivityStateListener listener, Activity activity) {
|
|
assert isInitialized();
|
|
assert activity != null;
|
|
|
|
ActivityInfo info = sActivityInfo.get(activity);
|
|
assert info != null
|
|
: "destroyed: " + activity.isDestroyed() + " finishing: " + activity.isFinishing();
|
|
assert 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.
|
|
*/
|
|
@MainThread
|
|
public static void unregisterActivityStateListener(ActivityStateListener listener) {
|
|
if (sGeneralActivityStateListeners != null) {
|
|
sGeneralActivityStateListeners.removeObserver(listener);
|
|
}
|
|
|
|
// Loop through all observer lists for all activities and remove the listener.
|
|
synchronized (sActivityInfo) {
|
|
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.
|
|
*/
|
|
@MainThread
|
|
public static void registerApplicationStateListener(ApplicationStateListener listener) {
|
|
if (sApplicationStateListeners == null) {
|
|
sApplicationStateListeners = new ObserverList<>();
|
|
}
|
|
sApplicationStateListeners.addObserver(listener);
|
|
}
|
|
|
|
/**
|
|
* Unregisters the given listener from receiving state changes.
|
|
*
|
|
* @param listener Listener that doesn't want to receive state changes.
|
|
*/
|
|
@MainThread
|
|
public static void unregisterApplicationStateListener(ApplicationStateListener listener) {
|
|
if (sApplicationStateListeners == null) return;
|
|
sApplicationStateListeners.removeObserver(listener);
|
|
}
|
|
|
|
/**
|
|
* Robolectric JUnit tests create a new application between each test, while all the context
|
|
* in static classes isn't reset. This function allows to reset the application status to avoid
|
|
* being in a dirty state.
|
|
*/
|
|
@MainThread
|
|
public static void destroyForJUnitTests() {
|
|
synchronized (sActivityInfo) {
|
|
if (sApplicationStateListeners != null) sApplicationStateListeners.clear();
|
|
if (sGeneralActivityStateListeners != null) sGeneralActivityStateListeners.clear();
|
|
if (sTaskVisibilityListeners != null) sTaskVisibilityListeners.clear();
|
|
sActivityInfo.clear();
|
|
if (sWindowFocusListeners != null) sWindowFocusListeners.clear();
|
|
sCurrentApplicationState = ApplicationState.UNKNOWN;
|
|
sActivity = null;
|
|
sNativeApplicationStateListener = null;
|
|
}
|
|
}
|
|
|
|
/** Mark all Activities as destroyed to avoid side-effects in future test. */
|
|
@MainThread
|
|
public static void resetActivitiesForInstrumentationTests() {
|
|
assert ThreadUtils.runningOnUiThread();
|
|
|
|
synchronized (sActivityInfo) {
|
|
// Copy the set to avoid concurrent modifications to the underlying set.
|
|
for (Activity activity : new HashSet<>(sActivityInfo.keySet())) {
|
|
assert activity.getApplication() == null
|
|
: "Real activities that are launched should be closed by test code "
|
|
+ "and not rely on this cleanup of mocks.";
|
|
onStateChangeForTesting(activity, ActivityState.DESTROYED);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
ApplicationStatusJni.get()
|
|
.onApplicationStateChange(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.
|
|
*/
|
|
@ApplicationState
|
|
@GuardedBy("sActivityInfo")
|
|
private static int determineApplicationStateLocked() {
|
|
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;
|
|
}
|
|
|
|
@NativeMethods
|
|
interface Natives {
|
|
// Called to notify the native side of state changes.
|
|
// IMPORTANT: This is always called on the main thread!
|
|
void onApplicationStateChange(@ApplicationState int newState);
|
|
}
|
|
}
|