1 // Copyright 2014 The Chromium Authors 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.view.Window; 13 14 import androidx.annotation.AnyThread; 15 import androidx.annotation.MainThread; 16 import androidx.annotation.Nullable; 17 import androidx.annotation.VisibleForTesting; 18 19 import org.chromium.base.annotations.CalledByNative; 20 import org.chromium.base.annotations.JNINamespace; 21 import org.chromium.base.annotations.NativeMethods; 22 import org.chromium.build.BuildConfig; 23 24 import java.lang.reflect.Field; 25 import java.lang.reflect.InvocationHandler; 26 import java.lang.reflect.InvocationTargetException; 27 import java.lang.reflect.Method; 28 import java.lang.reflect.Proxy; 29 import java.util.ArrayList; 30 import java.util.Collections; 31 import java.util.HashMap; 32 import java.util.HashSet; 33 import java.util.List; 34 import java.util.Map; 35 36 import javax.annotation.concurrent.GuardedBy; 37 38 /** 39 * Provides information about the current activity's status, and a way 40 * to register / unregister listeners for state changes. 41 * TODO(https://crbug.com/470582): ApplicationStatus will not work on WebView/WebLayer, and 42 * should be moved out of base and into //chrome. It should not be relied upon for //components. 43 */ 44 @JNINamespace("base::android") 45 public class ApplicationStatus { 46 private static final String TOOLBAR_CALLBACK_WRAPPER_CLASS = 47 "androidx.appcompat.app.ToolbarActionBar$ToolbarCallbackWrapper"; 48 49 private static class ActivityInfo { 50 private int mStatus = ActivityState.DESTROYED; 51 private ObserverList<ActivityStateListener> mListeners = new ObserverList<>(); 52 53 /** 54 * @return The current {@link ActivityState} of the activity. 55 */ 56 @ActivityState getStatus()57 public int getStatus() { 58 return mStatus; 59 } 60 61 /** 62 * @param status The new {@link ActivityState} of the activity. 63 */ setStatus(@ctivityState int status)64 public void setStatus(@ActivityState int status) { 65 mStatus = status; 66 } 67 68 /** 69 * @return A list of {@link ActivityStateListener}s listening to this activity. 70 */ getListeners()71 public ObserverList<ActivityStateListener> getListeners() { 72 return mListeners; 73 } 74 } 75 76 /** 77 * A map of which observers listen to state changes from which {@link Activity}. 78 */ 79 private static final Map<Activity, ActivityInfo> sActivityInfo = 80 Collections.synchronizedMap(new HashMap<Activity, ActivityInfo>()); 81 82 @SuppressLint("SupportAnnotationUsage") 83 @ApplicationState 84 @GuardedBy("sActivityInfo") 85 // The getStateForApplication() historically returned ApplicationState.HAS_DESTROYED_ACTIVITIES 86 // when no activity has been observed. 87 private static int sCurrentApplicationState = ApplicationState.UNKNOWN; 88 89 /** 90 * Last activity that was shown (or null if none or it was destroyed). 91 */ 92 @SuppressLint("StaticFieldLeak") 93 private static Activity sActivity; 94 95 /** 96 * A lazily initialized listener that forwards application state changes to native. 97 */ 98 private static ApplicationStateListener sNativeApplicationStateListener; 99 100 /** 101 * A list of observers to be notified when any {@link Activity} has a state change. 102 */ 103 private static ObserverList<ActivityStateListener> sGeneralActivityStateListeners; 104 105 /** 106 * A list of observers to be notified when the visibility state of this {@link Application} 107 * changes. See {@link #getStateForApplication()}. 108 */ 109 private static ObserverList<ApplicationStateListener> sApplicationStateListeners; 110 111 /** 112 * A list of observers to be notified when the window focus changes. 113 * See {@link #registerWindowFocusChangedListener}. 114 */ 115 private static ObserverList<WindowFocusChangedListener> sWindowFocusListeners; 116 117 /** 118 * A list of observers to be notified when the visibility of any task changes. 119 */ 120 private static ObserverList<TaskVisibilityListener> sTaskVisibilityListeners; 121 122 /** 123 * Interface to be implemented by listeners. 124 */ 125 public interface ApplicationStateListener { 126 /** 127 * Called when the application's state changes. 128 * 129 * @param newState The application state. 130 */ onApplicationStateChange(@pplicationState int newState)131 void onApplicationStateChange(@ApplicationState int newState); 132 } 133 134 /** 135 * Interface to be implemented by listeners. 136 */ 137 public interface ActivityStateListener { 138 /** 139 * Called when the activity's state changes. 140 * 141 * @param activity The activity that had a state change. 142 * @param newState New activity state. 143 */ onActivityStateChange(Activity activity, @ActivityState int newState)144 void onActivityStateChange(Activity activity, @ActivityState int newState); 145 } 146 147 /** 148 * Interface to be implemented by listeners for window focus events. 149 */ 150 public interface WindowFocusChangedListener { 151 /** 152 * Called when the window focus changes for {@code activity}. 153 * 154 * @param activity The {@link Activity} that has a window focus changed event. 155 * @param hasFocus Whether or not {@code activity} gained or lost focus. 156 */ onWindowFocusChanged(Activity activity, boolean hasFocus)157 public void onWindowFocusChanged(Activity activity, boolean hasFocus); 158 } 159 160 /** 161 * Interface to be implemented by listeners for task visibility changes. 162 */ 163 public interface TaskVisibilityListener { 164 /** 165 * Called when the visibility of a task changes. 166 * 167 * @param taskId The unique Id of the task that changed visibility. 168 * @param isVisible The new visibility state of the task. 169 */ onTaskVisibilityChanged(int taskId, boolean isVisible)170 void onTaskVisibilityChanged(int taskId, boolean isVisible); 171 } 172 ApplicationStatus()173 private ApplicationStatus() {} 174 175 /** 176 * Registers a listener to receive window focus updates on activities in this application. 177 * 178 * @param listener Listener to receive window focus events. 179 */ 180 @MainThread registerWindowFocusChangedListener(WindowFocusChangedListener listener)181 public static void registerWindowFocusChangedListener(WindowFocusChangedListener listener) { 182 assert isInitialized(); 183 if (sWindowFocusListeners == null) sWindowFocusListeners = new ObserverList<>(); 184 sWindowFocusListeners.addObserver(listener); 185 } 186 187 /** 188 * Unregisters a listener from receiving window focus updates on activities in this application. 189 * 190 * @param listener Listener that doesn't want to receive window focus events. 191 */ 192 @MainThread unregisterWindowFocusChangedListener(WindowFocusChangedListener listener)193 public static void unregisterWindowFocusChangedListener(WindowFocusChangedListener listener) { 194 if (sWindowFocusListeners == null) return; 195 sWindowFocusListeners.removeObserver(listener); 196 } 197 198 /** 199 * Register a listener to receive task visibility updates. 200 * 201 * @param listener Listener to receive task visibility events. 202 */ 203 @MainThread registerTaskVisibilityListener(TaskVisibilityListener listener)204 public static void registerTaskVisibilityListener(TaskVisibilityListener listener) { 205 assert isInitialized(); 206 if (sTaskVisibilityListeners == null) sTaskVisibilityListeners = new ObserverList<>(); 207 sTaskVisibilityListeners.addObserver(listener); 208 } 209 210 /** 211 * Unregisters a listener from receiving task visibility updates. 212 * 213 * @param listener Listener that doesn't want to receive task visibility events. 214 */ 215 @MainThread unregisterTaskVisibilityListener(TaskVisibilityListener listener)216 public static void unregisterTaskVisibilityListener(TaskVisibilityListener listener) { 217 if (sTaskVisibilityListeners == null) return; 218 sTaskVisibilityListeners.removeObserver(listener); 219 } 220 221 /** 222 * Intercepts calls to an existing Window.Callback. Most invocations are passed on directly 223 * to the composed Window.Callback but enables intercepting/manipulating others. 224 * <p> 225 * This is used to relay window focus changes throughout the app and remedy a bug in the 226 * appcompat library. 227 */ 228 @VisibleForTesting 229 static class WindowCallbackProxy implements InvocationHandler { 230 private final Window.Callback mCallback; 231 private final Activity mActivity; 232 WindowCallbackProxy(Activity activity, Window.Callback callback)233 public WindowCallbackProxy(Activity activity, Window.Callback callback) { 234 mCallback = callback; 235 mActivity = activity; 236 } 237 238 @Override invoke(Object proxy, Method method, Object[] args)239 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 240 if (method.getName().equals("onWindowFocusChanged") && args.length == 1 241 && args[0] instanceof Boolean) { 242 onWindowFocusChanged((boolean) args[0]); 243 return null; 244 } else { 245 try { 246 return method.invoke(mCallback, args); 247 } catch (InvocationTargetException e) { 248 // Special-case for when a method is not defined on the underlying 249 // Window.Callback object. Because we're using a Proxy to forward all method 250 // calls, this breaks the Android framework's handling for apps built against 251 // an older SDK. The framework expects an AbstractMethodError but due to 252 // reflection it becomes wrapped inside an InvocationTargetException. Undo the 253 // wrapping to signal the framework accordingly. 254 if (e.getCause() instanceof AbstractMethodError) { 255 throw e.getCause(); 256 } 257 throw e; 258 } 259 } 260 } 261 onWindowFocusChanged(boolean hasFocus)262 public void onWindowFocusChanged(boolean hasFocus) { 263 mCallback.onWindowFocusChanged(hasFocus); 264 265 if (sWindowFocusListeners != null) { 266 for (WindowFocusChangedListener listener : sWindowFocusListeners) { 267 listener.onWindowFocusChanged(mActivity, hasFocus); 268 } 269 } 270 } 271 } 272 isInitialized()273 public static boolean isInitialized() { 274 synchronized (sActivityInfo) { 275 return sCurrentApplicationState != ApplicationState.UNKNOWN; 276 } 277 } 278 279 /** 280 * Initializes the activity status for a specified application. 281 * 282 * @param application The application whose status you wish to monitor. 283 */ 284 @MainThread initialize(Application application)285 public static void initialize(Application application) { 286 assert !isInitialized(); 287 synchronized (sActivityInfo) { 288 sCurrentApplicationState = ApplicationState.HAS_DESTROYED_ACTIVITIES; 289 } 290 291 registerWindowFocusChangedListener(new WindowFocusChangedListener() { 292 @Override 293 public void onWindowFocusChanged(Activity activity, boolean hasFocus) { 294 if (!hasFocus || activity == sActivity) return; 295 296 int state = getStateForActivity(activity); 297 298 if (state != ActivityState.DESTROYED && state != ActivityState.STOPPED) { 299 sActivity = activity; 300 } 301 302 // TODO(dtrainor): Notify of active activity change? 303 } 304 }); 305 306 application.registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { 307 @Override 308 public void onActivityCreated(final Activity activity, Bundle savedInstanceState) { 309 onStateChange(activity, ActivityState.CREATED); 310 Window.Callback callback = activity.getWindow().getCallback(); 311 activity.getWindow().setCallback(createWindowCallbackProxy(activity, callback)); 312 } 313 314 @Override 315 public void onActivityDestroyed(Activity activity) { 316 onStateChange(activity, ActivityState.DESTROYED); 317 checkCallback(activity); 318 } 319 320 @Override 321 public void onActivityPaused(Activity activity) { 322 onStateChange(activity, ActivityState.PAUSED); 323 checkCallback(activity); 324 } 325 326 @Override 327 public void onActivityResumed(Activity activity) { 328 onStateChange(activity, ActivityState.RESUMED); 329 checkCallback(activity); 330 } 331 332 @Override 333 public void onActivitySaveInstanceState(Activity activity, Bundle outState) { 334 checkCallback(activity); 335 } 336 337 @Override 338 public void onActivityStarted(Activity activity) { 339 onStateChange(activity, ActivityState.STARTED); 340 checkCallback(activity); 341 } 342 343 @Override 344 public void onActivityStopped(Activity activity) { 345 onStateChange(activity, ActivityState.STOPPED); 346 checkCallback(activity); 347 } 348 349 private void checkCallback(Activity activity) { 350 if (BuildConfig.ENABLE_ASSERTS) { 351 assert reachesWindowCallback(activity.getWindow().getCallback()); 352 } 353 } 354 }); 355 } 356 357 @VisibleForTesting createWindowCallbackProxy(Activity activity, Window.Callback callback)358 static Window.Callback createWindowCallbackProxy(Activity activity, Window.Callback callback) { 359 return (Window.Callback) Proxy.newProxyInstance(Window.Callback.class.getClassLoader(), 360 new Class[] {Window.Callback.class}, 361 new ApplicationStatus.WindowCallbackProxy(activity, callback)); 362 } 363 364 /** 365 * Tries to trace down to our WindowCallbackProxy from the given callback. 366 * Since the callback can be overwritten by embedder code we try to ensure 367 * that there at least seem to be a reference back to our callback by 368 * checking the declared fields of the given callback using reflection. 369 */ 370 @VisibleForTesting reachesWindowCallback(@ullable Window.Callback callback)371 static boolean reachesWindowCallback(@Nullable Window.Callback callback) { 372 if (callback == null) return false; 373 if (callback.getClass().getName().equals(TOOLBAR_CALLBACK_WRAPPER_CLASS)) { 374 // We're actually not going to get called, see AndroidX report here: 375 // https://issuetracker.google.com/issues/155165145. 376 // But this was accepted in the old code as well so mimic that until 377 // AndroidX is fixed and updated. 378 return true; 379 } 380 if (Proxy.isProxyClass(callback.getClass())) { 381 return Proxy.getInvocationHandler(callback) 382 instanceof ApplicationStatus.WindowCallbackProxy; 383 } 384 for (Class<?> c = callback.getClass(); c != Object.class; c = c.getSuperclass()) { 385 for (Field f : c.getDeclaredFields()) { 386 if (f.getType().isAssignableFrom(Window.Callback.class)) { 387 boolean isAccessible = f.isAccessible(); 388 f.setAccessible(true); 389 Window.Callback fieldCb; 390 try { 391 fieldCb = (Window.Callback) f.get(callback); 392 } catch (IllegalAccessException ex) { 393 continue; 394 } finally { 395 f.setAccessible(isAccessible); 396 } 397 if (reachesWindowCallback(fieldCb)) { 398 return true; 399 } 400 } 401 } 402 } 403 return false; 404 } 405 406 /** 407 * Must be called by the main activity when it changes state. 408 * 409 * @param activity Current activity. 410 * @param newState New state value. 411 */ onStateChange(Activity activity, @ActivityState int newState)412 private static void onStateChange(Activity activity, @ActivityState int newState) { 413 if (activity == null) throw new IllegalArgumentException("null activity is not supported"); 414 415 if (sActivity == null 416 || newState == ActivityState.CREATED 417 || newState == ActivityState.RESUMED 418 || newState == ActivityState.STARTED) { 419 sActivity = activity; 420 } 421 422 int oldApplicationState = getStateForApplication(); 423 boolean oldTaskVisibility = isTaskVisible(activity.getTaskId()); 424 ActivityInfo info; 425 426 synchronized (sActivityInfo) { 427 if (newState == ActivityState.CREATED) { 428 assert !sActivityInfo.containsKey(activity); 429 sActivityInfo.put(activity, new ActivityInfo()); 430 } 431 432 info = sActivityInfo.get(activity); 433 info.setStatus(newState); 434 435 // Remove before calling listeners so that isEveryActivityDestroyed() returns false when 436 // this was the last activity. 437 if (newState == ActivityState.DESTROYED) { 438 sActivityInfo.remove(activity); 439 if (activity == sActivity) sActivity = null; 440 } 441 442 sCurrentApplicationState = determineApplicationStateLocked(); 443 } 444 445 // Notify all state observers that are specifically listening to this activity. 446 for (ActivityStateListener listener : info.getListeners()) { 447 listener.onActivityStateChange(activity, newState); 448 } 449 450 // Notify all state observers that are listening globally for all activity state 451 // changes. 452 if (sGeneralActivityStateListeners != null) { 453 for (ActivityStateListener listener : sGeneralActivityStateListeners) { 454 listener.onActivityStateChange(activity, newState); 455 } 456 } 457 458 boolean taskVisibility = isTaskVisible(activity.getTaskId()); 459 if (taskVisibility != oldTaskVisibility && sTaskVisibilityListeners != null) { 460 for (TaskVisibilityListener listener : sTaskVisibilityListeners) { 461 listener.onTaskVisibilityChanged(activity.getTaskId(), taskVisibility); 462 } 463 } 464 465 int applicationState = getStateForApplication(); 466 if (applicationState != oldApplicationState && sApplicationStateListeners != null) { 467 for (ApplicationStateListener listener : sApplicationStateListeners) { 468 listener.onApplicationStateChange(applicationState); 469 } 470 } 471 } 472 473 /** 474 * Testing method to update the state of the specified activity. 475 */ 476 @VisibleForTesting 477 @MainThread onStateChangeForTesting(Activity activity, int newState)478 public static void onStateChangeForTesting(Activity activity, int newState) { 479 onStateChange(activity, newState); 480 } 481 482 /** 483 * @return The most recent focused {@link Activity} tracked by this class. Being focused means 484 * out of all the activities tracked here, it has most recently gained window focus. 485 */ 486 @MainThread getLastTrackedFocusedActivity()487 public static Activity getLastTrackedFocusedActivity() { 488 return sActivity; 489 } 490 491 /** 492 * @return A {@link List} of all non-destroyed {@link Activity}s. 493 */ 494 @AnyThread getRunningActivities()495 public static List<Activity> getRunningActivities() { 496 assert isInitialized(); 497 synchronized (sActivityInfo) { 498 return new ArrayList<>(sActivityInfo.keySet()); 499 } 500 } 501 502 /** 503 * Query the state for a given activity. If the activity is not being tracked, this will 504 * return {@link ActivityState#DESTROYED}. 505 * 506 * <p> 507 * Please note that Chrome can have multiple activities running simultaneously. Please also 508 * look at {@link #getStateForApplication()} for more details. 509 * 510 * <p> 511 * When relying on this method, be familiar with the expected life cycle state 512 * transitions: 513 * <a href="http://developer.android.com/guide/components/activities.html#Lifecycle"> 514 * Activity Lifecycle 515 * </a> 516 * 517 * <p> 518 * During activity transitions (activity B launching in front of activity A), A will completely 519 * paused before the creation of activity B begins. 520 * 521 * <p> 522 * A basic flow for activity A starting, followed by activity B being opened and then closed: 523 * <ul> 524 * <li> -- Starting Activity A -- 525 * <li> Activity A - ActivityState.CREATED 526 * <li> Activity A - ActivityState.STARTED 527 * <li> Activity A - ActivityState.RESUMED 528 * <li> -- Starting Activity B -- 529 * <li> Activity A - ActivityState.PAUSED 530 * <li> Activity B - ActivityState.CREATED 531 * <li> Activity B - ActivityState.STARTED 532 * <li> Activity B - ActivityState.RESUMED 533 * <li> Activity A - ActivityState.STOPPED 534 * <li> -- Closing Activity B, Activity A regaining focus -- 535 * <li> Activity B - ActivityState.PAUSED 536 * <li> Activity A - ActivityState.STARTED 537 * <li> Activity A - ActivityState.RESUMED 538 * <li> Activity B - ActivityState.STOPPED 539 * <li> Activity B - ActivityState.DESTROYED 540 * </ul> 541 * 542 * @param activity The activity whose state is to be returned. 543 * @return The state of the specified activity (see {@link ActivityState}). 544 */ 545 @ActivityState 546 @AnyThread getStateForActivity(@ullable Activity activity)547 public static int getStateForActivity(@Nullable Activity activity) { 548 assert isInitialized(); 549 if (activity == null) return ActivityState.DESTROYED; 550 ActivityInfo info = sActivityInfo.get(activity); 551 return info != null ? info.getStatus() : ActivityState.DESTROYED; 552 } 553 554 /** 555 * @return The state of the application (see {@link ApplicationState}). 556 */ 557 @AnyThread 558 @ApplicationState 559 @CalledByNative getStateForApplication()560 public static int getStateForApplication() { 561 synchronized (sActivityInfo) { 562 return sCurrentApplicationState; 563 } 564 } 565 566 /** 567 * Checks whether or not any Activity in this Application is visible to the user. Note that 568 * this includes the PAUSED state, which can happen when the Activity is temporarily covered 569 * by another Activity's Fragment (e.g.). 570 * 571 * @return Whether any Activity under this Application is visible. 572 */ 573 @AnyThread 574 @CalledByNative hasVisibleActivities()575 public static boolean hasVisibleActivities() { 576 assert isInitialized(); 577 int state = getStateForApplication(); 578 return state == ApplicationState.HAS_RUNNING_ACTIVITIES 579 || state == ApplicationState.HAS_PAUSED_ACTIVITIES; 580 } 581 582 /** 583 * Checks to see if there are any active Activity instances being watched by ApplicationStatus. 584 * 585 * @return True if all Activities have been destroyed. 586 */ 587 @AnyThread isEveryActivityDestroyed()588 public static boolean isEveryActivityDestroyed() { 589 assert isInitialized(); 590 return sActivityInfo.isEmpty(); 591 } 592 593 /** 594 * Returns the visibility of the task with the given taskId. A task is visible if any of its 595 * Activities are in RESUMED or PAUSED state. 596 * 597 * @param taskId The id of the task whose visibility needs to be checked. 598 * @return Whether the task is visible or not. 599 */ 600 @AnyThread isTaskVisible(int taskId)601 public static boolean isTaskVisible(int taskId) { 602 assert isInitialized(); 603 for (Map.Entry<Activity, ActivityInfo> entry : sActivityInfo.entrySet()) { 604 if (entry.getKey().getTaskId() == taskId) { 605 @ActivityState 606 int state = entry.getValue().getStatus(); 607 if (state == ActivityState.RESUMED || state == ActivityState.PAUSED) { 608 return true; 609 } 610 } 611 } 612 return false; 613 } 614 615 /** 616 * Registers the given listener to receive state changes for all activities. 617 * 618 * @param listener Listener to receive state changes. 619 */ 620 @MainThread registerStateListenerForAllActivities(ActivityStateListener listener)621 public static void registerStateListenerForAllActivities(ActivityStateListener listener) { 622 assert isInitialized(); 623 if (sGeneralActivityStateListeners == null) { 624 sGeneralActivityStateListeners = new ObserverList<>(); 625 } 626 sGeneralActivityStateListeners.addObserver(listener); 627 } 628 629 /** 630 * Registers the given listener to receive state changes for {@code activity}. After a call to 631 * {@link ActivityStateListener#onActivityStateChange(Activity, int)} with 632 * {@link ActivityState#DESTROYED} all listeners associated with that particular 633 * {@link Activity} are removed. 634 * 635 * @param listener Listener to receive state changes. 636 * @param activity Activity to track or {@code null} to track all activities. 637 */ 638 @MainThread 639 @SuppressLint("NewApi") registerStateListenerForActivity( ActivityStateListener listener, Activity activity)640 public static void registerStateListenerForActivity( 641 ActivityStateListener listener, Activity activity) { 642 assert isInitialized(); 643 assert activity != null; 644 645 ActivityInfo info = sActivityInfo.get(activity); 646 assert info.getStatus() != ActivityState.DESTROYED; 647 info.getListeners().addObserver(listener); 648 } 649 650 /** 651 * Unregisters the given listener from receiving activity state changes. 652 * 653 * @param listener Listener that doesn't want to receive state changes. 654 */ 655 @MainThread unregisterActivityStateListener(ActivityStateListener listener)656 public static void unregisterActivityStateListener(ActivityStateListener listener) { 657 if (sGeneralActivityStateListeners != null) { 658 sGeneralActivityStateListeners.removeObserver(listener); 659 } 660 661 // Loop through all observer lists for all activities and remove the listener. 662 synchronized (sActivityInfo) { 663 for (ActivityInfo info : sActivityInfo.values()) { 664 info.getListeners().removeObserver(listener); 665 } 666 } 667 } 668 669 /** 670 * Registers the given listener to receive state changes for the application. 671 * 672 * @param listener Listener to receive state state changes. 673 */ 674 @MainThread registerApplicationStateListener(ApplicationStateListener listener)675 public static void registerApplicationStateListener(ApplicationStateListener listener) { 676 if (sApplicationStateListeners == null) { 677 sApplicationStateListeners = new ObserverList<>(); 678 } 679 sApplicationStateListeners.addObserver(listener); 680 } 681 682 /** 683 * Unregisters the given listener from receiving state changes. 684 * 685 * @param listener Listener that doesn't want to receive state changes. 686 */ 687 @MainThread unregisterApplicationStateListener(ApplicationStateListener listener)688 public static void unregisterApplicationStateListener(ApplicationStateListener listener) { 689 if (sApplicationStateListeners == null) return; 690 sApplicationStateListeners.removeObserver(listener); 691 } 692 693 /** 694 * Robolectric JUnit tests create a new application between each test, while all the context 695 * in static classes isn't reset. This function allows to reset the application status to avoid 696 * being in a dirty state. 697 */ 698 @MainThread destroyForJUnitTests()699 public static void destroyForJUnitTests() { 700 synchronized (sActivityInfo) { 701 if (sApplicationStateListeners != null) sApplicationStateListeners.clear(); 702 if (sGeneralActivityStateListeners != null) sGeneralActivityStateListeners.clear(); 703 if (sTaskVisibilityListeners != null) sTaskVisibilityListeners.clear(); 704 sActivityInfo.clear(); 705 if (sWindowFocusListeners != null) sWindowFocusListeners.clear(); 706 sCurrentApplicationState = ApplicationState.UNKNOWN; 707 sActivity = null; 708 sNativeApplicationStateListener = null; 709 } 710 } 711 712 /** 713 * Mark all Activities as destroyed to avoid side-effects in future test. 714 */ 715 @MainThread resetActivitiesForInstrumentationTests()716 public static void resetActivitiesForInstrumentationTests() { 717 assert ThreadUtils.runningOnUiThread(); 718 719 synchronized (sActivityInfo) { 720 // Copy the set to avoid concurrent modifications to the underlying set. 721 for (Activity activity : new HashSet<>(sActivityInfo.keySet())) { 722 assert activity.getApplication() 723 == null : "Real activities that are launched should be closed by test code " 724 + "and not rely on this cleanup of mocks."; 725 onStateChangeForTesting(activity, ActivityState.DESTROYED); 726 } 727 } 728 } 729 730 /** 731 * Registers the single thread-safe native activity status listener. 732 * This handles the case where the caller is not on the main thread. 733 * Note that this is used by a leaky singleton object from the native 734 * side, hence lifecycle management is greatly simplified. 735 */ 736 @CalledByNative registerThreadSafeNativeApplicationStateListener()737 private static void registerThreadSafeNativeApplicationStateListener() { 738 ThreadUtils.runOnUiThread(new Runnable() { 739 @Override 740 public void run() { 741 if (sNativeApplicationStateListener != null) return; 742 743 sNativeApplicationStateListener = new ApplicationStateListener() { 744 @Override 745 public void onApplicationStateChange(int newState) { 746 ApplicationStatusJni.get().onApplicationStateChange(newState); 747 } 748 }; 749 registerApplicationStateListener(sNativeApplicationStateListener); 750 } 751 }); 752 } 753 754 /** 755 * Determines the current application state as defined by {@link ApplicationState}. This will 756 * loop over all the activities and check their state to determine what the general application 757 * state should be. 758 * 759 * @return HAS_RUNNING_ACTIVITIES if any activity is not paused, stopped, or destroyed. 760 * HAS_PAUSED_ACTIVITIES if none are running and one is paused. 761 * HAS_STOPPED_ACTIVITIES if none are running/paused and one is stopped. 762 * HAS_DESTROYED_ACTIVITIES if none are running/paused/stopped. 763 */ 764 @ApplicationState 765 @GuardedBy("sActivityInfo") determineApplicationStateLocked()766 private static int determineApplicationStateLocked() { 767 boolean hasPausedActivity = false; 768 boolean hasStoppedActivity = false; 769 770 for (ActivityInfo info : sActivityInfo.values()) { 771 int state = info.getStatus(); 772 if (state != ActivityState.PAUSED && state != ActivityState.STOPPED 773 && state != ActivityState.DESTROYED) { 774 return ApplicationState.HAS_RUNNING_ACTIVITIES; 775 } else if (state == ActivityState.PAUSED) { 776 hasPausedActivity = true; 777 } else if (state == ActivityState.STOPPED) { 778 hasStoppedActivity = true; 779 } 780 } 781 782 if (hasPausedActivity) return ApplicationState.HAS_PAUSED_ACTIVITIES; 783 if (hasStoppedActivity) return ApplicationState.HAS_STOPPED_ACTIVITIES; 784 return ApplicationState.HAS_DESTROYED_ACTIVITIES; 785 } 786 787 @NativeMethods 788 interface Natives { 789 // Called to notify the native side of state changes. 790 // IMPORTANT: This is always called on the main thread! onApplicationStateChange(@pplicationState int newState)791 void onApplicationStateChange(@ApplicationState int newState); 792 } 793 } 794