/*
* 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.Collections;
import java.util.HashSet;
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> flags;
private final OnViolationListener listener;
/** The default, lax policy which doesn't catch anything. */
@NonNull
public static final Policy LAX = new Policy(new HashSet<Flag>(), null);
private Policy(@NonNull Set<Flag> flags, @Nullable OnViolationListener listener) {
this.flags = Collections.unmodifiableSet(flags);
this.listener = listener;
}
/**
* 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> flags;
private OnViolationListener listener;
/** Create a Builder that detects nothing and has no violations. */
public Builder() {
flags = new HashSet<>();
}
/** Log detected violations to the system log. */
@NonNull
@SuppressLint("BuilderSetStyle")
public Builder penaltyLog() {
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.
*/
@NonNull
@SuppressLint("BuilderSetStyle")
public Builder penaltyDeath() {
flags.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.listener = 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() {
flags.add(Flag.DETECT_FRAGMENT_REUSE);
return this;
}
/** Detects usage of the <fragment> tag inside XML layouts. */
@NonNull
@SuppressLint("BuilderSetStyle")
public Builder detectFragmentTagUsage() {
flags.add(Flag.DETECT_FRAGMENT_TAG_USAGE);
return this;
}
/**
* Detects calls to #{@link Fragment#setRetainInstance} and
* #{@link Fragment#getRetainInstance()}.
*/
@NonNull
@SuppressLint("BuilderSetStyle")
public Builder detectRetainInstanceUsage() {
flags.add(Flag.DETECT_RETAIN_INSTANCE_USAGE);
return this;
}
/** Detects calls to #{@link Fragment#setUserVisibleHint}. */
@NonNull
@SuppressLint("BuilderSetStyle")
public Builder detectSetUserVisibleHint() {
flags.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() {
flags.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() {
flags.add(Flag.DETECT_WRONG_FRAGMENT_CONTAINER);
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 (listener == null && !flags.contains(Flag.PENALTY_DEATH)) {
penaltyLog();
}
return new Policy(flags, listener);
}
}
}
/** 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) {
Policy policy = getNearestPolicy(fragment);
if (policy.flags.contains(Flag.DETECT_FRAGMENT_REUSE)) {
handlePolicyViolation(fragment, policy, new FragmentReuseViolation());
}
}
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static void onFragmentTagUsage(@NonNull Fragment fragment) {
Policy policy = getNearestPolicy(fragment);
if (policy.flags.contains(Flag.DETECT_FRAGMENT_TAG_USAGE)) {
handlePolicyViolation(fragment, policy, new FragmentTagUsageViolation());
}
}
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static void onRetainInstanceUsage(@NonNull Fragment fragment) {
Policy policy = getNearestPolicy(fragment);
if (policy.flags.contains(Flag.DETECT_RETAIN_INSTANCE_USAGE)) {
handlePolicyViolation(fragment, policy, new RetainInstanceUsageViolation());
}
}
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static void onSetUserVisibleHint(@NonNull Fragment fragment) {
Policy policy = getNearestPolicy(fragment);
if (policy.flags.contains(Flag.DETECT_SET_USER_VISIBLE_HINT)) {
handlePolicyViolation(fragment, policy, new SetUserVisibleHintViolation());
}
}
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static void onTargetFragmentUsage(@NonNull Fragment fragment) {
Policy policy = getNearestPolicy(fragment);
if (policy.flags.contains(Flag.DETECT_TARGET_FRAGMENT_USAGE)) {
handlePolicyViolation(fragment, policy, new TargetFragmentUsageViolation());
}
}
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static void onWrongFragmentContainer(@NonNull Fragment fragment) {
Policy policy = getNearestPolicy(fragment);
if (policy.flags.contains(Flag.DETECT_WRONG_FRAGMENT_CONTAINER)) {
handlePolicyViolation(fragment, policy, new WrongFragmentContainerViolation());
}
}
@VisibleForTesting
static void onPolicyViolation(@NonNull Fragment fragment, @NonNull Violation violation) {
Policy policy = getNearestPolicy(fragment);
handlePolicyViolation(fragment, policy, violation);
}
private static void handlePolicyViolation(
@NonNull Fragment fragment,
@NonNull final Policy policy,
@NonNull final Violation violation
) {
final String fragmentName = fragment.getClass().getName();
if (policy.flags.contains(Flag.PENALTY_LOG)) {
Log.d(TAG, "Policy violation in " + fragmentName, violation);
}
if (policy.listener != null) {
runOnHostThread(fragment, new Runnable() {
@Override
public void run() {
policy.listener.onViolation(violation);
}
});
}
if (policy.flags.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
}
}
}