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