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