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.annotation.SuppressLint; 8 import android.app.Activity; 9 import android.app.Application; 10 import android.app.Application.ActivityLifecycleCallbacks; 11 import android.os.Bundle; 12 import android.support.annotation.Nullable; 13 import android.view.Window; 14 15 import org.chromium.base.annotations.CalledByNative; 16 import org.chromium.base.annotations.JNINamespace; 17 18 import java.lang.ref.WeakReference; 19 import java.lang.reflect.InvocationHandler; 20 import java.lang.reflect.InvocationTargetException; 21 import java.lang.reflect.Method; 22 import java.lang.reflect.Proxy; 23 import java.util.ArrayList; 24 import java.util.List; 25 import java.util.Map; 26 import java.util.concurrent.ConcurrentHashMap; 27 28 /** 29 * Provides information about the current activity's status, and a way 30 * to register / unregister listeners for state changes. 31 */ 32 @JNINamespace("base::android") 33 public class ApplicationStatus { 34 private static final String TOOLBAR_CALLBACK_INTERNAL_WRAPPER_CLASS = 35 "android.support.v7.internal.app.ToolbarActionBar$ToolbarCallbackWrapper"; 36 // In builds using the --use_unpublished_apis flag, the ToolbarActionBar class name does not 37 // include the "internal" package. 38 private static final String TOOLBAR_CALLBACK_WRAPPER_CLASS = 39 "android.support.v7.app.ToolbarActionBar$ToolbarCallbackWrapper"; 40 private static final String WINDOW_PROFILER_CALLBACK = 41 "com.android.tools.profiler.support.event.WindowProfilerCallback"; 42 43 private static class ActivityInfo { 44 private int mStatus = ActivityState.DESTROYED; 45 private ObserverList<ActivityStateListener> mListeners = new ObserverList<>(); 46 47 /** 48 * @return The current {@link ActivityState} of the activity. 49 */ 50 @ActivityState getStatus()51 public int getStatus() { 52 return mStatus; 53 } 54 55 /** 56 * @param status The new {@link ActivityState} of the activity. 57 */ setStatus(@ctivityState int status)58 public void setStatus(@ActivityState int status) { 59 mStatus = status; 60 } 61 62 /** 63 * @return A list of {@link ActivityStateListener}s listening to this activity. 64 */ getListeners()65 public ObserverList<ActivityStateListener> getListeners() { 66 return mListeners; 67 } 68 } 69 70 static { 71 // Chrome initializes this only for the main process. This assert aims to try and catch 72 // usages from GPU / renderers, while still allowing tests. 73 assert ContextUtils.isMainProcess() 74 || ContextUtils.getProcessName().contains(":test") 75 : "Cannot use ApplicationState from process: " ContextUtils.getProcessName()76 + ContextUtils.getProcessName(); 77 } 78 79 private static final Object sCurrentApplicationStateLock = new Object(); 80 81 @SuppressLint("SupportAnnotationUsage") 82 @ApplicationState 83 // The getStateForApplication() historically returned ApplicationState.HAS_DESTROYED_ACTIVITIES 84 // when no activity has been observed. 85 private static Integer sCurrentApplicationState = ApplicationState.HAS_DESTROYED_ACTIVITIES; 86 87 /** Last activity that was shown (or null if none or it was destroyed). */ 88 @SuppressLint("StaticFieldLeak") 89 private static Activity sActivity; 90 91 /** A lazily initialized listener that forwards application state changes to native. */ 92 private static ApplicationStateListener sNativeApplicationStateListener; 93 94 private static boolean sIsInitialized; 95 96 /** 97 * A map of which observers listen to state changes from which {@link Activity}. 98 */ 99 private static final Map<Activity, ActivityInfo> sActivityInfo = new ConcurrentHashMap<>(); 100 101 /** 102 * A list of observers to be notified when any {@link Activity} has a state change. 103 */ 104 private static final ObserverList<ActivityStateListener> sGeneralActivityStateListeners = 105 new ObserverList<>(); 106 107 /** 108 * A list of observers to be notified when the visibility state of this {@link Application} 109 * changes. See {@link #getStateForApplication()}. 110 */ 111 private static final ObserverList<ApplicationStateListener> sApplicationStateListeners = 112 new ObserverList<>(); 113 114 /** 115 * A list of observers to be notified when the window focus changes. 116 * See {@link #registerWindowFocusChangedListener}. 117 */ 118 private static final ObserverList<WindowFocusChangedListener> sWindowFocusListeners = 119 new ObserverList<>(); 120 121 /** 122 * Interface to be implemented by listeners. 123 */ 124 public interface ApplicationStateListener { 125 /** 126 * Called when the application's state changes. 127 * @param newState The application state. 128 */ onApplicationStateChange(@pplicationState int newState)129 void onApplicationStateChange(@ApplicationState int newState); 130 } 131 132 /** 133 * Interface to be implemented by listeners. 134 */ 135 public interface ActivityStateListener { 136 /** 137 * Called when the activity's state changes. 138 * @param activity The activity that had a state change. 139 * @param newState New activity state. 140 */ onActivityStateChange(Activity activity, @ActivityState int newState)141 void onActivityStateChange(Activity activity, @ActivityState int newState); 142 } 143 144 /** 145 * Interface to be implemented by listeners for window focus events. 146 */ 147 public interface WindowFocusChangedListener { 148 /** 149 * Called when the window focus changes for {@code activity}. 150 * @param activity The {@link Activity} that has a window focus changed event. 151 * @param hasFocus Whether or not {@code activity} gained or lost focus. 152 */ onWindowFocusChanged(Activity activity, boolean hasFocus)153 public void onWindowFocusChanged(Activity activity, boolean hasFocus); 154 } 155 ApplicationStatus()156 private ApplicationStatus() {} 157 158 /** 159 * Registers a listener to receive window focus updates on activities in this application. 160 * @param listener Listener to receive window focus events. 161 */ registerWindowFocusChangedListener(WindowFocusChangedListener listener)162 public static void registerWindowFocusChangedListener(WindowFocusChangedListener listener) { 163 sWindowFocusListeners.addObserver(listener); 164 } 165 166 /** 167 * Unregisters a listener from receiving window focus updates on activities in this application. 168 * @param listener Listener that doesn't want to receive window focus events. 169 */ unregisterWindowFocusChangedListener(WindowFocusChangedListener listener)170 public static void unregisterWindowFocusChangedListener(WindowFocusChangedListener listener) { 171 sWindowFocusListeners.removeObserver(listener); 172 } 173 174 /** 175 * Intercepts calls to an existing Window.Callback. Most invocations are passed on directly 176 * to the composed Window.Callback but enables intercepting/manipulating others. 177 * 178 * This is used to relay window focus changes throughout the app and remedy a bug in the 179 * appcompat library. 180 */ 181 private static class WindowCallbackProxy implements InvocationHandler { 182 private final Window.Callback mCallback; 183 private final Activity mActivity; 184 WindowCallbackProxy(Activity activity, Window.Callback callback)185 public WindowCallbackProxy(Activity activity, Window.Callback callback) { 186 mCallback = callback; 187 mActivity = activity; 188 } 189 190 @Override invoke(Object proxy, Method method, Object[] args)191 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 192 if (method.getName().equals("onWindowFocusChanged") && args.length == 1 193 && args[0] instanceof Boolean) { 194 onWindowFocusChanged((boolean) args[0]); 195 return null; 196 } else { 197 try { 198 return method.invoke(mCallback, args); 199 } catch (InvocationTargetException e) { 200 // Special-case for when a method is not defined on the underlying 201 // Window.Callback object. Because we're using a Proxy to forward all method 202 // calls, this breaks the Android framework's handling for apps built against 203 // an older SDK. The framework expects an AbstractMethodError but due to 204 // reflection it becomes wrapped inside an InvocationTargetException. Undo the 205 // wrapping to signal the framework accordingly. 206 if (e.getCause() instanceof AbstractMethodError) { 207 throw e.getCause(); 208 } 209 throw e; 210 } 211 } 212 } 213 onWindowFocusChanged(boolean hasFocus)214 public void onWindowFocusChanged(boolean hasFocus) { 215 mCallback.onWindowFocusChanged(hasFocus); 216 217 for (WindowFocusChangedListener listener : sWindowFocusListeners) { 218 listener.onWindowFocusChanged(mActivity, hasFocus); 219 } 220 } 221 } 222 223 /** 224 * Initializes the activity status for a specified application. 225 * 226 * @param application The application whose status you wish to monitor. 227 */ initialize(Application application)228 public static void initialize(Application application) { 229 if (sIsInitialized) return; 230 sIsInitialized = true; 231 232 registerWindowFocusChangedListener(new WindowFocusChangedListener() { 233 @Override 234 public void onWindowFocusChanged(Activity activity, boolean hasFocus) { 235 if (!hasFocus || activity == sActivity) return; 236 237 int state = getStateForActivity(activity); 238 239 if (state != ActivityState.DESTROYED && state != ActivityState.STOPPED) { 240 sActivity = activity; 241 } 242 243 // TODO(dtrainor): Notify of active activity change? 244 } 245 }); 246 247 application.registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { 248 @Override 249 public void onActivityCreated(final Activity activity, Bundle savedInstanceState) { 250 onStateChange(activity, ActivityState.CREATED); 251 Window.Callback callback = activity.getWindow().getCallback(); 252 activity.getWindow().setCallback((Window.Callback) Proxy.newProxyInstance( 253 Window.Callback.class.getClassLoader(), new Class[] {Window.Callback.class}, 254 new ApplicationStatus.WindowCallbackProxy(activity, callback))); 255 } 256 257 @Override 258 public void onActivityDestroyed(Activity activity) { 259 onStateChange(activity, ActivityState.DESTROYED); 260 checkCallback(activity); 261 } 262 263 @Override 264 public void onActivityPaused(Activity activity) { 265 onStateChange(activity, ActivityState.PAUSED); 266 checkCallback(activity); 267 } 268 269 @Override 270 public void onActivityResumed(Activity activity) { 271 onStateChange(activity, ActivityState.RESUMED); 272 checkCallback(activity); 273 } 274 275 @Override 276 public void onActivitySaveInstanceState(Activity activity, Bundle outState) { 277 checkCallback(activity); 278 } 279 280 @Override 281 public void onActivityStarted(Activity activity) { 282 onStateChange(activity, ActivityState.STARTED); 283 checkCallback(activity); 284 } 285 286 @Override 287 public void onActivityStopped(Activity activity) { 288 onStateChange(activity, ActivityState.STOPPED); 289 checkCallback(activity); 290 } 291 292 private void checkCallback(Activity activity) { 293 if (BuildConfig.DCHECK_IS_ON) { 294 Class<? extends Window.Callback> callback = 295 activity.getWindow().getCallback().getClass(); 296 assert(Proxy.isProxyClass(callback) 297 || callback.getName().equals(TOOLBAR_CALLBACK_WRAPPER_CLASS) 298 || callback.getName().equals(TOOLBAR_CALLBACK_INTERNAL_WRAPPER_CLASS) 299 || callback.getName().equals(WINDOW_PROFILER_CALLBACK)); 300 } 301 } 302 }); 303 } 304 305 /** 306 * Asserts that initialize method has been called. 307 */ assertInitialized()308 private static void assertInitialized() { 309 if (!sIsInitialized) { 310 throw new IllegalStateException("ApplicationStatus has not been initialized yet."); 311 } 312 } 313 314 /** 315 * Must be called by the main activity when it changes state. 316 * 317 * @param activity Current activity. 318 * @param newState New state value. 319 */ onStateChange(Activity activity, @ActivityState int newState)320 private static void onStateChange(Activity activity, @ActivityState int newState) { 321 if (activity == null) throw new IllegalArgumentException("null activity is not supported"); 322 323 if (sActivity == null 324 || newState == ActivityState.CREATED 325 || newState == ActivityState.RESUMED 326 || newState == ActivityState.STARTED) { 327 sActivity = activity; 328 } 329 330 int oldApplicationState = getStateForApplication(); 331 ActivityInfo info; 332 333 synchronized (sCurrentApplicationStateLock) { 334 if (newState == ActivityState.CREATED) { 335 assert !sActivityInfo.containsKey(activity); 336 sActivityInfo.put(activity, new ActivityInfo()); 337 } 338 339 info = sActivityInfo.get(activity); 340 info.setStatus(newState); 341 342 // Remove before calling listeners so that isEveryActivityDestroyed() returns false when 343 // this was the last activity. 344 if (newState == ActivityState.DESTROYED) { 345 sActivityInfo.remove(activity); 346 if (activity == sActivity) sActivity = null; 347 } 348 349 sCurrentApplicationState = determineApplicationState(); 350 } 351 352 // Notify all state observers that are specifically listening to this activity. 353 for (ActivityStateListener listener : info.getListeners()) { 354 listener.onActivityStateChange(activity, newState); 355 } 356 357 // Notify all state observers that are listening globally for all activity state 358 // changes. 359 for (ActivityStateListener listener : sGeneralActivityStateListeners) { 360 listener.onActivityStateChange(activity, newState); 361 } 362 363 int applicationState = getStateForApplication(); 364 if (applicationState != oldApplicationState) { 365 for (ApplicationStateListener listener : sApplicationStateListeners) { 366 listener.onApplicationStateChange(applicationState); 367 } 368 } 369 } 370 371 /** 372 * Testing method to update the state of the specified activity. 373 */ 374 @VisibleForTesting onStateChangeForTesting(Activity activity, int newState)375 public static void onStateChangeForTesting(Activity activity, int newState) { 376 onStateChange(activity, newState); 377 } 378 379 /** 380 * @return The most recent focused {@link Activity} tracked by this class. Being focused means 381 * out of all the activities tracked here, it has most recently gained window focus. 382 */ getLastTrackedFocusedActivity()383 public static Activity getLastTrackedFocusedActivity() { 384 return sActivity; 385 } 386 387 /** 388 * @return A {@link List} of all non-destroyed {@link Activity}s. 389 */ getRunningActivities()390 public static List<WeakReference<Activity>> getRunningActivities() { 391 assertInitialized(); 392 List<WeakReference<Activity>> activities = new ArrayList<>(); 393 for (Activity activity : sActivityInfo.keySet()) { 394 activities.add(new WeakReference<>(activity)); 395 } 396 return activities; 397 } 398 399 /** 400 * Query the state for a given activity. If the activity is not being tracked, this will 401 * return {@link ActivityState#DESTROYED}. 402 * 403 * <p> 404 * Please note that Chrome can have multiple activities running simultaneously. Please also 405 * look at {@link #getStateForApplication()} for more details. 406 * 407 * <p> 408 * When relying on this method, be familiar with the expected life cycle state 409 * transitions: 410 * <a href="http://developer.android.com/guide/components/activities.html#Lifecycle"> 411 * Activity Lifecycle 412 * </a> 413 * 414 * <p> 415 * During activity transitions (activity B launching in front of activity A), A will completely 416 * paused before the creation of activity B begins. 417 * 418 * <p> 419 * A basic flow for activity A starting, followed by activity B being opened and then closed: 420 * <ul> 421 * <li> -- Starting Activity A -- 422 * <li> Activity A - ActivityState.CREATED 423 * <li> Activity A - ActivityState.STARTED 424 * <li> Activity A - ActivityState.RESUMED 425 * <li> -- Starting Activity B -- 426 * <li> Activity A - ActivityState.PAUSED 427 * <li> Activity B - ActivityState.CREATED 428 * <li> Activity B - ActivityState.STARTED 429 * <li> Activity B - ActivityState.RESUMED 430 * <li> Activity A - ActivityState.STOPPED 431 * <li> -- Closing Activity B, Activity A regaining focus -- 432 * <li> Activity B - ActivityState.PAUSED 433 * <li> Activity A - ActivityState.STARTED 434 * <li> Activity A - ActivityState.RESUMED 435 * <li> Activity B - ActivityState.STOPPED 436 * <li> Activity B - ActivityState.DESTROYED 437 * </ul> 438 * 439 * @param activity The activity whose state is to be returned. 440 * @return The state of the specified activity (see {@link ActivityState}). 441 */ 442 @ActivityState getStateForActivity(@ullable Activity activity)443 public static int getStateForActivity(@Nullable Activity activity) { 444 ApplicationStatus.assertInitialized(); 445 if (activity == null) return ActivityState.DESTROYED; 446 ActivityInfo info = sActivityInfo.get(activity); 447 return info != null ? info.getStatus() : ActivityState.DESTROYED; 448 } 449 450 /** 451 * @return The state of the application (see {@link ApplicationState}). 452 */ 453 @ApplicationState 454 @CalledByNative getStateForApplication()455 public static int getStateForApplication() { 456 synchronized (sCurrentApplicationStateLock) { 457 return sCurrentApplicationState; 458 } 459 } 460 461 /** 462 * Checks whether or not any Activity in this Application is visible to the user. Note that 463 * this includes the PAUSED state, which can happen when the Activity is temporarily covered 464 * by another Activity's Fragment (e.g.). 465 * @return Whether any Activity under this Application is visible. 466 */ hasVisibleActivities()467 public static boolean hasVisibleActivities() { 468 int state = getStateForApplication(); 469 return state == ApplicationState.HAS_RUNNING_ACTIVITIES 470 || state == ApplicationState.HAS_PAUSED_ACTIVITIES; 471 } 472 473 /** 474 * Checks to see if there are any active Activity instances being watched by ApplicationStatus. 475 * @return True if all Activities have been destroyed. 476 */ isEveryActivityDestroyed()477 public static boolean isEveryActivityDestroyed() { 478 return sActivityInfo.isEmpty(); 479 } 480 481 /** 482 * Registers the given listener to receive state changes for all activities. 483 * @param listener Listener to receive state changes. 484 */ registerStateListenerForAllActivities(ActivityStateListener listener)485 public static void registerStateListenerForAllActivities(ActivityStateListener listener) { 486 sGeneralActivityStateListeners.addObserver(listener); 487 } 488 489 /** 490 * Registers the given listener to receive state changes for {@code activity}. After a call to 491 * {@link ActivityStateListener#onActivityStateChange(Activity, int)} with 492 * {@link ActivityState#DESTROYED} all listeners associated with that particular 493 * {@link Activity} are removed. 494 * @param listener Listener to receive state changes. 495 * @param activity Activity to track or {@code null} to track all activities. 496 */ 497 @SuppressLint("NewApi") registerStateListenerForActivity(ActivityStateListener listener, Activity activity)498 public static void registerStateListenerForActivity(ActivityStateListener listener, 499 Activity activity) { 500 if (activity == null) { 501 throw new IllegalStateException("Attempting to register listener on a null activity."); 502 } 503 ApplicationStatus.assertInitialized(); 504 505 ActivityInfo info = sActivityInfo.get(activity); 506 if (info == null) { 507 throw new IllegalStateException( 508 "Attempting to register listener on an untracked activity."); 509 } 510 assert info.getStatus() != ActivityState.DESTROYED; 511 info.getListeners().addObserver(listener); 512 } 513 514 /** 515 * Unregisters the given listener from receiving activity state changes. 516 * @param listener Listener that doesn't want to receive state changes. 517 */ unregisterActivityStateListener(ActivityStateListener listener)518 public static void unregisterActivityStateListener(ActivityStateListener listener) { 519 sGeneralActivityStateListeners.removeObserver(listener); 520 521 // Loop through all observer lists for all activities and remove the listener. 522 for (ActivityInfo info : sActivityInfo.values()) { 523 info.getListeners().removeObserver(listener); 524 } 525 } 526 527 /** 528 * Registers the given listener to receive state changes for the application. 529 * @param listener Listener to receive state state changes. 530 */ registerApplicationStateListener(ApplicationStateListener listener)531 public static void registerApplicationStateListener(ApplicationStateListener listener) { 532 sApplicationStateListeners.addObserver(listener); 533 } 534 535 /** 536 * Unregisters the given listener from receiving state changes. 537 * @param listener Listener that doesn't want to receive state changes. 538 */ unregisterApplicationStateListener(ApplicationStateListener listener)539 public static void unregisterApplicationStateListener(ApplicationStateListener listener) { 540 sApplicationStateListeners.removeObserver(listener); 541 } 542 543 /** 544 * Robolectric JUnit tests create a new application between each test, while all the context 545 * in static classes isn't reset. This function allows to reset the application status to avoid 546 * being in a dirty state. 547 */ destroyForJUnitTests()548 public static void destroyForJUnitTests() { 549 sApplicationStateListeners.clear(); 550 sGeneralActivityStateListeners.clear(); 551 sActivityInfo.clear(); 552 sWindowFocusListeners.clear(); 553 sIsInitialized = false; 554 synchronized (sCurrentApplicationStateLock) { 555 sCurrentApplicationState = determineApplicationState(); 556 } 557 sActivity = null; 558 sNativeApplicationStateListener = null; 559 } 560 561 /** 562 * Registers the single thread-safe native activity status listener. 563 * This handles the case where the caller is not on the main thread. 564 * Note that this is used by a leaky singleton object from the native 565 * side, hence lifecycle management is greatly simplified. 566 */ 567 @CalledByNative registerThreadSafeNativeApplicationStateListener()568 private static void registerThreadSafeNativeApplicationStateListener() { 569 ThreadUtils.runOnUiThread(new Runnable() { 570 @Override 571 public void run() { 572 if (sNativeApplicationStateListener != null) return; 573 574 sNativeApplicationStateListener = new ApplicationStateListener() { 575 @Override 576 public void onApplicationStateChange(int newState) { 577 nativeOnApplicationStateChange(newState); 578 } 579 }; 580 registerApplicationStateListener(sNativeApplicationStateListener); 581 } 582 }); 583 } 584 585 /** 586 * Determines the current application state as defined by {@link ApplicationState}. This will 587 * loop over all the activities and check their state to determine what the general application 588 * state should be. 589 * @return HAS_RUNNING_ACTIVITIES if any activity is not paused, stopped, or destroyed. 590 * HAS_PAUSED_ACTIVITIES if none are running and one is paused. 591 * HAS_STOPPED_ACTIVITIES if none are running/paused and one is stopped. 592 * HAS_DESTROYED_ACTIVITIES if none are running/paused/stopped. 593 */ 594 @ApplicationState determineApplicationState()595 private static int determineApplicationState() { 596 boolean hasPausedActivity = false; 597 boolean hasStoppedActivity = false; 598 599 for (ActivityInfo info : sActivityInfo.values()) { 600 int state = info.getStatus(); 601 if (state != ActivityState.PAUSED 602 && state != ActivityState.STOPPED 603 && state != ActivityState.DESTROYED) { 604 return ApplicationState.HAS_RUNNING_ACTIVITIES; 605 } else if (state == ActivityState.PAUSED) { 606 hasPausedActivity = true; 607 } else if (state == ActivityState.STOPPED) { 608 hasStoppedActivity = true; 609 } 610 } 611 612 if (hasPausedActivity) return ApplicationState.HAS_PAUSED_ACTIVITIES; 613 if (hasStoppedActivity) return ApplicationState.HAS_STOPPED_ACTIVITIES; 614 return ApplicationState.HAS_DESTROYED_ACTIVITIES; 615 } 616 617 // Called to notify the native side of state changes. 618 // IMPORTANT: This is always called on the main thread! nativeOnApplicationStateChange(@pplicationState int newState)619 private static native void nativeOnApplicationStateChange(@ApplicationState int newState); 620 } 621