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