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