1 /*
2  * Copyright 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.sample.core.app;
18 
19 import static android.os.Build.VERSION.SDK_INT;
20 
21 import static androidx.annotation.RestrictTo.Scope.LIBRARY;
22 
23 import android.app.Activity;
24 import android.app.Application;
25 import android.app.Application.ActivityLifecycleCallbacks;
26 import android.content.res.Configuration;
27 import android.os.Bundle;
28 import android.os.Handler;
29 import android.os.IBinder;
30 import android.os.Looper;
31 import android.util.Log;
32 
33 import androidx.annotation.ChecksSdkIntAtLeast;
34 import androidx.annotation.NonNull;
35 import androidx.annotation.RestrictTo;
36 
37 import java.lang.reflect.Field;
38 import java.lang.reflect.Method;
39 import java.util.List;
40 
41 /**
42  * The goal here is to get common (and correct) behavior around Activity recreation for all API
43  * versions up until P, where the behavior was specified to be useful and implemented to match the
44  * specification. On API 26 and 27, recreate() doesn't actually recreate the Activity if it's
45  * not in the foreground; it will be recreated when the user next interacts with it. This has a few
46  * undesirable consequences:
47  *
48  * <p>1. It's impossible to recreate multiple activities at once, which means that activities in the
49  * background will observe the new configuration before they're recreated. If we keep them on the
50  * old configuration, we have two conflicting configurations active in the app, which leads to
51  * logging skew.
52  *
53  * <p>2. Recreation occurs in the critical path of user interaction - re-inflating a bunch of views
54  * isn't free, and we'd rather do it when we're in the background than when the user is staring at
55  * the screen waiting to see us.
56  *
57  * <p>On API < 26, recreate() was implemented with a single call to a private method on
58  * ActivityThread. That method still exists in 26 and 27, so we can use reflection to call it and
59  * get the exact same behavior as < 26. However, that behavior has problems itself. When
60  * an Activity in the background is recreated, it goes through: destroy -> create -> start ->
61  * resume -> pause and doesn't stop. This is a violation of the contract for onStart/onStop,
62  * but that might be palatable if it didn't also have the effect of preventing new configurations
63  * from being applied - since the Activity doesn't go through onStop, our tracking of whether
64  * our app is visible thinks we're always visible, and thus can't do another recreation later.
65  *
66  * <p>The fix for this is to add the missing onStop() call, by using reflection to call into
67  * ActivityThread.
68  *
69  */
70 @RestrictTo(LIBRARY)
71 @SuppressWarnings({"PrivateApi", "JavaReflectionMemberAccess", "unused"})
72 final class ActivityRecreatorChecked {
ActivityRecreatorChecked()73     private ActivityRecreatorChecked() {}
74 
75     private static final String LOG_TAG = "ActivityRecreatorChecked";
76 
77     // Activity.mMainThread
78     protected static final Field mainThreadField;
79     // Activity.mToken. This object is an identifier that is the same between multiple instances of
80     //the same underlying Activity.
81     protected static final Field tokenField;
82     // On API 25, a third param was added to performStopActivity
83     protected static final Method performStopActivity3ParamsMethod;
84     // Before API 25, performStopActivity had two params
85     protected static final Method performStopActivity2ParamsMethod;
86     // ActivityThread.requestRelaunchActivity
87     protected static final Method requestRelaunchActivityMethod;
88 
89     private static final Handler mainHandler = new Handler(Looper.getMainLooper());
90 
91     static {
92         Class<?> activityThreadClass = getActivityThreadClass();
93         mainThreadField = getMainThreadField();
94         tokenField = getTokenField();
95         performStopActivity3ParamsMethod = getPerformStopActivity3Params(activityThreadClass);
96         performStopActivity2ParamsMethod = getPerformStopActivity2Params(activityThreadClass);
97         requestRelaunchActivityMethod = getRequestRelaunchActivityMethod(activityThreadClass);
98     }
99 
100     /**
101      * Equivalent to {@link Activity#recreate}, but working around a number of platform bugs.
102      *
103      * @return true if a recreate() task was successfully scheduled.
104      */
recreate(@onNull final Activity activity)105     static boolean recreate(@NonNull final Activity activity) {
106         // On Android O and later we can rely on the platform recreate()
107         if (SDK_INT >= 28) {
108             activity.recreate();
109             return true;
110         }
111 
112         // API 26 needs this workaround but it's not possible because our reflective lookup failed.
113         if (needsRelaunchCall() && requestRelaunchActivityMethod == null) {
114             return false;
115         }
116         // All versions of android so far need this workaround, but it's not possible because our
117         // reflective lookup failed.
118         if (performStopActivity2ParamsMethod == null && performStopActivity3ParamsMethod == null) {
119             return false;
120         }
121         try {
122             final Object token = tokenField.get(activity);
123             if (token == null) {
124                 return false;
125             }
126             Object activityThread = mainThreadField.get(activity);
127             if (activityThread == null) {
128                 return false;
129             }
130 
131             final Application application = activity.getApplication();
132             final LifecycleCheckCallbacks callbacks = new LifecycleCheckCallbacks(activity);
133             application.registerActivityLifecycleCallbacks(callbacks);
134 
135             /*
136              * Runnables scheduled before/after recreate() will run before and after the Runnables
137              * scheduled by recreate(). This allows us to bound the time where mActivity lifecycle
138              * events that could be caused by recreate() run - that way we can detect onPause()
139              * from the new Activity instance, and schedule onStop to run immediately after it.
140              */
141             mainHandler.post(() -> callbacks.currentlyRecreatingToken = token);
142 
143             try {
144                 if (needsRelaunchCall()) {
145                     requestRelaunchActivityMethod.invoke(activityThread,
146                             token, null, null, 0, false, null, null, false, false);
147                 } else {
148                     activity.recreate();
149                 }
150                 return true;
151             } finally {
152                 mainHandler.post(() -> {
153                     // Since we're calling hidden API, it's entirely possible for it to
154                     // simply do nothing;
155                     // if that's the case, make sure to unregister so we don't leak memory
156                     // waiting for an event that will never happen.
157                     application.unregisterActivityLifecycleCallbacks(callbacks);
158                 });
159             }
160         } catch (Throwable t) {
161             return false;
162         }
163     }
164 
165     // Only reachable on SDK_INT < 28
166     private static final class LifecycleCheckCallbacks implements ActivityLifecycleCallbacks {
167         Object currentlyRecreatingToken;
168 
169         private Activity mActivity;
170         private final int mRecreatingHashCode;
171 
172         // Whether the activity on which recreate() was called went through onStart after
173         // recreate() was called (and thus the callback was registered).
174         private boolean mStarted = false;
175 
176         // Whether the activity on which recreate() was called went through onDestroy after
177         // recreate() was called. This means we successfully initiated a recreate().
178         private boolean mDestroyed = false;
179 
180         // Whether we'll force the activity on which recreate() was called to go through an
181         // onStop()
182         private boolean mStopQueued = false;
183 
LifecycleCheckCallbacks(@onNull Activity aboutToRecreate)184         LifecycleCheckCallbacks(@NonNull Activity aboutToRecreate) {
185             mActivity = aboutToRecreate;
186             mRecreatingHashCode = mActivity.hashCode();
187         }
188 
189         @Override
onActivityCreated(Activity activity, Bundle savedInstanceState)190         public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
191         }
192 
193         @Override
onActivityStarted(Activity activity)194         public void onActivityStarted(Activity activity) {
195             // If we see a start call on the original mActivity instance, then the mActivity
196             // starting event executed between our call to recreate() and the actual
197             // recreation of the mActivity. In that case, a stop() call should not be scheduled.
198             if (mActivity == activity) {
199                 mStarted = true;
200             }
201         }
202 
203         @Override
onActivityResumed(Activity activity)204         public void onActivityResumed(Activity activity) {
205         }
206 
207         @Override
onActivityPaused(Activity activity)208         public void onActivityPaused(Activity activity) {
209             if (mDestroyed // Original mActivity must be gone
210                     && !mStopQueued // Don't schedule stop twice for one recreate() call
211                     && !mStarted
212                     // Don't schedule stop if the original instance starting raced with recreate()
213                     && queueOnStopIfNecessary(
214                     currentlyRecreatingToken, mRecreatingHashCode, activity)) {
215                 mStopQueued = true;
216                 // Don't retain this object longer than necessary
217                 currentlyRecreatingToken = null;
218             }
219         }
220 
221         @Override
onActivitySaveInstanceState(Activity activity, Bundle outState)222         public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
223         }
224 
225         @Override
onActivityStopped(Activity activity)226         public void onActivityStopped(Activity activity) {
227             // Not possible to get a start/stop pair in the same UI thread loop
228         }
229 
230         @Override
onActivityDestroyed(Activity activity)231         public void onActivityDestroyed(Activity activity) {
232             if (mActivity == activity) {
233                 // Once the original mActivity instance is mDestroyed, we don't need to compare to
234                 // it any
235                 // longer, and we don't want to retain it any longer than necessary.
236                 mActivity = null;
237                 mDestroyed = true;
238             }
239         }
240     }
241 
242     /**
243      * Returns true if a stop call was scheduled successfully.
244      *
245      * Only reachable on SDK < 28.
246      */
queueOnStopIfNecessary( Object currentlyRecreatingToken, int currentlyRecreatingHashCode, Activity activity)247     protected static boolean queueOnStopIfNecessary(
248             Object currentlyRecreatingToken, int currentlyRecreatingHashCode, Activity activity) {
249         try {
250             final Object token = tokenField.get(activity);
251             if (token != currentlyRecreatingToken
252                     || activity.hashCode() != currentlyRecreatingHashCode) {
253                 // We're looking at a different activity, don't try to make it stop! Note that
254                 // tokens are reused on SDK 21-23 but Activity objects (and thus hashCode, in
255                 // all likelihood) are not, so we need to check both.
256                 return false;
257             }
258             final Object activityThread = mainThreadField.get(activity);
259             // These operations are posted at the front of the queue, so that operations
260             // scheduled from onCreate, onStart etc run after the onStop call - this should
261             // cause any redundant loads to be immediately cancelled.
262             mainHandler.postAtFrontOfQueue(() -> {
263                 try {
264                     if (SDK_INT < 28) {
265                         if (performStopActivity3ParamsMethod != null) {
266                             performStopActivity3ParamsMethod.invoke(activityThread,
267                                     token, false, "AppCompat recreation");
268                         } else {
269                             performStopActivity2ParamsMethod.invoke(activityThread,
270                                     token, false);
271                         }
272                     }
273                 } catch (RuntimeException e) {
274                     // If an Activity throws from onStop, don't swallow it
275                     if (e.getClass() == RuntimeException.class
276                             && e.getMessage() != null
277                             && e.getMessage().startsWith("Unable to stop")) {
278                         throw e;
279                     }
280                     // Otherwise just swallow it - we're calling random private methods,
281                     // there's no guarantee on how they'll behave.
282                 } catch (Throwable t) {
283                     Log.e(LOG_TAG, "Exception while invoking performStopActivity", t);
284                 }
285             });
286             return true;
287         } catch (Throwable t) {
288             Log.e(LOG_TAG, "Exception while fetching field values", t);
289             return false;
290         }
291     }
292 
getPerformStopActivity3Params(Class<?> activityThreadClass)293     private static Method getPerformStopActivity3Params(Class<?> activityThreadClass) {
294         if (activityThreadClass == null) {
295             return null;
296         }
297         try {
298             Method performStop = activityThreadClass.getDeclaredMethod("performStopActivity",
299                     IBinder.class, boolean.class, String.class);
300             performStop.setAccessible(true);
301             return performStop;
302         } catch (Throwable t) {
303             return null;
304         }
305     }
306 
getPerformStopActivity2Params(Class<?> activityThreadClass)307     private static Method getPerformStopActivity2Params(Class<?> activityThreadClass) {
308         if (activityThreadClass == null) {
309             return null;
310         }
311         try {
312             Method performStop = activityThreadClass.getDeclaredMethod("performStopActivity",
313                     IBinder.class, boolean.class);
314             performStop.setAccessible(true);
315             return performStop;
316         } catch (Throwable t) {
317             return null;
318         }
319     }
320 
321     @ChecksSdkIntAtLeast(api = 26)
needsRelaunchCall()322     private static boolean needsRelaunchCall() {
323         return SDK_INT == 26 || SDK_INT == 27;
324     }
325 
getRequestRelaunchActivityMethod(Class<?> activityThreadClass)326     private static Method getRequestRelaunchActivityMethod(Class<?> activityThreadClass) {
327         if (!needsRelaunchCall() || activityThreadClass == null) {
328             return null;
329         }
330         try {
331             Method relaunch = activityThreadClass.getDeclaredMethod(
332                     "requestRelaunchActivity",
333                     IBinder.class,
334                     List.class,
335                     List.class,
336                     int.class,
337                     boolean.class,
338                     Configuration.class,
339                     Configuration.class,
340                     boolean.class,
341                     boolean.class);
342             relaunch.setAccessible(true);
343             return relaunch;
344         } catch (Throwable t) {
345             return null;
346         }
347     }
348 
getMainThreadField()349     private static Field getMainThreadField() {
350         try {
351             Field mainThreadField = Activity.class.getDeclaredField("mMainThread");
352             mainThreadField.setAccessible(true);
353             return mainThreadField;
354         } catch (Throwable t) {
355             return null;
356         }
357     }
358 
getTokenField()359     private static Field getTokenField() {
360         try {
361             Field tokenField = Activity.class.getDeclaredField("mToken");
362             tokenField.setAccessible(true);
363             return tokenField;
364         } catch (Throwable t) {
365             return null;
366         }
367     }
368 
getActivityThreadClass()369     private static Class<?> getActivityThreadClass() {
370         try {
371             return Class.forName("android.app.ActivityThread");
372         } catch (Throwable t) {
373             return null;
374         }
375     }
376 }
377