Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 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.fragment.app.strictmode; |
| 18 | |
| 19 | import android.annotation.SuppressLint; |
Simon Schiller | 39b483d | 2021-03-02 17:08:35 +0000 | [diff] [blame] | 20 | import android.os.Handler; |
| 21 | import android.os.Looper; |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 22 | import android.util.Log; |
| 23 | |
| 24 | import androidx.annotation.NonNull; |
| 25 | import androidx.annotation.Nullable; |
| 26 | import androidx.annotation.RestrictTo; |
| 27 | import androidx.annotation.VisibleForTesting; |
| 28 | import androidx.fragment.app.Fragment; |
Simon Schiller | df2d4fd | 2021-03-29 14:48:42 -0700 | [diff] [blame] | 29 | import androidx.fragment.app.FragmentContainerView; |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 30 | import androidx.fragment.app.FragmentManager; |
| 31 | |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 32 | import java.util.HashMap; |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 33 | import java.util.HashSet; |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 34 | import java.util.Map; |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 35 | import java.util.Set; |
| 36 | |
| 37 | /** |
| 38 | * FragmentStrictMode is a tool which detects things you might be doing by accident and brings |
| 39 | * them to your attention so you can fix them. Basically, it's a version of |
| 40 | * {@link android.os.StrictMode} specifically for fragment-related issues. |
| 41 | * |
| 42 | * <p>You can decide what should happen when a violation is detected. For example, using {@link |
| 43 | * Policy.Builder#penaltyLog} you can watch the output of <code>adb logcat</code> while you |
| 44 | * use your application to see the violations as they happen. |
| 45 | */ |
| 46 | @SuppressLint("SyntheticAccessor") |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 47 | public final class FragmentStrictMode { |
| 48 | private static final String TAG = "FragmentStrictMode"; |
| 49 | private static Policy defaultPolicy = Policy.LAX; |
| 50 | |
| 51 | private enum Flag { |
| 52 | PENALTY_LOG, |
Simon Schiller | cb3b96b | 2021-03-03 23:36:49 -0800 | [diff] [blame] | 53 | PENALTY_DEATH, |
| 54 | |
Simon Schiller | 9719885 | 2021-03-23 17:04:29 +0100 | [diff] [blame] | 55 | DETECT_FRAGMENT_REUSE, |
Simon Schiller | 57ca221 | 2021-03-22 16:09:30 -0700 | [diff] [blame] | 56 | DETECT_FRAGMENT_TAG_USAGE, |
Simon Schiller | 909ebda | 2021-03-16 12:52:26 -0700 | [diff] [blame] | 57 | DETECT_RETAIN_INSTANCE_USAGE, |
Simon Schiller | f796782 | 2021-03-22 01:26:40 -0700 | [diff] [blame] | 58 | DETECT_SET_USER_VISIBLE_HINT, |
| 59 | DETECT_TARGET_FRAGMENT_USAGE, |
Simon Schiller | df2d4fd | 2021-03-29 14:48:42 -0700 | [diff] [blame] | 60 | DETECT_WRONG_FRAGMENT_CONTAINER, |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 61 | } |
| 62 | |
| 63 | private FragmentStrictMode() {} |
| 64 | |
| 65 | /** |
| 66 | * When #{@link Policy.Builder#penaltyListener} is enabled, the listener is called when a |
| 67 | * violation occurs. |
| 68 | */ |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 69 | public interface OnViolationListener { |
| 70 | |
Simon Schiller | 5e9ec57 | 2021-04-12 11:42:39 -0700 | [diff] [blame] | 71 | /** Called on a policy violation. */ |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 72 | void onViolation(@NonNull Violation violation); |
| 73 | } |
| 74 | |
| 75 | /** |
| 76 | * {@link FragmentStrictMode} policy applied to a certain {@link FragmentManager} (or globally). |
| 77 | * |
| 78 | * <p>This policy can either be enabled globally using {@link #setDefaultPolicy} or for a |
| 79 | * specific {@link FragmentManager} using {@link FragmentManager#setStrictModePolicy(Policy)}. |
| 80 | * The current policy can be retrieved using {@link #getDefaultPolicy} and |
| 81 | * {@link FragmentManager#getStrictModePolicy} respectively. |
| 82 | * |
| 83 | * <p>Note that multiple penalties may be provided and they're run in order from least to most |
| 84 | * severe (logging before process death, for example). There's currently no mechanism to choose |
| 85 | * different penalties for different detected actions. |
| 86 | */ |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 87 | public static final class Policy { |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 88 | private final Set<Flag> mFlags; |
| 89 | private final OnViolationListener mListener; |
| 90 | private final Map<Class<? extends Fragment>, |
| 91 | Set<Class<? extends Violation>>> mAllowedViolations; |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 92 | |
| 93 | /** The default, lax policy which doesn't catch anything. */ |
| 94 | @NonNull |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 95 | public static final Policy LAX = new Policy(new HashSet<>(), null, new HashMap<>()); |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 96 | |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 97 | private Policy( |
| 98 | @NonNull Set<Flag> flags, |
| 99 | @Nullable OnViolationListener listener, |
| 100 | @NonNull Map<Class<? extends Fragment>, |
| 101 | Set<Class<? extends Violation>>> allowedViolations) { |
| 102 | this.mFlags = new HashSet<>(flags); |
| 103 | this.mListener = listener; |
| 104 | |
| 105 | Map<Class<? extends Fragment>, Set<Class<? extends Violation>>> |
| 106 | newAllowedViolationsMap = new HashMap<>(); |
| 107 | for (Map.Entry<Class<? extends Fragment>, |
| 108 | Set<Class<? extends Violation>>> entry : allowedViolations.entrySet()) { |
| 109 | newAllowedViolationsMap.put(entry.getKey(), new HashSet<>(entry.getValue())); |
| 110 | } |
| 111 | this.mAllowedViolations = newAllowedViolationsMap; |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 112 | } |
| 113 | |
| 114 | /** |
| 115 | * Creates {@link Policy} instances. Methods whose names start with {@code detect} specify |
| 116 | * what problems we should look for. Methods whose names start with {@code penalty} specify |
| 117 | * what we should do when we detect a problem. |
| 118 | * |
| 119 | * <p>You can call as many {@code detect} and {@code penalty} methods as you like. Currently |
| 120 | * order is insignificant: all penalties apply to all detected problems. |
| 121 | */ |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 122 | public static final class Builder { |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 123 | private final Set<Flag> mFlags; |
| 124 | private OnViolationListener mListener; |
| 125 | private final Map<Class<? extends Fragment>, |
| 126 | Set<Class<? extends Violation>>> mAllowedViolations; |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 127 | |
| 128 | /** Create a Builder that detects nothing and has no violations. */ |
| 129 | public Builder() { |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 130 | mFlags = new HashSet<>(); |
| 131 | mAllowedViolations = new HashMap<>(); |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 132 | } |
| 133 | |
| 134 | /** Log detected violations to the system log. */ |
| 135 | @NonNull |
| 136 | @SuppressLint("BuilderSetStyle") |
| 137 | public Builder penaltyLog() { |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 138 | mFlags.add(Flag.PENALTY_LOG); |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 139 | return this; |
| 140 | } |
| 141 | |
| 142 | /** |
| 143 | * Throws an exception on violation. This penalty runs at the end of all enabled |
| 144 | * penalties so you'll still get to see logging or other violations before the exception |
| 145 | * is thrown. |
| 146 | */ |
| 147 | @NonNull |
| 148 | @SuppressLint("BuilderSetStyle") |
| 149 | public Builder penaltyDeath() { |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 150 | mFlags.add(Flag.PENALTY_DEATH); |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 151 | return this; |
| 152 | } |
| 153 | |
| 154 | /** |
| 155 | * Call #{@link OnViolationListener#onViolation} for every violation. The listener will |
Simon Schiller | 39b483d | 2021-03-02 17:08:35 +0000 | [diff] [blame] | 156 | * be called on the main thread of the fragment host. |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 157 | */ |
| 158 | @NonNull |
| 159 | @SuppressLint("BuilderSetStyle") |
| 160 | public Builder penaltyListener(@NonNull OnViolationListener listener) { |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 161 | this.mListener = listener; |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 162 | return this; |
| 163 | } |
| 164 | |
Simon Schiller | 9719885 | 2021-03-23 17:04:29 +0100 | [diff] [blame] | 165 | /** |
| 166 | * Detects cases, where a #{@link Fragment} instance is reused, after it was previously |
| 167 | * removed from a #{@link FragmentManager}. |
| 168 | */ |
| 169 | @NonNull |
| 170 | @SuppressLint("BuilderSetStyle") |
| 171 | public Builder detectFragmentReuse() { |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 172 | mFlags.add(Flag.DETECT_FRAGMENT_REUSE); |
Simon Schiller | 9719885 | 2021-03-23 17:04:29 +0100 | [diff] [blame] | 173 | return this; |
| 174 | } |
| 175 | |
Simon Schiller | 57ca221 | 2021-03-22 16:09:30 -0700 | [diff] [blame] | 176 | /** Detects usage of the <fragment> tag inside XML layouts. */ |
| 177 | @NonNull |
| 178 | @SuppressLint("BuilderSetStyle") |
| 179 | public Builder detectFragmentTagUsage() { |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 180 | mFlags.add(Flag.DETECT_FRAGMENT_TAG_USAGE); |
Simon Schiller | 57ca221 | 2021-03-22 16:09:30 -0700 | [diff] [blame] | 181 | return this; |
| 182 | } |
| 183 | |
Simon Schiller | 909ebda | 2021-03-16 12:52:26 -0700 | [diff] [blame] | 184 | /** |
| 185 | * Detects calls to #{@link Fragment#setRetainInstance} and |
| 186 | * #{@link Fragment#getRetainInstance()}. |
| 187 | */ |
| 188 | @NonNull |
| 189 | @SuppressLint("BuilderSetStyle") |
| 190 | public Builder detectRetainInstanceUsage() { |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 191 | mFlags.add(Flag.DETECT_RETAIN_INSTANCE_USAGE); |
Simon Schiller | 909ebda | 2021-03-16 12:52:26 -0700 | [diff] [blame] | 192 | return this; |
| 193 | } |
| 194 | |
Simon Schiller | cb3b96b | 2021-03-03 23:36:49 -0800 | [diff] [blame] | 195 | /** Detects calls to #{@link Fragment#setUserVisibleHint}. */ |
| 196 | @NonNull |
| 197 | @SuppressLint("BuilderSetStyle") |
| 198 | public Builder detectSetUserVisibleHint() { |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 199 | mFlags.add(Flag.DETECT_SET_USER_VISIBLE_HINT); |
Simon Schiller | cb3b96b | 2021-03-03 23:36:49 -0800 | [diff] [blame] | 200 | return this; |
| 201 | } |
| 202 | |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 203 | /** |
Simon Schiller | f796782 | 2021-03-22 01:26:40 -0700 | [diff] [blame] | 204 | * Detects calls to #{@link Fragment#setTargetFragment}, |
| 205 | * #{@link Fragment#getTargetFragment()} and #{@link Fragment#getTargetRequestCode()}. |
| 206 | */ |
| 207 | @NonNull |
| 208 | @SuppressLint("BuilderSetStyle") |
| 209 | public Builder detectTargetFragmentUsage() { |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 210 | mFlags.add(Flag.DETECT_TARGET_FRAGMENT_USAGE); |
Simon Schiller | f796782 | 2021-03-22 01:26:40 -0700 | [diff] [blame] | 211 | return this; |
| 212 | } |
| 213 | |
| 214 | /** |
Simon Schiller | df2d4fd | 2021-03-29 14:48:42 -0700 | [diff] [blame] | 215 | * Detects cases where a #{@link Fragment} is added to a container other than a |
| 216 | * #{@link FragmentContainerView}. |
| 217 | */ |
| 218 | @NonNull |
| 219 | @SuppressLint("BuilderSetStyle") |
| 220 | public Builder detectWrongFragmentContainer() { |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 221 | mFlags.add(Flag.DETECT_WRONG_FRAGMENT_CONTAINER); |
| 222 | return this; |
| 223 | } |
| 224 | |
| 225 | /** |
| 226 | * Allow the specified {@link Fragment} class to bypass penalties for the |
| 227 | * specified {@link Violation}, if detected. |
| 228 | * |
| 229 | * By default, all {@link Fragment} classes will incur penalties for any |
| 230 | * detected {@link Violation}. |
| 231 | */ |
| 232 | @NonNull |
| 233 | @SuppressLint("BuilderSetStyle") |
| 234 | public Builder allowViolation( |
| 235 | @NonNull Class<? extends Fragment> fragmentClass, |
| 236 | @NonNull Class<? extends Violation> violationClass) { |
| 237 | Set<Class<? extends Violation>> violationsToBypass = |
| 238 | mAllowedViolations.get(fragmentClass); |
| 239 | if (violationsToBypass == null) { |
| 240 | violationsToBypass = new HashSet<>(); |
| 241 | } |
| 242 | violationsToBypass.add(violationClass); |
| 243 | mAllowedViolations.put(fragmentClass, violationsToBypass); |
Simon Schiller | df2d4fd | 2021-03-29 14:48:42 -0700 | [diff] [blame] | 244 | return this; |
| 245 | } |
| 246 | |
| 247 | /** |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 248 | * Construct the Policy instance. |
| 249 | * |
| 250 | * <p>Note: if no penalties are enabled before calling <code>build</code>, {@link |
| 251 | * #penaltyLog} is implicitly set. |
| 252 | */ |
| 253 | @NonNull |
| 254 | public Policy build() { |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 255 | if (mListener == null && !mFlags.contains(Flag.PENALTY_DEATH)) { |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 256 | penaltyLog(); |
| 257 | } |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 258 | return new Policy(mFlags, mListener, mAllowedViolations); |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 259 | } |
| 260 | } |
| 261 | } |
| 262 | |
| 263 | /** Returns the current default policy. */ |
| 264 | @NonNull |
| 265 | public static Policy getDefaultPolicy() { |
| 266 | return defaultPolicy; |
| 267 | } |
| 268 | |
| 269 | /** |
| 270 | * Sets the policy for what actions should be detected, as well as the penalty if such actions |
| 271 | * occur. |
| 272 | * |
| 273 | * @param policy the policy to put into place |
| 274 | */ |
| 275 | public static void setDefaultPolicy(@NonNull Policy policy) { |
| 276 | defaultPolicy = policy; |
| 277 | } |
| 278 | |
| 279 | private static Policy getNearestPolicy(@Nullable Fragment fragment) { |
| 280 | while (fragment != null) { |
| 281 | if (fragment.isAdded()) { |
| 282 | FragmentManager fragmentManager = fragment.getParentFragmentManager(); |
| 283 | if (fragmentManager.getStrictModePolicy() != null) { |
| 284 | return fragmentManager.getStrictModePolicy(); |
| 285 | } |
| 286 | } |
| 287 | fragment = fragment.getParentFragment(); |
| 288 | } |
| 289 | return defaultPolicy; |
| 290 | } |
| 291 | |
Simon Schiller | cb3b96b | 2021-03-03 23:36:49 -0800 | [diff] [blame] | 292 | @RestrictTo(RestrictTo.Scope.LIBRARY) |
Simon Schiller | 9719885 | 2021-03-23 17:04:29 +0100 | [diff] [blame] | 293 | public static void onFragmentReuse(@NonNull Fragment fragment) { |
Sanura N'Jaka | 74dde5c | 2021-05-13 16:10:55 +0000 | [diff] [blame] | 294 | Violation violation = new FragmentReuseViolation(); |
| 295 | logIfDebuggingEnabled(fragment.getClass().getName(), violation); |
| 296 | |
Simon Schiller | 9719885 | 2021-03-23 17:04:29 +0100 | [diff] [blame] | 297 | Policy policy = getNearestPolicy(fragment); |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 298 | if (policy.mFlags.contains(Flag.DETECT_FRAGMENT_REUSE) |
| 299 | && shouldHandlePolicyViolation( |
| 300 | fragment.getClass(), policy, violation.getClass())) { |
Sanura N'Jaka | 74dde5c | 2021-05-13 16:10:55 +0000 | [diff] [blame] | 301 | handlePolicyViolation(fragment, policy, violation); |
Simon Schiller | 9719885 | 2021-03-23 17:04:29 +0100 | [diff] [blame] | 302 | } |
| 303 | } |
| 304 | |
| 305 | @RestrictTo(RestrictTo.Scope.LIBRARY) |
Simon Schiller | 57ca221 | 2021-03-22 16:09:30 -0700 | [diff] [blame] | 306 | public static void onFragmentTagUsage(@NonNull Fragment fragment) { |
Sanura N'Jaka | 74dde5c | 2021-05-13 16:10:55 +0000 | [diff] [blame] | 307 | Violation violation = new FragmentTagUsageViolation(); |
| 308 | logIfDebuggingEnabled(fragment.getClass().getName(), violation); |
| 309 | |
Simon Schiller | 57ca221 | 2021-03-22 16:09:30 -0700 | [diff] [blame] | 310 | Policy policy = getNearestPolicy(fragment); |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 311 | if (policy.mFlags.contains(Flag.DETECT_FRAGMENT_TAG_USAGE) |
| 312 | && shouldHandlePolicyViolation( |
| 313 | fragment.getClass(), policy, violation.getClass())) { |
Sanura N'Jaka | 74dde5c | 2021-05-13 16:10:55 +0000 | [diff] [blame] | 314 | handlePolicyViolation(fragment, policy, violation); |
Simon Schiller | 57ca221 | 2021-03-22 16:09:30 -0700 | [diff] [blame] | 315 | } |
| 316 | } |
| 317 | |
| 318 | @RestrictTo(RestrictTo.Scope.LIBRARY) |
Simon Schiller | 909ebda | 2021-03-16 12:52:26 -0700 | [diff] [blame] | 319 | public static void onRetainInstanceUsage(@NonNull Fragment fragment) { |
Sanura N'Jaka | 74dde5c | 2021-05-13 16:10:55 +0000 | [diff] [blame] | 320 | Violation violation = new RetainInstanceUsageViolation(); |
| 321 | logIfDebuggingEnabled(fragment.getClass().getName(), violation); |
| 322 | |
Simon Schiller | 909ebda | 2021-03-16 12:52:26 -0700 | [diff] [blame] | 323 | Policy policy = getNearestPolicy(fragment); |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 324 | if (policy.mFlags.contains(Flag.DETECT_RETAIN_INSTANCE_USAGE) |
| 325 | && shouldHandlePolicyViolation( |
| 326 | fragment.getClass(), policy, violation.getClass())) { |
Sanura N'Jaka | 74dde5c | 2021-05-13 16:10:55 +0000 | [diff] [blame] | 327 | handlePolicyViolation(fragment, policy, violation); |
Simon Schiller | 909ebda | 2021-03-16 12:52:26 -0700 | [diff] [blame] | 328 | } |
| 329 | } |
| 330 | |
| 331 | @RestrictTo(RestrictTo.Scope.LIBRARY) |
Simon Schiller | cb3b96b | 2021-03-03 23:36:49 -0800 | [diff] [blame] | 332 | public static void onSetUserVisibleHint(@NonNull Fragment fragment) { |
Sanura N'Jaka | 74dde5c | 2021-05-13 16:10:55 +0000 | [diff] [blame] | 333 | Violation violation = new SetUserVisibleHintViolation(); |
| 334 | logIfDebuggingEnabled(fragment.getClass().getName(), violation); |
| 335 | |
Simon Schiller | cb3b96b | 2021-03-03 23:36:49 -0800 | [diff] [blame] | 336 | Policy policy = getNearestPolicy(fragment); |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 337 | if (policy.mFlags.contains(Flag.DETECT_SET_USER_VISIBLE_HINT) |
| 338 | && shouldHandlePolicyViolation( |
| 339 | fragment.getClass(), policy, violation.getClass())) { |
Sanura N'Jaka | 74dde5c | 2021-05-13 16:10:55 +0000 | [diff] [blame] | 340 | handlePolicyViolation(fragment, policy, violation); |
Simon Schiller | cb3b96b | 2021-03-03 23:36:49 -0800 | [diff] [blame] | 341 | } |
| 342 | } |
| 343 | |
Simon Schiller | f796782 | 2021-03-22 01:26:40 -0700 | [diff] [blame] | 344 | @RestrictTo(RestrictTo.Scope.LIBRARY) |
| 345 | public static void onTargetFragmentUsage(@NonNull Fragment fragment) { |
Sanura N'Jaka | 74dde5c | 2021-05-13 16:10:55 +0000 | [diff] [blame] | 346 | Violation violation = new TargetFragmentUsageViolation(); |
| 347 | logIfDebuggingEnabled(fragment.getClass().getName(), violation); |
| 348 | |
Simon Schiller | f796782 | 2021-03-22 01:26:40 -0700 | [diff] [blame] | 349 | Policy policy = getNearestPolicy(fragment); |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 350 | if (policy.mFlags.contains(Flag.DETECT_TARGET_FRAGMENT_USAGE) |
| 351 | && shouldHandlePolicyViolation( |
| 352 | fragment.getClass(), policy, violation.getClass())) { |
Sanura N'Jaka | 74dde5c | 2021-05-13 16:10:55 +0000 | [diff] [blame] | 353 | handlePolicyViolation(fragment, policy, violation); |
Simon Schiller | f796782 | 2021-03-22 01:26:40 -0700 | [diff] [blame] | 354 | } |
| 355 | } |
| 356 | |
Simon Schiller | df2d4fd | 2021-03-29 14:48:42 -0700 | [diff] [blame] | 357 | @RestrictTo(RestrictTo.Scope.LIBRARY) |
| 358 | public static void onWrongFragmentContainer(@NonNull Fragment fragment) { |
Sanura N'Jaka | 74dde5c | 2021-05-13 16:10:55 +0000 | [diff] [blame] | 359 | Violation violation = new WrongFragmentContainerViolation(); |
| 360 | logIfDebuggingEnabled(fragment.getClass().getName(), violation); |
| 361 | |
Simon Schiller | df2d4fd | 2021-03-29 14:48:42 -0700 | [diff] [blame] | 362 | Policy policy = getNearestPolicy(fragment); |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 363 | if (policy.mFlags.contains(Flag.DETECT_WRONG_FRAGMENT_CONTAINER) |
| 364 | && shouldHandlePolicyViolation( |
| 365 | fragment.getClass(), policy, violation.getClass())) { |
Sanura N'Jaka | 74dde5c | 2021-05-13 16:10:55 +0000 | [diff] [blame] | 366 | handlePolicyViolation(fragment, policy, violation); |
Simon Schiller | df2d4fd | 2021-03-29 14:48:42 -0700 | [diff] [blame] | 367 | } |
| 368 | } |
| 369 | |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 370 | @VisibleForTesting |
Simon Schiller | cb3b96b | 2021-03-03 23:36:49 -0800 | [diff] [blame] | 371 | static void onPolicyViolation(@NonNull Fragment fragment, @NonNull Violation violation) { |
Sanura N'Jaka | 74dde5c | 2021-05-13 16:10:55 +0000 | [diff] [blame] | 372 | logIfDebuggingEnabled(fragment.getClass().getName(), violation); |
| 373 | |
Simon Schiller | cb3b96b | 2021-03-03 23:36:49 -0800 | [diff] [blame] | 374 | Policy policy = getNearestPolicy(fragment); |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 375 | if (shouldHandlePolicyViolation(fragment.getClass(), policy, violation.getClass())) { |
| 376 | handlePolicyViolation(fragment, policy, violation); |
| 377 | } |
Simon Schiller | cb3b96b | 2021-03-03 23:36:49 -0800 | [diff] [blame] | 378 | } |
| 379 | |
Sanura N'Jaka | 74dde5c | 2021-05-13 16:10:55 +0000 | [diff] [blame] | 380 | private static void logIfDebuggingEnabled( |
| 381 | @NonNull String fragmentName, |
| 382 | @NonNull final Violation violation |
| 383 | ) { |
| 384 | if (FragmentManager.isLoggingEnabled(Log.DEBUG)) { |
| 385 | Log.d(FragmentManager.TAG, "StrictMode violation in " + fragmentName, |
| 386 | violation); |
| 387 | } |
| 388 | } |
| 389 | |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 390 | private static boolean shouldHandlePolicyViolation( |
| 391 | @NonNull Class<? extends Fragment> fragmentClass, |
| 392 | @NonNull final Policy policy, |
| 393 | @NonNull Class<? extends Violation> violationClass) { |
| 394 | Set<Class<? extends Violation>> violationsToBypass = |
| 395 | policy.mAllowedViolations.get(fragmentClass); |
| 396 | return violationsToBypass == null || !violationsToBypass.contains(violationClass); |
| 397 | } |
| 398 | |
Simon Schiller | cb3b96b | 2021-03-03 23:36:49 -0800 | [diff] [blame] | 399 | private static void handlePolicyViolation( |
| 400 | @NonNull Fragment fragment, |
| 401 | @NonNull final Policy policy, |
| 402 | @NonNull final Violation violation |
| 403 | ) { |
Simon Schiller | 39b483d | 2021-03-02 17:08:35 +0000 | [diff] [blame] | 404 | final String fragmentName = fragment.getClass().getName(); |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 405 | |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 406 | if (policy.mFlags.contains(Flag.PENALTY_LOG)) { |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 407 | Log.d(TAG, "Policy violation in " + fragmentName, violation); |
| 408 | } |
| 409 | |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 410 | if (policy.mListener != null) { |
Simon Schiller | 39b483d | 2021-03-02 17:08:35 +0000 | [diff] [blame] | 411 | runOnHostThread(fragment, new Runnable() { |
| 412 | @Override |
| 413 | public void run() { |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 414 | policy.mListener.onViolation(violation); |
Simon Schiller | 39b483d | 2021-03-02 17:08:35 +0000 | [diff] [blame] | 415 | } |
| 416 | }); |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 417 | } |
| 418 | |
Sanura N'Jaka | 1c68382 | 2021-05-13 01:31:39 +0000 | [diff] [blame^] | 419 | if (policy.mFlags.contains(Flag.PENALTY_DEATH)) { |
Simon Schiller | 39b483d | 2021-03-02 17:08:35 +0000 | [diff] [blame] | 420 | runOnHostThread(fragment, new Runnable() { |
| 421 | @Override |
| 422 | public void run() { |
| 423 | Log.e(TAG, "Policy violation with PENALTY_DEATH in " + fragmentName, violation); |
| 424 | throw violation; |
| 425 | } |
| 426 | }); |
| 427 | } |
| 428 | } |
| 429 | |
| 430 | private static void runOnHostThread(@NonNull Fragment fragment, @NonNull Runnable runnable) { |
| 431 | if (fragment.isAdded()) { |
| 432 | Handler handler = fragment.getParentFragmentManager().getHost().getHandler(); |
| 433 | if (handler.getLooper() == Looper.myLooper()) { |
| 434 | runnable.run(); // Already on correct thread -> run synchronously |
| 435 | } else { |
| 436 | handler.post(runnable); // Switch to correct thread |
| 437 | } |
| 438 | } else { |
| 439 | runnable.run(); // Fragment is not attached to any host -> run synchronously |
Simon Schiller | e154ee0 | 2021-02-16 10:29:29 -0800 | [diff] [blame] | 440 | } |
| 441 | } |
| 442 | } |