blob: f8f73b134c4509957251a50e046e5fbc647b1b28 [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.
*/
package androidx.fragment.app.strictmode
import android.annotation.SuppressLint
import android.os.Looper
import android.util.Log
import android.view.ViewGroup
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.strictmode.FragmentStrictMode.Policy
/**
* FragmentStrictMode is a tool which detects things you might be doing by accident and brings
* them to your attention so you can fix them. Basically, it's a version of
* [android.os.StrictMode] specifically for fragment-related issues.
*
* You can decide what should happen when a violation is detected. For example, using
* [Policy.Builder.penaltyLog] you can watch the output of `adb logcat` while you
* use your application to see the violations as they happen.
*/
object FragmentStrictMode {
private const val TAG = "FragmentStrictMode"
/**
* The current policy for what actions should be detected, as well as the penalty if such
* actions occur.
*/
var defaultPolicy = Policy.LAX
private fun getNearestPolicy(fragment: Fragment?): Policy {
var declaringFragment = fragment
while (declaringFragment != null) {
if (declaringFragment.isAdded) {
val fragmentManager = declaringFragment.parentFragmentManager
if (fragmentManager.strictModePolicy != null) {
return fragmentManager.strictModePolicy!!
}
}
declaringFragment = declaringFragment.parentFragment
}
return defaultPolicy
}
/**
* @hide
*/
@JvmStatic
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun onFragmentReuse(fragment: Fragment, previousFragmentId: String) {
val violation: Violation = FragmentReuseViolation(fragment, previousFragmentId)
logIfDebuggingEnabled(violation)
val policy = getNearestPolicy(fragment)
if (policy.flags.contains(Flag.DETECT_FRAGMENT_REUSE) &&
shouldHandlePolicyViolation(policy, fragment.javaClass, violation.javaClass)
) {
handlePolicyViolation(policy, violation)
}
}
/**
* @hide
*/
@JvmStatic
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun onFragmentTagUsage(
fragment: Fragment,
container: ViewGroup?
) {
val violation: Violation = FragmentTagUsageViolation(fragment, container)
logIfDebuggingEnabled(violation)
val policy = getNearestPolicy(fragment)
if (policy.flags.contains(Flag.DETECT_FRAGMENT_TAG_USAGE) &&
shouldHandlePolicyViolation(policy, fragment.javaClass, violation.javaClass)
) {
handlePolicyViolation(policy, violation)
}
}
/**
* @hide
*/
@JvmStatic
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun onSetRetainInstanceUsage(fragment: Fragment) {
val violation: Violation = SetRetainInstanceUsageViolation(fragment)
logIfDebuggingEnabled(violation)
val policy = getNearestPolicy(fragment)
if (policy.flags.contains(Flag.DETECT_RETAIN_INSTANCE_USAGE) &&
shouldHandlePolicyViolation(policy, fragment.javaClass, violation.javaClass)
) {
handlePolicyViolation(policy, violation)
}
}
/**
* @hide
*/
@JvmStatic
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun onGetRetainInstanceUsage(fragment: Fragment) {
val violation: Violation = GetRetainInstanceUsageViolation(fragment)
logIfDebuggingEnabled(violation)
val policy = getNearestPolicy(fragment)
if (policy.flags.contains(Flag.DETECT_RETAIN_INSTANCE_USAGE) &&
shouldHandlePolicyViolation(policy, fragment.javaClass, violation.javaClass)
) {
handlePolicyViolation(policy, violation)
}
}
/**
* @hide
*/
@JvmStatic
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun onSetUserVisibleHint(fragment: Fragment, isVisibleToUser: Boolean) {
val violation: Violation = SetUserVisibleHintViolation(fragment, isVisibleToUser)
logIfDebuggingEnabled(violation)
val policy = getNearestPolicy(fragment)
if (policy.flags.contains(Flag.DETECT_SET_USER_VISIBLE_HINT) &&
shouldHandlePolicyViolation(policy, fragment.javaClass, violation.javaClass)
) {
handlePolicyViolation(policy, violation)
}
}
/**
* @hide
*/
@JvmStatic
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun onSetTargetFragmentUsage(
violatingFragment: Fragment,
targetFragment: Fragment,
requestCode: Int
) {
val violation: Violation = SetTargetFragmentUsageViolation(
violatingFragment, targetFragment, requestCode
)
logIfDebuggingEnabled(violation)
val policy = getNearestPolicy(violatingFragment)
if (policy.flags.contains(Flag.DETECT_TARGET_FRAGMENT_USAGE) &&
shouldHandlePolicyViolation(policy, violatingFragment.javaClass, violation.javaClass)
) {
handlePolicyViolation(policy, violation)
}
}
/**
* @hide
*/
@JvmStatic
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun onGetTargetFragmentUsage(fragment: Fragment) {
val violation: Violation = GetTargetFragmentUsageViolation(fragment)
logIfDebuggingEnabled(violation)
val policy = getNearestPolicy(fragment)
if (policy.flags.contains(Flag.DETECT_TARGET_FRAGMENT_USAGE) &&
shouldHandlePolicyViolation(policy, fragment.javaClass, violation.javaClass)
) {
handlePolicyViolation(policy, violation)
}
}
/**
* @hide
*/
@JvmStatic
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun onGetTargetFragmentRequestCodeUsage(fragment: Fragment) {
val violation: Violation = GetTargetFragmentRequestCodeUsageViolation(fragment)
logIfDebuggingEnabled(violation)
val policy = getNearestPolicy(fragment)
if (policy.flags.contains(Flag.DETECT_TARGET_FRAGMENT_USAGE) &&
shouldHandlePolicyViolation(policy, fragment.javaClass, violation.javaClass)
) {
handlePolicyViolation(policy, violation)
}
}
/**
* @hide
*/
@JvmStatic
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun onWrongFragmentContainer(
fragment: Fragment,
container: ViewGroup
) {
val violation: Violation = WrongFragmentContainerViolation(fragment, container)
logIfDebuggingEnabled(violation)
val policy = getNearestPolicy(fragment)
if (policy.flags.contains(Flag.DETECT_WRONG_FRAGMENT_CONTAINER) &&
shouldHandlePolicyViolation(policy, fragment.javaClass, violation.javaClass)
) {
handlePolicyViolation(policy, violation)
}
}
@VisibleForTesting
fun onPolicyViolation(violation: Violation) {
logIfDebuggingEnabled(violation)
val fragment: Fragment = violation.fragment
val policy = getNearestPolicy(fragment)
if (shouldHandlePolicyViolation(policy, fragment.javaClass, violation.javaClass)) {
handlePolicyViolation(policy, violation)
}
}
private fun logIfDebuggingEnabled(violation: Violation) {
if (FragmentManager.isLoggingEnabled(Log.DEBUG)) {
Log.d(
FragmentManager.TAG,
"StrictMode violation in ${violation.fragment.javaClass.name}",
violation
)
}
}
private fun shouldHandlePolicyViolation(
policy: Policy,
fragmentClass: Class<out Fragment>,
violationClass: Class<out Violation>
): Boolean {
val violationsToBypass = policy.mAllowedViolations[fragmentClass] ?: return true
if (violationClass.superclass != Violation::class.java) {
if (violationsToBypass.contains(violationClass.superclass)) {
return false
}
}
return !violationsToBypass.contains(violationClass)
}
private fun handlePolicyViolation(
policy: Policy,
violation: Violation
) {
val fragment: Fragment = violation.fragment
val fragmentName = fragment.javaClass.name
if (policy.flags.contains(Flag.PENALTY_LOG)) {
Log.d(TAG, "Policy violation in $fragmentName", violation)
}
if (policy.listener != null) {
runOnHostThread(fragment) { policy.listener.onViolation(violation) }
}
if (policy.flags.contains(Flag.PENALTY_DEATH)) {
runOnHostThread(fragment) {
Log.e(TAG, "Policy violation with PENALTY_DEATH in $fragmentName", violation)
throw violation
}
}
}
private fun runOnHostThread(fragment: Fragment, runnable: Runnable) {
if (fragment.isAdded) {
val handler = fragment.parentFragmentManager.host.handler
if (handler.looper == Looper.myLooper()) {
runnable.run() // Already on correct thread -> run synchronously
} else {
handler.post(runnable) // Switch to correct thread
}
} else {
runnable.run() // Fragment is not attached to any host -> run synchronously
}
}
internal enum class Flag {
PENALTY_LOG,
PENALTY_DEATH,
DETECT_FRAGMENT_REUSE,
DETECT_FRAGMENT_TAG_USAGE,
DETECT_RETAIN_INSTANCE_USAGE,
DETECT_SET_USER_VISIBLE_HINT,
DETECT_TARGET_FRAGMENT_USAGE,
DETECT_WRONG_FRAGMENT_CONTAINER
}
/**
* When [Policy.Builder.penaltyListener] is enabled, the listener is called when a
* violation occurs.
*/
fun interface OnViolationListener {
/** Called on a policy violation. */
fun onViolation(violation: Violation)
}
/**
* [FragmentStrictMode] policy applied to a certain [FragmentManager] (or globally).
*
* This policy can either be enabled globally using [defaultPolicy] or for a
* specific [FragmentManager] using [FragmentManager.setStrictModePolicy].
* The current policy can be retrieved using [defaultPolicy] and
* [FragmentManager.getStrictModePolicy] respectively.
*
* Note that multiple penalties may be provided and they're run in order from least to most
* severe (logging before process death, for example). There's currently no mechanism to choose
* different penalties for different detected actions.
*/
class Policy internal constructor(
internal val flags: Set<Flag>,
listener: OnViolationListener?,
allowedViolations: Map<Class<out Fragment>, MutableSet<Class<out Violation>>>
) {
internal val listener: OnViolationListener?
internal val mAllowedViolations: Map<Class<out Fragment>, Set<Class<out Violation>>>
/**
* Creates [Policy] instances. Methods whose names start with `detect` specify
* what problems we should look for. Methods whose names start with `penalty` specify
* what we should do when we detect a problem.
*
* You can call as many `detect` and `penalty` methods as you like. Currently
* order is insignificant: all penalties apply to all detected problems.
*/
class Builder {
private val flags: MutableSet<Flag> = mutableSetOf()
private var listener: OnViolationListener? = null
private val mAllowedViolations:
MutableMap<Class<out Fragment>, MutableSet<Class<out Violation>>> = mutableMapOf()
/** Log detected violations to the system log. */
@SuppressLint("BuilderSetStyle")
fun penaltyLog(): Builder {
flags.add(Flag.PENALTY_LOG)
return this
}
/**
* Throws an exception on violation. This penalty runs at the end of all enabled
* penalties so you'll still get to see logging or other violations before the exception
* is thrown.
*/
@SuppressLint("BuilderSetStyle")
fun penaltyDeath(): Builder {
flags.add(Flag.PENALTY_DEATH)
return this
}
/**
* Call [OnViolationListener.onViolation] for every violation. The listener will
* be called on the main thread of the fragment host.
*/
@SuppressLint("BuilderSetStyle")
fun penaltyListener(listener: OnViolationListener): Builder {
this.listener = listener
return this
}
/**
* Detects cases, where a [Fragment] instance is reused, after it was previously
* removed from a [FragmentManager].
*/
@SuppressLint("BuilderSetStyle")
fun detectFragmentReuse(): Builder {
flags.add(Flag.DETECT_FRAGMENT_REUSE)
return this
}
/** Detects usage of the <fragment> tag inside XML layouts. */
@SuppressLint("BuilderSetStyle")
fun detectFragmentTagUsage(): Builder {
flags.add(Flag.DETECT_FRAGMENT_TAG_USAGE)
return this
}
/**
* Detects calls to [Fragment.setRetainInstance] and [Fragment.getRetainInstance].
*/
@SuppressLint("BuilderSetStyle")
fun detectRetainInstanceUsage(): Builder {
flags.add(Flag.DETECT_RETAIN_INSTANCE_USAGE)
return this
}
/** Detects calls to [Fragment.setUserVisibleHint]. */
@SuppressLint("BuilderSetStyle")
fun detectSetUserVisibleHint(): Builder {
flags.add(Flag.DETECT_SET_USER_VISIBLE_HINT)
return this
}
/**
* Detects calls to [Fragment.setTargetFragment], [Fragment.getTargetFragment] and
* [Fragment.getTargetRequestCode].
*/
@SuppressLint("BuilderSetStyle")
fun detectTargetFragmentUsage(): Builder {
flags.add(Flag.DETECT_TARGET_FRAGMENT_USAGE)
return this
}
/**
* Detects cases where a [Fragment] is added to a container other than a
* [androidx.fragment.app.FragmentContainerView].
*/
@SuppressLint("BuilderSetStyle")
fun detectWrongFragmentContainer(): Builder {
flags.add(Flag.DETECT_WRONG_FRAGMENT_CONTAINER)
return this
}
/**
* Allow the specified [Fragment] class to bypass penalties for the specified
* [Violation], if detected.
*
* By default, all [Fragment] classes will incur penalties for any detected [Violation].
*/
@SuppressLint("BuilderSetStyle")
fun allowViolation(
fragmentClass: Class<out Fragment>,
violationClass: Class<out Violation>
): Builder {
var violationsToBypass = mAllowedViolations[fragmentClass]
if (violationsToBypass == null) {
violationsToBypass = mutableSetOf()
}
violationsToBypass.add(violationClass)
mAllowedViolations[fragmentClass] = violationsToBypass
return this
}
/**
* Construct the Policy instance.
*
* Note: if no penalties are enabled before calling `build`, [penaltyLog] is implicitly
* set.
*/
fun build(): Policy {
if (listener == null && !flags.contains(Flag.PENALTY_DEATH)) {
penaltyLog()
}
return Policy(flags, listener, mAllowedViolations)
}
}
internal companion object {
/** The default, lax policy which doesn't catch anything. */
@JvmField
val LAX = Policy(emptySet(), null, emptyMap())
}
init {
this.listener = listener
val newAllowedViolationsMap:
MutableMap<Class<out Fragment>, Set<Class<out Violation>>> = mutableMapOf()
for ((key, value) in allowedViolations) {
newAllowedViolationsMap[key] = value
}
mAllowedViolations = newAllowedViolationsMap
}
}
}