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