[go: nahoru, domu]

blob: 4c4368a9199a7ae5e9189cc84ccdf771c95d2a86 [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.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentContainerView;
import androidx.fragment.app.FragmentManager;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* 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
* {@link android.os.StrictMode} specifically for fragment-related issues.
*
* <p>You can decide what should happen when a violation is detected. For example, using {@link
* Policy.Builder#penaltyLog} you can watch the output of <code>adb logcat</code> while you
* use your application to see the violations as they happen.
*/
@SuppressLint("SyntheticAccessor")
public final class FragmentStrictMode {
private static final String TAG = "FragmentStrictMode";
private static Policy defaultPolicy = Policy.LAX;
private enum 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,
}
private FragmentStrictMode() {}
/**
* When #{@link Policy.Builder#penaltyListener} is enabled, the listener is called when a
* violation occurs.
*/
public interface OnViolationListener {
/** Called on a policy violation. */
void onViolation(@NonNull Violation violation);
}
/**
* {@link FragmentStrictMode} policy applied to a certain {@link FragmentManager} (or globally).
*
* <p>This policy can either be enabled globally using {@link #setDefaultPolicy} or for a
* specific {@link FragmentManager} using {@link FragmentManager#setStrictModePolicy(Policy)}.
* The current policy can be retrieved using {@link #getDefaultPolicy} and
* {@link FragmentManager#getStrictModePolicy} respectively.
*
* <p>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.
*/
public static final class Policy {
private final Set<Flag> mFlags;
private final OnViolationListener mListener;
private final Map<Class<? extends Fragment>,
Set<Class<? extends Violation>>> mAllowedViolations;
/** The default, lax policy which doesn't catch anything. */
@NonNull
public static final Policy LAX = new Policy(new HashSet<>(), null, new HashMap<>());
private Policy(
@NonNull Set<Flag> flags,
@Nullable OnViolationListener listener,
@NonNull Map<Class<? extends Fragment>,
Set<Class<? extends Violation>>> allowedViolations) {
this.mFlags = new HashSet<>(flags);
this.mListener = listener;
Map<Class<? extends Fragment>, Set<Class<? extends Violation>>>
newAllowedViolationsMap = new HashMap<>();
for (Map.Entry<Class<? extends Fragment>,
Set<Class<? extends Violation>>> entry : allowedViolations.entrySet()) {
newAllowedViolationsMap.put(entry.getKey(), new HashSet<>(entry.getValue()));
}
this.mAllowedViolations = newAllowedViolationsMap;
}
/**
* Creates {@link Policy} instances. Methods whose names start with {@code detect} specify
* what problems we should look for. Methods whose names start with {@code penalty} specify
* what we should do when we detect a problem.
*
* <p>You can call as many {@code detect} and {@code penalty} methods as you like. Currently
* order is insignificant: all penalties apply to all detected problems.
*/
public static final class Builder {
private final Set<Flag> mFlags;
private OnViolationListener mListener;
private final Map<Class<? extends Fragment>,
Set<Class<? extends Violation>>> mAllowedViolations;
/** Create a Builder that detects nothing and has no violations. */
public Builder() {
mFlags = new HashSet<>();
mAllowedViolations = new HashMap<>();
}
/** Log detected violations to the system log. */
@NonNull
@SuppressLint("BuilderSetStyle")
public Builder penaltyLog() {
mFlags.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.
*/
@NonNull
@SuppressLint("BuilderSetStyle")
public Builder penaltyDeath() {
mFlags.add(Flag.PENALTY_DEATH);
return this;
}
/**
* Call #{@link OnViolationListener#onViolation} for every violation. The listener will
* be called on the main thread of the fragment host.
*/
@NonNull
@SuppressLint("BuilderSetStyle")
public Builder penaltyListener(@NonNull OnViolationListener listener) {
this.mListener = listener;
return this;
}
/**
* Detects cases, where a #{@link Fragment} instance is reused, after it was previously
* removed from a #{@link FragmentManager}.
*/
@NonNull
@SuppressLint("BuilderSetStyle")
public Builder detectFragmentReuse() {
mFlags.add(Flag.DETECT_FRAGMENT_REUSE);
return this;
}
/** Detects usage of the &lt;fragment&gt; tag inside XML layouts. */
@NonNull
@SuppressLint("BuilderSetStyle")
public Builder detectFragmentTagUsage() {
mFlags.add(Flag.DETECT_FRAGMENT_TAG_USAGE);
return this;
}
/**
* Detects calls to #{@link Fragment#setRetainInstance} and
* #{@link Fragment#getRetainInstance()}.
*/
@NonNull
@SuppressLint("BuilderSetStyle")
public Builder detectRetainInstanceUsage() {
mFlags.add(Flag.DETECT_RETAIN_INSTANCE_USAGE);
return this;
}
/** Detects calls to #{@link Fragment#setUserVisibleHint}. */
@NonNull
@SuppressLint("BuilderSetStyle")
public Builder detectSetUserVisibleHint() {
mFlags.add(Flag.DETECT_SET_USER_VISIBLE_HINT);
return this;
}
/**
* Detects calls to #{@link Fragment#setTargetFragment},
* #{@link Fragment#getTargetFragment()} and #{@link Fragment#getTargetRequestCode()}.
*/
@NonNull
@SuppressLint("BuilderSetStyle")
public Builder detectTargetFragmentUsage() {
mFlags.add(Flag.DETECT_TARGET_FRAGMENT_USAGE);
return this;
}
/**
* Detects cases where a #{@link Fragment} is added to a container other than a
* #{@link FragmentContainerView}.
*/
@NonNull
@SuppressLint("BuilderSetStyle")
public Builder detectWrongFragmentContainer() {
mFlags.add(Flag.DETECT_WRONG_FRAGMENT_CONTAINER);
return this;
}
/**
* Allow the specified {@link Fragment} class to bypass penalties for the
* specified {@link Violation}, if detected.
*
* By default, all {@link Fragment} classes will incur penalties for any
* detected {@link Violation}.
*/
@NonNull
@SuppressLint("BuilderSetStyle")
public Builder allowViolation(
@NonNull Class<? extends Fragment> fragmentClass,
@NonNull Class<? extends Violation> violationClass) {
Set<Class<? extends Violation>> violationsToBypass =
mAllowedViolations.get(fragmentClass);
if (violationsToBypass == null) {
violationsToBypass = new HashSet<>();
}
violationsToBypass.add(violationClass);
mAllowedViolations.put(fragmentClass, violationsToBypass);
return this;
}
/**
* Construct the Policy instance.
*
* <p>Note: if no penalties are enabled before calling <code>build</code>, {@link
* #penaltyLog} is implicitly set.
*/
@NonNull
public Policy build() {
if (mListener == null && !mFlags.contains(Flag.PENALTY_DEATH)) {
penaltyLog();
}
return new Policy(mFlags, mListener, mAllowedViolations);
}
}
}
/** Returns the current default policy. */
@NonNull
public static Policy getDefaultPolicy() {
return defaultPolicy;
}
/**
* Sets the policy for what actions should be detected, as well as the penalty if such actions
* occur.
*
* @param policy the policy to put into place
*/
public static void setDefaultPolicy(@NonNull Policy policy) {
defaultPolicy = policy;
}
private static Policy getNearestPolicy(@Nullable Fragment fragment) {
while (fragment != null) {
if (fragment.isAdded()) {
FragmentManager fragmentManager = fragment.getParentFragmentManager();
if (fragmentManager.getStrictModePolicy() != null) {
return fragmentManager.getStrictModePolicy();
}
}
fragment = fragment.getParentFragment();
}
return defaultPolicy;
}
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static void onFragmentReuse(@NonNull Fragment fragment) {
Violation violation = new FragmentReuseViolation();
logIfDebuggingEnabled(fragment.getClass().getName(), violation);
Policy policy = getNearestPolicy(fragment);
if (policy.mFlags.contains(Flag.DETECT_FRAGMENT_REUSE)
&& shouldHandlePolicyViolation(
fragment.getClass(), policy, violation.getClass())) {
handlePolicyViolation(fragment, policy, violation);
}
}
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static void onFragmentTagUsage(@NonNull Fragment fragment) {
Violation violation = new FragmentTagUsageViolation();
logIfDebuggingEnabled(fragment.getClass().getName(), violation);
Policy policy = getNearestPolicy(fragment);
if (policy.mFlags.contains(Flag.DETECT_FRAGMENT_TAG_USAGE)
&& shouldHandlePolicyViolation(
fragment.getClass(), policy, violation.getClass())) {
handlePolicyViolation(fragment, policy, violation);
}
}
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static void onRetainInstanceUsage(@NonNull Fragment fragment) {
Violation violation = new RetainInstanceUsageViolation();
logIfDebuggingEnabled(fragment.getClass().getName(), violation);
Policy policy = getNearestPolicy(fragment);
if (policy.mFlags.contains(Flag.DETECT_RETAIN_INSTANCE_USAGE)
&& shouldHandlePolicyViolation(
fragment.getClass(), policy, violation.getClass())) {
handlePolicyViolation(fragment, policy, violation);
}
}
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static void onSetUserVisibleHint(@NonNull Fragment fragment) {
Violation violation = new SetUserVisibleHintViolation();
logIfDebuggingEnabled(fragment.getClass().getName(), violation);
Policy policy = getNearestPolicy(fragment);
if (policy.mFlags.contains(Flag.DETECT_SET_USER_VISIBLE_HINT)
&& shouldHandlePolicyViolation(
fragment.getClass(), policy, violation.getClass())) {
handlePolicyViolation(fragment, policy, violation);
}
}
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static void onTargetFragmentUsage(@NonNull Fragment fragment) {
Violation violation = new TargetFragmentUsageViolation();
logIfDebuggingEnabled(fragment.getClass().getName(), violation);
Policy policy = getNearestPolicy(fragment);
if (policy.mFlags.contains(Flag.DETECT_TARGET_FRAGMENT_USAGE)
&& shouldHandlePolicyViolation(
fragment.getClass(), policy, violation.getClass())) {
handlePolicyViolation(fragment, policy, violation);
}
}
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static void onWrongFragmentContainer(@NonNull Fragment fragment) {
Violation violation = new WrongFragmentContainerViolation();
logIfDebuggingEnabled(fragment.getClass().getName(), violation);
Policy policy = getNearestPolicy(fragment);
if (policy.mFlags.contains(Flag.DETECT_WRONG_FRAGMENT_CONTAINER)
&& shouldHandlePolicyViolation(
fragment.getClass(), policy, violation.getClass())) {
handlePolicyViolation(fragment, policy, violation);
}
}
@VisibleForTesting
static void onPolicyViolation(@NonNull Fragment fragment, @NonNull Violation violation) {
logIfDebuggingEnabled(fragment.getClass().getName(), violation);
Policy policy = getNearestPolicy(fragment);
if (shouldHandlePolicyViolation(fragment.getClass(), policy, violation.getClass())) {
handlePolicyViolation(fragment, policy, violation);
}
}
private static void logIfDebuggingEnabled(
@NonNull String fragmentName,
@NonNull final Violation violation
) {
if (FragmentManager.isLoggingEnabled(Log.DEBUG)) {
Log.d(FragmentManager.TAG, "StrictMode violation in " + fragmentName,
violation);
}
}
private static boolean shouldHandlePolicyViolation(
@NonNull Class<? extends Fragment> fragmentClass,
@NonNull final Policy policy,
@NonNull Class<? extends Violation> violationClass) {
Set<Class<? extends Violation>> violationsToBypass =
policy.mAllowedViolations.get(fragmentClass);
return violationsToBypass == null || !violationsToBypass.contains(violationClass);
}
private static void handlePolicyViolation(
@NonNull Fragment fragment,
@NonNull final Policy policy,
@NonNull final Violation violation
) {
final String fragmentName = fragment.getClass().getName();
if (policy.mFlags.contains(Flag.PENALTY_LOG)) {
Log.d(TAG, "Policy violation in " + fragmentName, violation);
}
if (policy.mListener != null) {
runOnHostThread(fragment, new Runnable() {
@Override
public void run() {
policy.mListener.onViolation(violation);
}
});
}
if (policy.mFlags.contains(Flag.PENALTY_DEATH)) {
runOnHostThread(fragment, new Runnable() {
@Override
public void run() {
Log.e(TAG, "Policy violation with PENALTY_DEATH in " + fragmentName, violation);
throw violation;
}
});
}
}
private static void runOnHostThread(@NonNull Fragment fragment, @NonNull Runnable runnable) {
if (fragment.isAdded()) {
Handler handler = fragment.getParentFragmentManager().getHost().getHandler();
if (handler.getLooper() == 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
}
}
}