blob: 1446e0471ffa00d6623b7a9bb3c1cfb0b414f89a [file] [log] [blame]
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// This file is a conversion of ActivityRecreatorChecked.java, using the built-in Android Studio
// Java-to-Kotlin convertor.
package androidx.sample.core.app
import android.app.Activity
import android.app.Application.ActivityLifecycleCallbacks
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.util.Log
import androidx.annotation.ChecksSdkIntAtLeast
import androidx.annotation.RestrictTo
import java.lang.reflect.Field
import java.lang.reflect.Method
/**
* The goal here is to get common (and correct) behavior around Activity recreation for all API
* versions up until P, where the behavior was specified to be useful and implemented to match the
* specification. On API 26 and 27, recreate() doesn't actually recreate the Activity if it's
* not in the foreground; it will be recreated when the user next interacts with it. This has a few
* undesirable consequences:
*
*
* 1. It's impossible to recreate multiple activities at once, which means that activities in the
* background will observe the new configuration before they're recreated. If we keep them on the
* old configuration, we have two conflicting configurations active in the app, which leads to
* logging skew.
*
*
* 2. Recreation occurs in the critical path of user interaction - re-inflating a bunch of views
* isn't free, and we'd rather do it when we're in the background than when the user is staring at
* the screen waiting to see us.
*
*
* On API < 26, recreate() was implemented with a single call to a private method on
* ActivityThread. That method still exists in 26 and 27, so we can use reflection to call it and
* get the exact same behavior as < 26. However, that behavior has problems itself. When
* an Activity in the background is recreated, it goes through: destroy -> create -> start ->
* resume -> pause and doesn't stop. This is a violation of the contract for onStart/onStop,
* but that might be palatable if it didn't also have the effect of preventing new configurations
* from being applied - since the Activity doesn't go through onStop, our tracking of whether
* our app is visible thinks we're always visible, and thus can't do another recreation later.
*
*
* The fix for this is to add the missing onStop() call, by using reflection to call into
* ActivityThread.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
internal object ActivityRecreatorKtChecked {
private const val LOG_TAG = "ActivityRecreatorChecked"
// Activity.mMainThread
private var mainThreadField: Field? = null
// Activity.mToken. This object is an identifier that is the same between multiple instances of
// the same underlying Activity.
private var tokenField: Field? = null
// On API 25, a third param was added to performStopActivity
private var performStopActivity3ParamsMethod: Method? = null
// Before API 25, performStopActivity had two params
private var performStopActivity2ParamsMethod: Method? = null
// ActivityThread.requestRelaunchActivity
private var requestRelaunchActivityMethod: Method? = null
private val mainHandler = Handler(Looper.getMainLooper())
/**
* Equivalent to [Activity.recreate], but working around a number of platform bugs.
*
* @return true if a recreate() task was successfully scheduled.
*/
fun recreate(activity: Activity): Boolean {
// On Android O and later we can rely on the platform recreate()
if (Build.VERSION.SDK_INT >= 28) {
activity.recreate()
return true
}
// API 26 needs this workaround but it's not possible because our reflective lookup failed.
if (needsRelaunchCall() && requestRelaunchActivityMethod == null) {
return false
}
// All versions of android so far need this workaround, but it's not possible because our
// reflective lookup failed.
if (performStopActivity2ParamsMethod == null && performStopActivity3ParamsMethod == null) {
return false
}
return try {
val token = tokenField!!.get(activity)
?: return false
val activityThread = mainThreadField!!.get(activity)
?: return false
val application = activity.application
val callbacks = LifecycleCheckCallbacks(activity)
application.registerActivityLifecycleCallbacks(callbacks)
/*
* Runnables scheduled before/after recreate() will run before and after the Runnables
* scheduled by recreate(). This allows us to bound the time where mActivity lifecycle
* events that could be caused by recreate() run - that way we can detect onPause()
* from the new Activity instance, and schedule onStop to run immediately after it.
*/mainHandler.post {
callbacks.currentlyRecreatingToken = token
}
try {
if (needsRelaunchCall()) {
requestRelaunchActivityMethod!!.invoke(
activityThread,
token, null, null, 0, false, null, null, false, false
)
} else {
activity.recreate()
}
true
} finally {
mainHandler.post {
// Since we're calling hidden API, it's entirely possible for it to
// simply do nothing;
// if that's the case, make sure to unregister so we don't leak memory
// waiting for an event that will never happen.
application.unregisterActivityLifecycleCallbacks(callbacks)
}
}
} catch (t: Throwable) {
false
}
}
/**
* Returns true if a stop call was scheduled successfully.
*
* Only reachable on SDK < 28.
*/
internal fun queueOnStopIfNecessary(
currentlyRecreatingToken: Any?,
currentlyRecreatingHashCode: Int,
activity: Activity
): Boolean {
return try {
val token = tokenField!![activity]
if (token !== currentlyRecreatingToken ||
activity.hashCode() != currentlyRecreatingHashCode
) {
// We're looking at a different activity, don't try to make it stop! Note that
// tokens are reused on SDK 21-23 but Activity objects (and thus hashCode, in
// all likelihood) are not, so we need to check both.
return false
}
val activityThread = mainThreadField!![activity]
// These operations are posted at the front of the queue, so that operations
// scheduled from onCreate, onStart etc run after the onStop call - this should
// cause any redundant loads to be immediately cancelled.
mainHandler.postAtFrontOfQueue {
try {
if (Build.VERSION.SDK_INT < 28) {
if (performStopActivity3ParamsMethod != null) {
performStopActivity3ParamsMethod!!.invoke(
activityThread,
token, false, "AppCompat recreation"
)
} else {
performStopActivity2ParamsMethod!!.invoke(
activityThread,
token, false
)
}
}
} catch (e: RuntimeException) {
// If an Activity throws from onStop, don't swallow it
if ((e.javaClass == RuntimeException::class.java) &&
(e.message != null) &&
e.message!!.startsWith("Unable to stop")
) {
throw e
}
// Otherwise just swallow it - we're calling random private methods,
// there's no guarantee on how they'll behave.
} catch (t: Throwable) {
Log.e(
LOG_TAG,
"Exception while invoking performStopActivity",
t
)
}
}
true
} catch (t: Throwable) {
Log.e(LOG_TAG, "Exception while fetching field values", t)
false
}
}
private fun getPerformStopActivity3Params(activityThreadClass: Class<*>?): Method? {
if (activityThreadClass == null) {
return null
}
return try {
val performStop = activityThreadClass.getDeclaredMethod(
"performStopActivity",
IBinder::class.java, Boolean::class.javaPrimitiveType, String::class.java
)
performStop.isAccessible = true
performStop
} catch (t: Throwable) {
null
}
}
private fun getPerformStopActivity2Params(activityThreadClass: Class<*>?): Method? {
if (activityThreadClass == null) {
return null
}
return try {
val performStop = activityThreadClass.getDeclaredMethod(
"performStopActivity",
IBinder::class.java, Boolean::class.javaPrimitiveType
)
performStop.isAccessible = true
performStop
} catch (t: Throwable) {
null
}
}
@ChecksSdkIntAtLeast(api = 26)
private fun needsRelaunchCall(): Boolean {
return Build.VERSION.SDK_INT == 26 || Build.VERSION.SDK_INT == 27
}
private fun getRequestRelaunchActivityMethod(activityThreadClass: Class<*>?): Method? {
if (!needsRelaunchCall() || activityThreadClass == null) {
return null
}
return try {
val relaunch = activityThreadClass.getDeclaredMethod(
"requestRelaunchActivity",
IBinder::class.java,
MutableList::class.java,
MutableList::class.java,
Int::class.javaPrimitiveType,
Boolean::class.javaPrimitiveType,
Configuration::class.java,
Configuration::class.java,
Boolean::class.javaPrimitiveType,
Boolean::class.javaPrimitiveType
)
relaunch.isAccessible = true
relaunch
} catch (t: Throwable) {
null
}
}
private fun getMainThreadField(): Field? {
return try {
val mainThreadField = Activity::class.java.getDeclaredField("mMainThread")
mainThreadField.isAccessible = true
mainThreadField
} catch (t: Throwable) {
null
}
}
private fun getTokenField(): Field? {
return try {
val tokenField = Activity::class.java.getDeclaredField("mToken")
tokenField.isAccessible = true
tokenField
} catch (t: Throwable) {
null
}
}
private val activityThreadClass: Class<*>?
get() = try {
Class.forName("android.app.ActivityThread")
} catch (t: Throwable) {
null
}
// Only reachable on SDK_INT < 28
private class LifecycleCheckCallbacks internal constructor(aboutToRecreate: Activity) :
ActivityLifecycleCallbacks {
var currentlyRecreatingToken: Any? = null
private var mActivity: Activity?
private val mRecreatingHashCode: Int
// Whether the activity on which recreate() was called went through onStart after
// recreate() was called (and thus the callback was registered).
private var mStarted = false
// Whether the activity on which recreate() was called went through onDestroy after
// recreate() was called. This means we successfully initiated a recreate().
private var mDestroyed = false
// Whether we'll force the activity on which recreate() was called to go through an
// onStop()
private var mStopQueued = false
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
override fun onActivityStarted(activity: Activity) {
// If we see a start call on the original mActivity instance, then the mActivity
// starting event executed between our call to recreate() and the actual
// recreation of the mActivity. In that case, a stop() call should not be scheduled.
if (mActivity === activity) {
mStarted = true
}
}
override fun onActivityResumed(activity: Activity) {}
override fun onActivityPaused(activity: Activity) {
if (mDestroyed && // Original mActivity must be gone
!mStopQueued && // Don't schedule stop twice for one recreate() call
!mStarted && // Don't schedule stop if the original instance starting raced with...
queueOnStopIfNecessary(currentlyRecreatingToken, mRecreatingHashCode, activity)
) {
mStopQueued = true
// Don't retain this object longer than necessary
currentlyRecreatingToken = null
}
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityStopped(activity: Activity) {
// Not possible to get a start/stop pair in the same UI thread loop
}
override fun onActivityDestroyed(activity: Activity) {
if (mActivity === activity) {
// Once the original mActivity instance is mDestroyed, we don't need to compare to
// it any
// longer, and we don't want to retain it any longer than necessary.
mActivity = null
mDestroyed = true
}
}
init {
mActivity = aboutToRecreate
mRecreatingHashCode = mActivity.hashCode()
}
}
init {
val activityThreadClass = activityThreadClass
mainThreadField = getMainThreadField()
tokenField = getTokenField()
performStopActivity3ParamsMethod =
getPerformStopActivity3Params(activityThreadClass)
performStopActivity2ParamsMethod =
getPerformStopActivity2Params(activityThreadClass)
requestRelaunchActivityMethod =
getRequestRelaunchActivityMethod(activityThreadClass)
}
}