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.content.Context; 11 import android.os.Bundle; 12 13 import java.lang.ref.WeakReference; 14 import java.util.ArrayList; 15 import java.util.List; 16 import java.util.Map; 17 import 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") 24 public 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 */ getStatus()33 public int getStatus() { 34 return mStatus; 35 } 36 37 /** 38 * @param status The new {@link ActivityState} of the activity. 39 */ setStatus(int status)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 */ getListeners()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 */ onApplicationStateChange(int newState)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 */ onActivityStateChange(Activity activity, int newState)102 public void onActivityStateChange(Activity activity, int newState); 103 } 104 ApplicationStatus()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 */ initialize(BaseChromiumApplication application)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 */ 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 */ onStateChangeForTesting(Activity activity, int newState)225 public static void onStateChangeForTesting(Activity activity, int newState) { 226 onStateChange(activity, newState); 227 } 228 229 /** 230 * @return The most recent focused {@link Activity} tracked by this class. Being focused means 231 * out of all the activities tracked here, it has most recently gained window focus. 232 */ getLastTrackedFocusedActivity()233 public static Activity getLastTrackedFocusedActivity() { 234 return sActivity; 235 } 236 237 /** 238 * @return A {@link List} of all non-destroyed {@link Activity}s. 239 */ getRunningActivities()240 public static List<WeakReference<Activity>> getRunningActivities() { 241 List<WeakReference<Activity>> activities = new ArrayList<WeakReference<Activity>>(); 242 for (Activity activity : sActivityInfo.keySet()) { 243 activities.add(new WeakReference<Activity>(activity)); 244 } 245 return activities; 246 } 247 248 /** 249 * @return The {@link Context} for the {@link Application}. 250 */ getApplicationContext()251 public static Context getApplicationContext() { 252 return sApplication != null ? sApplication.getApplicationContext() : null; 253 } 254 255 /** 256 * Query the state for a given activity. If the activity is not being tracked, this will 257 * return {@link ActivityState#DESTROYED}. 258 * 259 * <p> 260 * Please note that Chrome can have multiple activities running simultaneously. Please also 261 * look at {@link #getStateForApplication()} for more details. 262 * 263 * <p> 264 * When relying on this method, be familiar with the expected life cycle state 265 * transitions: 266 * <a href="http://developer.android.com/guide/components/activities.html#Lifecycle"> 267 * Activity Lifecycle 268 * </a> 269 * 270 * <p> 271 * During activity transitions (activity B launching in front of activity A), A will completely 272 * paused before the creation of activity B begins. 273 * 274 * <p> 275 * A basic flow for activity A starting, followed by activity B being opened and then closed: 276 * <ul> 277 * <li> -- Starting Activity A -- 278 * <li> Activity A - ActivityState.CREATED 279 * <li> Activity A - ActivityState.STARTED 280 * <li> Activity A - ActivityState.RESUMED 281 * <li> -- Starting Activity B -- 282 * <li> Activity A - ActivityState.PAUSED 283 * <li> Activity B - ActivityState.CREATED 284 * <li> Activity B - ActivityState.STARTED 285 * <li> Activity B - ActivityState.RESUMED 286 * <li> Activity A - ActivityState.STOPPED 287 * <li> -- Closing Activity B, Activity A regaining focus -- 288 * <li> Activity B - ActivityState.PAUSED 289 * <li> Activity A - ActivityState.STARTED 290 * <li> Activity A - ActivityState.RESUMED 291 * <li> Activity B - ActivityState.STOPPED 292 * <li> Activity B - ActivityState.DESTROYED 293 * </ul> 294 * 295 * @param activity The activity whose state is to be returned. 296 * @return The state of the specified activity (see {@link ActivityState}). 297 */ getStateForActivity(Activity activity)298 public static int getStateForActivity(Activity activity) { 299 ActivityInfo info = sActivityInfo.get(activity); 300 return info != null ? info.getStatus() : ActivityState.DESTROYED; 301 } 302 303 /** 304 * @return The state of the application (see {@link ApplicationState}). 305 */ getStateForApplication()306 public static int getStateForApplication() { 307 synchronized (sCachedApplicationStateLock) { 308 if (sCachedApplicationState == null) { 309 sCachedApplicationState = determineApplicationState(); 310 } 311 } 312 313 return sCachedApplicationState.intValue(); 314 } 315 316 /** 317 * Checks whether or not any Activity in this Application is visible to the user. Note that 318 * this includes the PAUSED state, which can happen when the Activity is temporarily covered 319 * by another Activity's Fragment (e.g.). 320 * @return Whether any Activity under this Application is visible. 321 */ hasVisibleActivities()322 public static boolean hasVisibleActivities() { 323 int state = getStateForApplication(); 324 return state == ApplicationState.HAS_RUNNING_ACTIVITIES 325 || state == ApplicationState.HAS_PAUSED_ACTIVITIES; 326 } 327 328 /** 329 * Checks to see if there are any active Activity instances being watched by ApplicationStatus. 330 * @return True if all Activities have been destroyed. 331 */ isEveryActivityDestroyed()332 public static boolean isEveryActivityDestroyed() { 333 return sActivityInfo.isEmpty(); 334 } 335 336 /** 337 * Registers the given listener to receive state changes for all activities. 338 * @param listener Listener to receive state changes. 339 */ registerStateListenerForAllActivities(ActivityStateListener listener)340 public static void registerStateListenerForAllActivities(ActivityStateListener listener) { 341 sGeneralActivityStateListeners.addObserver(listener); 342 } 343 344 /** 345 * Registers the given listener to receive state changes for {@code activity}. After a call to 346 * {@link ActivityStateListener#onActivityStateChange(Activity, int)} with 347 * {@link ActivityState#DESTROYED} all listeners associated with that particular 348 * {@link Activity} are removed. 349 * @param listener Listener to receive state changes. 350 * @param activity Activity to track or {@code null} to track all activities. 351 */ registerStateListenerForActivity(ActivityStateListener listener, Activity activity)352 public static void registerStateListenerForActivity(ActivityStateListener listener, 353 Activity activity) { 354 assert activity != null; 355 356 ActivityInfo info = sActivityInfo.get(activity); 357 assert info != null && info.getStatus() != ActivityState.DESTROYED; 358 info.getListeners().addObserver(listener); 359 } 360 361 /** 362 * Unregisters the given listener from receiving activity state changes. 363 * @param listener Listener that doesn't want to receive state changes. 364 */ unregisterActivityStateListener(ActivityStateListener listener)365 public static void unregisterActivityStateListener(ActivityStateListener listener) { 366 sGeneralActivityStateListeners.removeObserver(listener); 367 368 // Loop through all observer lists for all activities and remove the listener. 369 for (ActivityInfo info : sActivityInfo.values()) { 370 info.getListeners().removeObserver(listener); 371 } 372 } 373 374 /** 375 * Registers the given listener to receive state changes for the application. 376 * @param listener Listener to receive state state changes. 377 */ registerApplicationStateListener(ApplicationStateListener listener)378 public static void registerApplicationStateListener(ApplicationStateListener listener) { 379 sApplicationStateListeners.addObserver(listener); 380 } 381 382 /** 383 * Unregisters the given listener from receiving state changes. 384 * @param listener Listener that doesn't want to receive state changes. 385 */ unregisterApplicationStateListener(ApplicationStateListener listener)386 public static void unregisterApplicationStateListener(ApplicationStateListener listener) { 387 sApplicationStateListeners.removeObserver(listener); 388 } 389 390 /** 391 * Registers the single thread-safe native activity status listener. 392 * This handles the case where the caller is not on the main thread. 393 * Note that this is used by a leaky singleton object from the native 394 * side, hence lifecycle management is greatly simplified. 395 */ 396 @CalledByNative registerThreadSafeNativeApplicationStateListener()397 private static void registerThreadSafeNativeApplicationStateListener() { 398 ThreadUtils.runOnUiThread(new Runnable () { 399 @Override 400 public void run() { 401 if (sNativeApplicationStateListener != null) return; 402 403 sNativeApplicationStateListener = new ApplicationStateListener() { 404 @Override 405 public void onApplicationStateChange(int newState) { 406 nativeOnApplicationStateChange(newState); 407 } 408 }; 409 registerApplicationStateListener(sNativeApplicationStateListener); 410 } 411 }); 412 } 413 414 /** 415 * Determines the current application state as defined by {@link ApplicationState}. This will 416 * loop over all the activities and check their state to determine what the general application 417 * state should be. 418 * @return HAS_RUNNING_ACTIVITIES if any activity is not paused, stopped, or destroyed. 419 * HAS_PAUSED_ACTIVITIES if none are running and one is paused. 420 * HAS_STOPPED_ACTIVITIES if none are running/paused and one is stopped. 421 * HAS_DESTROYED_ACTIVITIES if none are running/paused/stopped. 422 */ determineApplicationState()423 private static int determineApplicationState() { 424 boolean hasPausedActivity = false; 425 boolean hasStoppedActivity = false; 426 427 for (ActivityInfo info : sActivityInfo.values()) { 428 int state = info.getStatus(); 429 if (state != ActivityState.PAUSED 430 && state != ActivityState.STOPPED 431 && state != ActivityState.DESTROYED) { 432 return ApplicationState.HAS_RUNNING_ACTIVITIES; 433 } else if (state == ActivityState.PAUSED) { 434 hasPausedActivity = true; 435 } else if (state == ActivityState.STOPPED) { 436 hasStoppedActivity = true; 437 } 438 } 439 440 if (hasPausedActivity) return ApplicationState.HAS_PAUSED_ACTIVITIES; 441 if (hasStoppedActivity) return ApplicationState.HAS_STOPPED_ACTIVITIES; 442 return ApplicationState.HAS_DESTROYED_ACTIVITIES; 443 } 444 445 // Called to notify the native side of state changes. 446 // IMPORTANT: This is always called on the main thread! nativeOnApplicationStateChange(int newState)447 private static native void nativeOnApplicationStateChange(int newState); 448 } 449