Add integration tests for lint checks, unify under superclass

Updates comments and messages within BanUncheckedReflection for clarity.

Ignores flaky test in BanInappropriateExperimentalUsageTest and adds DEBUG
blocks in the detector for debugging lint flakiness.

Bug: 181340206
Bug: 188048904
Test: BanUncheckedReflectionTest et al
Change-Id: Id85120377dac112ce9b547e06f2862ba95feb142
diff --git a/lint-checks/integration-tests/expected-lint-results.xml b/lint-checks/integration-tests/expected-lint-results.xml
index 6328019..ce6e261 100644
--- a/lint-checks/integration-tests/expected-lint-results.xml
+++ b/lint-checks/integration-tests/expected-lint-results.xml
@@ -148,7 +148,7 @@
     <issue
         id="ClassVerificationFailure"
         severity="Error"
-        message="This call references a method added in API level 19; however, the containing class androidx.core.widget.ListViewCompat is reachable from earlier API levels and will fail run-time class verification."
+        message="This call references a method added in API level 19; however, the containing class androidx.sample.core.widget.ListViewCompat is reachable from earlier API levels and will fail run-time class verification."
         category="Correctness"
         priority="5"
         summary="Even in cases where references to new APIs are gated on SDK_INT checks, run-time class verification will still fail on references to APIs that may not be available at run time, including platform APIs introduced after a library&apos;s minSdkVersion."
@@ -164,7 +164,7 @@
     <issue
         id="ClassVerificationFailure"
         severity="Error"
-        message="This call references a method added in API level 19; however, the containing class androidx.core.widget.ListViewCompat is reachable from earlier API levels and will fail run-time class verification."
+        message="This call references a method added in API level 19; however, the containing class androidx.sample.core.widget.ListViewCompat is reachable from earlier API levels and will fail run-time class verification."
         category="Correctness"
         priority="5"
         summary="Even in cases where references to new APIs are gated on SDK_INT checks, run-time class verification will still fail on references to APIs that may not be available at run time, including platform APIs introduced after a library&apos;s minSdkVersion."
diff --git a/lint-checks/integration-tests/src/main/AndroidManifest.xml b/lint-checks/integration-tests/src/main/AndroidManifest.xml
index 1a39913..98ea13f 100644
--- a/lint-checks/integration-tests/src/main/AndroidManifest.xml
+++ b/lint-checks/integration-tests/src/main/AndroidManifest.xml
@@ -13,4 +13,15 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<manifest package="androidx.lint.integration.tests" />
+<manifest package="androidx.lint.integration.tests"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+    <application>
+        <!-- Application-level metadata is not allowed. -->
+        <meta-data android:name="name" android:value="value" />
+
+        <service android:name="androidx.core.app.JobIntentService">
+            <!-- Service-level metadata is allowed. -->
+            <meta-data android:name="name" android:value="value" />
+        </service>
+    </application>
+</manifest>
diff --git a/lint-checks/integration-tests/src/main/java/androidx/ConcurrentHashMapUsageJava.java b/lint-checks/integration-tests/src/main/java/androidx/ConcurrentHashMapUsageJava.java
new file mode 100644
index 0000000..db30af2
--- /dev/null
+++ b/lint-checks/integration-tests/src/main/java/androidx/ConcurrentHashMapUsageJava.java
@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+import androidx.annotation.NonNull;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@SuppressWarnings("unused")
+public class ConcurrentHashMapUsageJava {
+
+    private final ConcurrentHashMap<?, ?> mMap = new ConcurrentHashMap<>();
+
+    @NonNull
+    public <V, K> Map<V, K> createMap() {
+        return new ConcurrentHashMap<>();
+    }
+}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/KeepAnnotationUsageJava.java b/lint-checks/integration-tests/src/main/java/androidx/KeepAnnotationUsageJava.java
new file mode 100644
index 0000000..964459f
--- /dev/null
+++ b/lint-checks/integration-tests/src/main/java/androidx/KeepAnnotationUsageJava.java
@@ -0,0 +1,23 @@
+/*
+ * 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;
+
+import androidx.annotation.Keep;
+
+@Keep
+public class KeepAnnotationUsageJava {
+}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/ParcelableUsageJava.java b/lint-checks/integration-tests/src/main/java/androidx/ParcelableUsageJava.java
new file mode 100644
index 0000000..04aafe1
--- /dev/null
+++ b/lint-checks/integration-tests/src/main/java/androidx/ParcelableUsageJava.java
@@ -0,0 +1,50 @@
+/*
+ * 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;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+@SuppressWarnings("unused")
+public class ParcelableUsageJava implements Parcelable {
+
+    protected ParcelableUsageJava(@NonNull Parcel in) {
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+    }
+
+    public static final Creator<ParcelableUsageJava> CREATOR = new Creator<ParcelableUsageJava>() {
+        @Override
+        public ParcelableUsageJava createFromParcel(Parcel in) {
+            return new ParcelableUsageJava(in);
+        }
+
+        @Override
+        public ParcelableUsageJava[] newArray(int size) {
+            return new ParcelableUsageJava[size];
+        }
+    };
+}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/SynchronizedMethodJava.java b/lint-checks/integration-tests/src/main/java/androidx/SynchronizedMethodJava.java
new file mode 100644
index 0000000..73e6365
--- /dev/null
+++ b/lint-checks/integration-tests/src/main/java/androidx/SynchronizedMethodJava.java
@@ -0,0 +1,24 @@
+/*
+ * 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;
+
+@SuppressWarnings("unused")
+public class SynchronizedMethodJava {
+
+    public synchronized void someMethod() {
+    }
+}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/TargetApiUsageJava.java b/lint-checks/integration-tests/src/main/java/androidx/TargetApiUsageJava.java
new file mode 100644
index 0000000..ff0d71d
--- /dev/null
+++ b/lint-checks/integration-tests/src/main/java/androidx/TargetApiUsageJava.java
@@ -0,0 +1,28 @@
+/*
+ * 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;
+
+import android.annotation.TargetApi;
+
+@SuppressWarnings("unused")
+@TargetApi(29)
+public class TargetApiUsageJava {
+
+    @TargetApi(30)
+    public void someMethod() {
+    }
+}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/sample/core/app/ActivityRecreator.java b/lint-checks/integration-tests/src/main/java/androidx/sample/core/app/ActivityRecreator.java
new file mode 100644
index 0000000..8790171
--- /dev/null
+++ b/lint-checks/integration-tests/src/main/java/androidx/sample/core/app/ActivityRecreator.java
@@ -0,0 +1,370 @@
+/*
+ * 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.sample.core.app;
+
+import static android.os.Build.VERSION.SDK_INT;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.app.Activity;
+import android.app.Application;
+import android.app.Application.ActivityLifecycleCallbacks;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.List;
+
+/**
+ * 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:
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>The fix for this is to add the missing onStop() call, by using reflection to call into
+ * ActivityThread.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+@SuppressWarnings({"PrivateApi", "JavaReflectionMemberAccess", "unused"})
+final class ActivityRecreator {
+    private ActivityRecreator() {}
+
+    private static final String LOG_TAG = "ActivityRecreator";
+
+    // Activity.mMainThread
+    protected static final Field mainThreadField;
+    // Activity.mToken. This object is an identifier that is the same between multiple instances of
+    //the same underlying Activity.
+    protected static final Field tokenField;
+    // On API 25, a third param was added to performStopActivity
+    protected static final Method performStopActivity3ParamsMethod;
+    // Before API 25, performStopActivity had two params
+    protected static final Method performStopActivity2ParamsMethod;
+    // ActivityThread.requestRelaunchActivity
+    protected static final Method requestRelaunchActivityMethod;
+
+    private static final Handler mainHandler = new Handler(Looper.getMainLooper());
+
+    static {
+        Class<?> activityThreadClass = getActivityThreadClass();
+        mainThreadField = getMainThreadField();
+        tokenField = getTokenField();
+        performStopActivity3ParamsMethod = getPerformStopActivity3Params(activityThreadClass);
+        performStopActivity2ParamsMethod = getPerformStopActivity2Params(activityThreadClass);
+        requestRelaunchActivityMethod = getRequestRelaunchActivityMethod(activityThreadClass);
+    }
+
+    /**
+     * Equivalent to {@link Activity#recreate}, but working around a number of platform bugs.
+     *
+     * @return true if a recreate() task was successfully scheduled.
+     */
+    static boolean recreate(@NonNull final Activity activity) {
+        // On Android O and later we can rely on the platform recreate()
+        if (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;
+        }
+        try {
+            final Object token = tokenField.get(activity);
+            if (token == null) {
+                return false;
+            }
+            Object activityThread = mainThreadField.get(activity);
+            if (activityThread == null) {
+                return false;
+            }
+
+            final Application application = activity.getApplication();
+            final LifecycleCheckCallbacks callbacks = new 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();
+                }
+                return 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 (Throwable t) {
+            return false;
+        }
+    }
+
+    private static final class LifecycleCheckCallbacks implements ActivityLifecycleCallbacks {
+        Object currentlyRecreatingToken;
+
+        private Activity mActivity;
+        private final int mRecreatingHashCode;
+
+        // Whether the activity on which recreate() was called went through onStart after
+        // recreate() was called (and thus the callback was registered).
+        private boolean 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 boolean mDestroyed = false;
+
+        // Whether we'll force the activity on which recreate() was called to go through an
+        // onStop()
+        private boolean mStopQueued = false;
+
+        LifecycleCheckCallbacks(@NonNull Activity aboutToRecreate) {
+            mActivity = aboutToRecreate;
+            mRecreatingHashCode = mActivity.hashCode();
+        }
+
+        @Override
+        public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
+        }
+
+        @Override
+        public void 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
+        public void onActivityResumed(Activity activity) {
+        }
+
+        @Override
+        public void 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 recreate()
+                    && queueOnStopIfNecessary(
+                    currentlyRecreatingToken, mRecreatingHashCode, activity)) {
+                mStopQueued = true;
+                // Don't retain this object longer than necessary
+                currentlyRecreatingToken = null;
+            }
+        }
+
+        @Override
+        public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
+        }
+
+        @Override
+        public void onActivityStopped(Activity activity) {
+            // Not possible to get a start/stop pair in the same UI thread loop
+        }
+
+        @Override
+        public void 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;
+            }
+        }
+    }
+
+    /**
+     * Returns true if a stop call was scheduled successfully
+     */
+    protected static boolean queueOnStopIfNecessary(
+            Object currentlyRecreatingToken, int currentlyRecreatingHashCode, Activity activity) {
+        try {
+            final Object token = tokenField.get(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;
+            }
+            final Object activityThread = mainThreadField.get(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 (performStopActivity3ParamsMethod != null) {
+                        performStopActivity3ParamsMethod.invoke(activityThread,
+                                token, false, "AppCompat recreation");
+                    } else {
+                        performStopActivity2ParamsMethod.invoke(activityThread,
+                                token, false);
+                    }
+                } catch (RuntimeException e) {
+                    // If an Activity throws from onStop, don't swallow it
+                    if (e.getClass() == RuntimeException.class
+                            && e.getMessage() != null
+                            && e.getMessage().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 (Throwable t) {
+                    Log.e(LOG_TAG, "Exception while invoking performStopActivity", t);
+                }
+            });
+            return true;
+        } catch (Throwable t) {
+            Log.e(LOG_TAG, "Exception while fetching field values", t);
+            return false;
+        }
+    }
+
+    private static Method getPerformStopActivity3Params(Class<?> activityThreadClass) {
+        if (activityThreadClass == null) {
+            return null;
+        }
+        try {
+            Method performStop = activityThreadClass.getDeclaredMethod("performStopActivity",
+                    IBinder.class, boolean.class, String.class);
+            performStop.setAccessible(true);
+            return performStop;
+        } catch (Throwable t) {
+            return null;
+        }
+    }
+
+    private static Method getPerformStopActivity2Params(Class<?> activityThreadClass) {
+        if (activityThreadClass == null) {
+            return null;
+        }
+        try {
+            Method performStop = activityThreadClass.getDeclaredMethod("performStopActivity",
+                    IBinder.class, boolean.class);
+            performStop.setAccessible(true);
+            return performStop;
+        } catch (Throwable t) {
+            return null;
+        }
+    }
+
+    private static boolean needsRelaunchCall() {
+        return SDK_INT == 26 || SDK_INT == 27;
+    }
+
+    private static Method getRequestRelaunchActivityMethod(Class<?> activityThreadClass) {
+        if (!needsRelaunchCall() || activityThreadClass == null) {
+            return null;
+        }
+        try {
+            Method relaunch = activityThreadClass.getDeclaredMethod(
+                    "requestRelaunchActivity",
+                    IBinder.class,
+                    List.class,
+                    List.class,
+                    int.class,
+                    boolean.class,
+                    Configuration.class,
+                    Configuration.class,
+                    boolean.class,
+                    boolean.class);
+            relaunch.setAccessible(true);
+            return relaunch;
+        } catch (Throwable t) {
+            return null;
+        }
+    }
+
+    private static Field getMainThreadField() {
+        try {
+            Field mainThreadField = Activity.class.getDeclaredField("mMainThread");
+            mainThreadField.setAccessible(true);
+            return mainThreadField;
+        } catch (Throwable t) {
+            return null;
+        }
+    }
+
+    private static Field getTokenField() {
+        try {
+            Field tokenField = Activity.class.getDeclaredField("mToken");
+            tokenField.setAccessible(true);
+            return tokenField;
+        } catch (Throwable t) {
+            return null;
+        }
+    }
+
+    private static Class<?> getActivityThreadClass() {
+        try {
+            return Class.forName("android.app.ActivityThread");
+        } catch (Throwable t) {
+            return null;
+        }
+    }
+}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/sample/core/app/ActivityRecreatorChecked.java b/lint-checks/integration-tests/src/main/java/androidx/sample/core/app/ActivityRecreatorChecked.java
new file mode 100644
index 0000000..0523d2d
--- /dev/null
+++ b/lint-checks/integration-tests/src/main/java/androidx/sample/core/app/ActivityRecreatorChecked.java
@@ -0,0 +1,377 @@
+/*
+ * 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.sample.core.app;
+
+import static android.os.Build.VERSION.SDK_INT;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.app.Activity;
+import android.app.Application;
+import android.app.Application.ActivityLifecycleCallbacks;
+import android.content.res.Configuration;
+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.NonNull;
+import androidx.annotation.RestrictTo;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.List;
+
+/**
+ * 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:
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>The fix for this is to add the missing onStop() call, by using reflection to call into
+ * ActivityThread.
+ *
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+@SuppressWarnings({"PrivateApi", "JavaReflectionMemberAccess", "unused"})
+final class ActivityRecreatorChecked {
+    private ActivityRecreatorChecked() {}
+
+    private static final String LOG_TAG = "ActivityRecreatorChecked";
+
+    // Activity.mMainThread
+    protected static final Field mainThreadField;
+    // Activity.mToken. This object is an identifier that is the same between multiple instances of
+    //the same underlying Activity.
+    protected static final Field tokenField;
+    // On API 25, a third param was added to performStopActivity
+    protected static final Method performStopActivity3ParamsMethod;
+    // Before API 25, performStopActivity had two params
+    protected static final Method performStopActivity2ParamsMethod;
+    // ActivityThread.requestRelaunchActivity
+    protected static final Method requestRelaunchActivityMethod;
+
+    private static final Handler mainHandler = new Handler(Looper.getMainLooper());
+
+    static {
+        Class<?> activityThreadClass = getActivityThreadClass();
+        mainThreadField = getMainThreadField();
+        tokenField = getTokenField();
+        performStopActivity3ParamsMethod = getPerformStopActivity3Params(activityThreadClass);
+        performStopActivity2ParamsMethod = getPerformStopActivity2Params(activityThreadClass);
+        requestRelaunchActivityMethod = getRequestRelaunchActivityMethod(activityThreadClass);
+    }
+
+    /**
+     * Equivalent to {@link Activity#recreate}, but working around a number of platform bugs.
+     *
+     * @return true if a recreate() task was successfully scheduled.
+     */
+    static boolean recreate(@NonNull final Activity activity) {
+        // On Android O and later we can rely on the platform recreate()
+        if (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;
+        }
+        try {
+            final Object token = tokenField.get(activity);
+            if (token == null) {
+                return false;
+            }
+            Object activityThread = mainThreadField.get(activity);
+            if (activityThread == null) {
+                return false;
+            }
+
+            final Application application = activity.getApplication();
+            final LifecycleCheckCallbacks callbacks = new 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();
+                }
+                return 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 (Throwable t) {
+            return false;
+        }
+    }
+
+    // Only reachable on SDK_INT < 28
+    private static final class LifecycleCheckCallbacks implements ActivityLifecycleCallbacks {
+        Object currentlyRecreatingToken;
+
+        private Activity mActivity;
+        private final int mRecreatingHashCode;
+
+        // Whether the activity on which recreate() was called went through onStart after
+        // recreate() was called (and thus the callback was registered).
+        private boolean 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 boolean mDestroyed = false;
+
+        // Whether we'll force the activity on which recreate() was called to go through an
+        // onStop()
+        private boolean mStopQueued = false;
+
+        LifecycleCheckCallbacks(@NonNull Activity aboutToRecreate) {
+            mActivity = aboutToRecreate;
+            mRecreatingHashCode = mActivity.hashCode();
+        }
+
+        @Override
+        public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
+        }
+
+        @Override
+        public void 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
+        public void onActivityResumed(Activity activity) {
+        }
+
+        @Override
+        public void 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 recreate()
+                    && queueOnStopIfNecessary(
+                    currentlyRecreatingToken, mRecreatingHashCode, activity)) {
+                mStopQueued = true;
+                // Don't retain this object longer than necessary
+                currentlyRecreatingToken = null;
+            }
+        }
+
+        @Override
+        public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
+        }
+
+        @Override
+        public void onActivityStopped(Activity activity) {
+            // Not possible to get a start/stop pair in the same UI thread loop
+        }
+
+        @Override
+        public void 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;
+            }
+        }
+    }
+
+    /**
+     * Returns true if a stop call was scheduled successfully.
+     *
+     * Only reachable on SDK < 28.
+     */
+    protected static boolean queueOnStopIfNecessary(
+            Object currentlyRecreatingToken, int currentlyRecreatingHashCode, Activity activity) {
+        try {
+            final Object token = tokenField.get(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;
+            }
+            final Object activityThread = mainThreadField.get(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 (SDK_INT < 28) {
+                        if (performStopActivity3ParamsMethod != null) {
+                            performStopActivity3ParamsMethod.invoke(activityThread,
+                                    token, false, "AppCompat recreation");
+                        } else {
+                            performStopActivity2ParamsMethod.invoke(activityThread,
+                                    token, false);
+                        }
+                    }
+                } catch (RuntimeException e) {
+                    // If an Activity throws from onStop, don't swallow it
+                    if (e.getClass() == RuntimeException.class
+                            && e.getMessage() != null
+                            && e.getMessage().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 (Throwable t) {
+                    Log.e(LOG_TAG, "Exception while invoking performStopActivity", t);
+                }
+            });
+            return true;
+        } catch (Throwable t) {
+            Log.e(LOG_TAG, "Exception while fetching field values", t);
+            return false;
+        }
+    }
+
+    private static Method getPerformStopActivity3Params(Class<?> activityThreadClass) {
+        if (activityThreadClass == null) {
+            return null;
+        }
+        try {
+            Method performStop = activityThreadClass.getDeclaredMethod("performStopActivity",
+                    IBinder.class, boolean.class, String.class);
+            performStop.setAccessible(true);
+            return performStop;
+        } catch (Throwable t) {
+            return null;
+        }
+    }
+
+    private static Method getPerformStopActivity2Params(Class<?> activityThreadClass) {
+        if (activityThreadClass == null) {
+            return null;
+        }
+        try {
+            Method performStop = activityThreadClass.getDeclaredMethod("performStopActivity",
+                    IBinder.class, boolean.class);
+            performStop.setAccessible(true);
+            return performStop;
+        } catch (Throwable t) {
+            return null;
+        }
+    }
+
+    @ChecksSdkIntAtLeast(api = 26)
+    private static boolean needsRelaunchCall() {
+        return SDK_INT == 26 || SDK_INT == 27;
+    }
+
+    private static Method getRequestRelaunchActivityMethod(Class<?> activityThreadClass) {
+        if (!needsRelaunchCall() || activityThreadClass == null) {
+            return null;
+        }
+        try {
+            Method relaunch = activityThreadClass.getDeclaredMethod(
+                    "requestRelaunchActivity",
+                    IBinder.class,
+                    List.class,
+                    List.class,
+                    int.class,
+                    boolean.class,
+                    Configuration.class,
+                    Configuration.class,
+                    boolean.class,
+                    boolean.class);
+            relaunch.setAccessible(true);
+            return relaunch;
+        } catch (Throwable t) {
+            return null;
+        }
+    }
+
+    private static Field getMainThreadField() {
+        try {
+            Field mainThreadField = Activity.class.getDeclaredField("mMainThread");
+            mainThreadField.setAccessible(true);
+            return mainThreadField;
+        } catch (Throwable t) {
+            return null;
+        }
+    }
+
+    private static Field getTokenField() {
+        try {
+            Field tokenField = Activity.class.getDeclaredField("mToken");
+            tokenField.setAccessible(true);
+            return tokenField;
+        } catch (Throwable t) {
+            return null;
+        }
+    }
+
+    private static Class<?> getActivityThreadClass() {
+        try {
+            return Class.forName("android.app.ActivityThread");
+        } catch (Throwable t) {
+            return null;
+        }
+    }
+}
diff --git a/lint-checks/integration-tests/src/main/java/androidx/core/widget/ListViewCompat.java b/lint-checks/integration-tests/src/main/java/androidx/sample/core/widget/ListViewCompat.java
similarity index 97%
rename from lint-checks/integration-tests/src/main/java/androidx/core/widget/ListViewCompat.java
rename to lint-checks/integration-tests/src/main/java/androidx/sample/core/widget/ListViewCompat.java
index 1d51e41..216ef20 100644
--- a/lint-checks/integration-tests/src/main/java/androidx/core/widget/ListViewCompat.java
+++ b/lint-checks/integration-tests/src/main/java/androidx/sample/core/widget/ListViewCompat.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2016 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.core.widget;
+package androidx.sample.core.widget;
 
 import android.os.Build;
 import android.view.View;
diff --git a/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt b/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt
index 5bc2e75..4ee07b7 100644
--- a/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/BanInappropriateExperimentalUsage.kt
@@ -22,13 +22,14 @@
 import com.android.tools.lint.detector.api.Category
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Incident
 import com.android.tools.lint.detector.api.Issue
 import com.android.tools.lint.detector.api.JavaContext
 import com.android.tools.lint.detector.api.Scope
 import com.android.tools.lint.detector.api.Severity
-import com.intellij.psi.PsiCompiledElement
 import org.jetbrains.uast.UAnnotated
 import org.jetbrains.uast.UAnnotation
+import org.jetbrains.uast.UClass
 import org.jetbrains.uast.UElement
 import org.jetbrains.uast.resolveToUElement
 
@@ -45,11 +46,28 @@
 
     private inner class AnnotationChecker(val context: JavaContext) : UElementHandler() {
         override fun visitAnnotation(node: UAnnotation) {
+            if (DEBUG) {
+                if (APPLICABLE_ANNOTATIONS.contains(node.qualifiedName) && node.sourcePsi != null) {
+                    (node.uastParent as? UClass)?.let { annotation ->
+                        println(
+                            "${context.driver.mode}: declared ${annotation.qualifiedName} in " +
+                                "${context.project}"
+                        )
+                    }
+                }
+            }
+
+            // If we find an usage of an experimentally-declared annotation, check it.
             val annotation = node.resolveToUElement()
             if (annotation is UAnnotated) {
                 val annotations = context.evaluator.getAllAnnotations(annotation, false)
-                val isOptIn = annotations.any { APPLICABLE_ANNOTATIONS.contains(it.qualifiedName) }
-                if (isOptIn) {
+                if (annotations.any { APPLICABLE_ANNOTATIONS.contains(it.qualifiedName) }) {
+                    if (DEBUG) {
+                        println(
+                            "${context.driver.mode}: used ${node.qualifiedName} in " +
+                                "${context.project}"
+                        )
+                    }
                     verifyUsageOfElementIsWithinSameGroup(context, node, annotation, ISSUE)
                 }
             }
@@ -64,28 +82,28 @@
     ) {
         val evaluator = context.evaluator
         val usageCoordinates = evaluator.getLibrary(usage) ?: context.project.mavenCoordinate
-        val annotationCoordinates = evaluator.getLibrary(annotation) ?: run {
-            // Is the annotation defined in source code?
-            if (usageCoordinates != null && annotation !is PsiCompiledElement) {
-                annotation.sourcePsi?.let { sourcePsi ->
-                    evaluator.getProject(sourcePsi)?.mavenCoordinate
-                }
-            } else {
-                null
-            }
-        }
         val usageGroupId = usageCoordinates?.groupId
-        val annotationGroupId = annotationCoordinates?.groupId
+        val annotationGroupId = evaluator.getLibrary(annotation)?.groupId
         if (annotationGroupId != usageGroupId && annotationGroupId != null) {
-            context.report(
-                issue, usage, context.getNameLocation(usage),
-                "`Experimental` and `RequiresOptIn` APIs may only be used within the same-version" +
-                    " group where they were defined."
-            )
+            if (DEBUG) {
+                println(
+                    "${context.driver.mode}: report usage of $annotationGroupId in $usageGroupId"
+                )
+            }
+            Incident(context)
+                .issue(issue)
+                .at(usage)
+                .message(
+                    "`Experimental` and `RequiresOptIn` APIs may only be used within the " +
+                        "same-version group where they were defined."
+                )
+                .report()
         }
     }
 
     companion object {
+        private const val DEBUG = false
+
         private const val KOTLIN_EXPERIMENTAL_ANNOTATION = "kotlin.Experimental"
         private const val KOTLIN_REQUIRES_OPT_IN_ANNOTATION = "kotlin.RequiresOptIn"
         private const val JAVA_EXPERIMENTAL_ANNOTATION =
diff --git a/lint-checks/src/main/java/androidx/build/lint/BanUncheckedReflection.kt b/lint-checks/src/main/java/androidx/build/lint/BanUncheckedReflection.kt
index 32c2133..725f322c 100644
--- a/lint-checks/src/main/java/androidx/build/lint/BanUncheckedReflection.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/BanUncheckedReflection.kt
@@ -16,6 +16,7 @@
 @file:Suppress("UnstableApiUsage")
 
 package androidx.build.lint
+
 import com.android.tools.lint.detector.api.Category
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Implementation
@@ -23,47 +24,55 @@
 import com.android.tools.lint.detector.api.JavaContext
 import com.android.tools.lint.detector.api.Scope
 import com.android.tools.lint.checks.VersionChecks.Companion.isWithinVersionCheckConditional
-import com.android.sdklib.SdkVersionInfo
+import com.android.sdklib.SdkVersionInfo.HIGHEST_KNOWN_API
 import com.android.tools.lint.detector.api.Severity
 import com.android.tools.lint.detector.api.SourceCodeScanner
 import com.intellij.psi.PsiMethod
 import org.jetbrains.uast.UCallExpression
-const val METHOD_REFLECTION_CLASS = "java.lang.reflect.Method"
-class BanUncheckedReflection : Detector(), SourceCodeScanner {
-    override fun getApplicableMethodNames() = listOf("invoke")
-    override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
-        // We are not really monitoring if the reflection call is within the right API check
-        // we leave that to the user, and so we check for any API check really. That means
-        // any check with an upper bound of the highest known API or a with a lower bound of 1
-        // (which should technically include every check) is good enough.
-        // Return if not reflection
-        if (!context.evaluator.isMemberInClass(method, METHOD_REFLECTION_CLASS)) return
-        // If not within an SDK check, flag
-        if (!isWithinVersionCheckConditional(
-                context, node, SdkVersionInfo.HIGHEST_KNOWN_API, false
-            ) && !isWithinVersionCheckConditional(
-                    context, node, 1, true
-                )
-        ) {
 
+class BanUncheckedReflection : Detector(), SourceCodeScanner {
+
+    override fun getApplicableMethodNames() = listOf(
+        METHOD_INVOKE_NAME
+    )
+
+    override fun visitMethodCall(
+        context: JavaContext,
+        node: UCallExpression,
+        method: PsiMethod
+    ) {
+        // We don't care if the invocation is correct -- there's another lint for that. We're
+        // just enforcing the "all reflection on the platform SDK must be gated on SDK_INT checks"
+        // policy. Also -- since we're not actually checking whether the invocation is on the
+        // platform SDK -- we're discouraging reflection in general.
+
+        // Skip if this isn't a call to `Method.invoke`.
+        if (!context.evaluator.isMemberInClass(method, METHOD_REFLECTION_CLASS)) return
+
+        // Flag if the call isn't inside an SDK_INT check.
+        if (!isWithinVersionCheckConditional(context, node, HIGHEST_KNOWN_API, false) &&
+            !isWithinVersionCheckConditional(context, node, 1, true)
+        ) {
             context.report(
                 ISSUE, node, context.getLocation(node),
-                "Calling Method.invoke without an SDK check"
+                "Calling `Method.invoke` without an SDK check"
             )
         }
     }
+
     companion object {
         val ISSUE = Issue.create(
             "BanUncheckedReflection",
             "Reflection that is not within an SDK check",
-            "Use of reflection can be risky and there is never a" +
-                " reason to use reflection without" +
-                " having to check for the device's SDK (either through SDK_INT comparison or " +
-                "methods such as isAtLeastP etc...)" +
-                ". Please surround the Method.invoke" +
-                " call with the appropriate SDK_INT check.",
+            "Jetpack policy discourages reflection. In cases where reflection is used on " +
+                "platform SDK classes, it must be used within an `SDK_INT` check that delegates " +
+                "to an equivalent public API on the latest version of the platform. If no " +
+                "equivalent public API exists, reflection must not be used.",
             Category.CORRECTNESS, 5, Severity.ERROR,
             Implementation(BanUncheckedReflection::class.java, Scope.JAVA_FILE_SCOPE)
         )
+
+        const val METHOD_REFLECTION_CLASS = "java.lang.reflect.Method"
+        const val METHOD_INVOKE_NAME = "invoke"
     }
 }
diff --git a/lint-checks/src/test/java/androidx/build/lint/AbstractLintDetectorTest.kt b/lint-checks/src/test/java/androidx/build/lint/AbstractLintDetectorTest.kt
new file mode 100644
index 0000000..8bcfd3e
--- /dev/null
+++ b/lint-checks/src/test/java/androidx/build/lint/AbstractLintDetectorTest.kt
@@ -0,0 +1,107 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.build.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.ProjectDescription
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestFiles
+import com.android.tools.lint.checks.infrastructure.TestLintResult
+import com.android.tools.lint.checks.infrastructure.TestMode
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import java.io.FileNotFoundException
+
+/**
+ * Implementation of [LintDetectorTest] that's slightly more Kotlin-friendly.
+ */
+abstract class AbstractLintDetectorTest(
+    val useDetector: Detector,
+    val useIssues: List<Issue>,
+    val stubs: Array<TestFile> = emptyArray(),
+) : LintDetectorTest() {
+    override fun getDetector(): Detector = useDetector
+
+    override fun getIssues(): List<Issue> = useIssues
+
+    fun check(
+        vararg projects: ProjectDescription
+    ): TestLintResult {
+        // If we have stubs, push those into a virtual project and pass them through the call to
+        // projects(), since attempting to call files() would overwrite the call to projects().
+        val projectsWithStubs = if (stubs.isNotEmpty()) {
+            arrayOf(*projects, project().files(*stubs))
+        } else {
+            projects
+        }
+
+        return lint()
+            .projects(*projectsWithStubs).testModes(TestMode.DEFAULT, TestMode.PARTIAL)
+            .run()
+    }
+
+    fun check(
+        vararg files: TestFile,
+    ): TestLintResult {
+        return lint()
+            .files(
+                *stubs,
+                *files
+            )
+            .run()
+    }
+}
+
+/**
+ * Creates a new [ProjectDescription].
+ */
+fun project(): ProjectDescription = ProjectDescription()
+
+/**
+ * Loads a [TestFile] from `AndroidManifest.xml` included in the JAR resources.
+ */
+fun manifestSample(): TestFile = TestFiles.manifest(
+    Stubs::class.java.getResource(
+        "/AndroidManifest.xml"
+    )?.readText() ?: throw FileNotFoundException(
+        "Could not find AndroidManifest.xml in the integration test project"
+    )
+)
+
+/**
+ * Loads a [TestFile] from Java source code included in the JAR resources.
+ */
+fun javaSample(className: String): TestFile = TestFiles.java(
+    Stubs::class.java.getResource(
+        "/java/${className.replace('.', '/')}.java"
+    )?.readText() ?: throw FileNotFoundException(
+        "Could not find Java sources for $className in the integration test project"
+    )
+)
+
+/**
+ * Loads a [TestFile] from Kotlin source code included in the JAR resources.
+ */
+fun ktSample(className: String): TestFile = TestFiles.kotlin(
+    Stubs::class.java.getResource(
+        "/java/${className.replace('.', '/')}.kt"
+    )?.readText() ?: throw FileNotFoundException(
+        "Could not find Kotlin sources for $className in the integration test project"
+    )
+)
diff --git a/lint-checks/src/test/java/androidx/build/lint/BanConcurrentHashMapTest.kt b/lint-checks/src/test/java/androidx/build/lint/BanConcurrentHashMapTest.kt
new file mode 100644
index 0000000..a01e86e
--- /dev/null
+++ b/lint-checks/src/test/java/androidx/build/lint/BanConcurrentHashMapTest.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.build.lint
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class BanConcurrentHashMapTest : AbstractLintDetectorTest(
+    useDetector = BanConcurrentHashMap(),
+    useIssues = listOf(BanConcurrentHashMap.ISSUE),
+) {
+
+    @Test
+    fun `Detection of ConcurrentHashMap usage in Java sources`() {
+        val input = arrayOf(
+            javaSample("androidx.ConcurrentHashMapUsageJava"),
+        )
+
+        /* ktlint-disable max-line-length */
+        val expected = """
+src/androidx/ConcurrentHashMapUsageJava.java:22: Error: Detected ConcurrentHashMap usage. [BanConcurrentHashMap]
+import java.util.concurrent.ConcurrentHashMap;
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+1 errors, 0 warnings
+        """.trimIndent()
+        /* ktlint-enable max-line-length */
+
+        check(*input).expect(expected)
+    }
+}
\ No newline at end of file
diff --git a/lint-checks/src/test/java/androidx/build/lint/BanInappropriateExperimentalUsageTest.kt b/lint-checks/src/test/java/androidx/build/lint/BanInappropriateExperimentalUsageTest.kt
index 0c7e01e..64e10d8 100644
--- a/lint-checks/src/test/java/androidx/build/lint/BanInappropriateExperimentalUsageTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/BanInappropriateExperimentalUsageTest.kt
@@ -14,20 +14,22 @@
  * limitations under the License.
  */
 
-@file:Suppress("UnstableApiUsage")
+@file:Suppress("UnstableApiUsage", "GroovyUnusedAssignment")
 
 package androidx.build.lint
 
-import com.android.tools.lint.checks.infrastructure.TestFile
-import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestFiles.gradle
-import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
+import com.android.tools.lint.checks.infrastructure.ProjectDescription
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
 @RunWith(JUnit4::class)
-class BanInappropriateExperimentalUsageTest {
+class BanInappropriateExperimentalUsageTest : AbstractLintDetectorTest(
+    useDetector = BanInappropriateExperimentalUsage(),
+    useIssues = listOf(BanInappropriateExperimentalUsage.ISSUE),
+    stubs = arrayOf(Stubs.OptIn),
+) {
 
     @Test
     fun `Test within-module Experimental usage via Gradle model`() {
@@ -42,24 +44,23 @@
                     group=sample.annotation.provider
                     """
                 ).indented(),
-                OPT_IN_KT,
             )
 
-        lint()
-            .projects(provider)
-            .issues(BanInappropriateExperimentalUsage.ISSUE)
-            .run()
-            .expect(
-                """
-                No warnings.
-                """.trimIndent()
-            )
+        /* ktlint-disable max-line-length */
+        val expected = """
+No warnings.
+        """.trimIndent()
+        /* ktlint-enable max-line-length */
+
+        check(provider).expect(expected)
     }
 
+    @Ignore("b/188814760")
     @Test
     fun `Test cross-module Experimental usage via Gradle model`() {
         val provider = project()
             .name("provider")
+            .type(ProjectDescription.Type.LIBRARY)
             .report(false)
             .files(
                 ktSample("sample.annotation.provider.ExperimentalSampleAnnotation"),
@@ -70,11 +71,11 @@
                     group=sample.annotation.provider
                     """
                 ).indented(),
-                OPT_IN_KT,
             )
 
         val consumer = project()
             .name("consumer")
+            .type(ProjectDescription.Type.LIBRARY)
             .dependsOn(provider)
             .files(
                 ktSample("androidx.sample.consumer.OutsideGroupExperimentalAnnotatedClass"),
@@ -83,69 +84,18 @@
                     apply plugin: 'com.android.library'
                     group=androidx.sample.consumer
                     """
-                ).indented()
+                ).indented(),
             )
 
-        lint()
-            .projects(provider, consumer)
-            .issues(BanInappropriateExperimentalUsage.ISSUE)
-            .run()
-            .expect(
-                /* ktlint-enable max-line-length */
-                """
-                src/main/kotlin/androidx/sample/consumer/OutsideGroupExperimentalAnnotatedClass.kt:25: Error: Experimental and RequiresOptIn APIs may only be used within the same-version group where they were defined. [IllegalExperimentalApiUsage]
-                    @ExperimentalSampleAnnotationJava
-                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-                1 errors, 0 warnings
-                """.trimIndent()
-                /* ktlint-enable max-line-length */
-            )
+        /* ktlint-disable max-line-length */
+        val expected = """
+../consumer/src/main/kotlin/androidx/sample/consumer/OutsideGroupExperimentalAnnotatedClass.kt:25: Error: Experimental and RequiresOptIn APIs may only be used within the same-version group where they were defined. [IllegalExperimentalApiUsage]
+    @ExperimentalSampleAnnotationJava
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+1 errors, 0 warnings
+        """.trimIndent()
+        /* ktlint-enable max-line-length */
+
+        check(provider, consumer).expect(expected)
     }
 }
-
-/* ktlint-disable max-line-length */
-
-/**
- * [TestFile] containing OptIn.kt from the Kotlin standard library.
- *
- * This is a workaround for the Kotlin standard library used by the Lint test harness not
- * including the Experimental annotation by default.
- */
-private val OPT_IN_KT: TestFile = TestFiles.kotlin(
-    """
-    package kotlin
-
-    import kotlin.annotation.AnnotationRetention.BINARY
-    import kotlin.annotation.AnnotationRetention.SOURCE
-    import kotlin.annotation.AnnotationTarget.*
-    import kotlin.internal.RequireKotlin
-    import kotlin.internal.RequireKotlinVersionKind
-    import kotlin.reflect.KClass
-
-    @Target(ANNOTATION_CLASS)
-    @Retention(BINARY)
-    @SinceKotlin("1.3")
-    @RequireKotlin("1.3.70", versionKind = RequireKotlinVersionKind.COMPILER_VERSION)
-    public annotation class RequiresOptIn(
-        val message: String = "",
-        val level: Level = Level.ERROR
-    ) {
-        public enum class Level {
-            WARNING,
-            ERROR,
-        }
-    }
-
-    @Target(
-        CLASS, PROPERTY, LOCAL_VARIABLE, VALUE_PARAMETER, CONSTRUCTOR, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER, EXPRESSION, FILE, TYPEALIAS
-    )
-    @Retention(SOURCE)
-    @SinceKotlin("1.3")
-    @RequireKotlin("1.3.70", versionKind = RequireKotlinVersionKind.COMPILER_VERSION)
-    public annotation class OptIn(
-        vararg val markerClass: KClass<out Annotation>
-    )
-    """.trimIndent()
-)
-
-/* ktlint-enable max-line-length */
diff --git a/lint-checks/src/test/java/androidx/build/lint/BanKeepAnnotationTest.kt b/lint-checks/src/test/java/androidx/build/lint/BanKeepAnnotationTest.kt
index 4b5b504..dceb43b 100644
--- a/lint-checks/src/test/java/androidx/build/lint/BanKeepAnnotationTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/BanKeepAnnotationTest.kt
@@ -14,55 +14,36 @@
  * limitations under the License.
  */
 
+@file:Suppress("UnstableApiUsage")
+
 package androidx.build.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest
-import com.android.tools.lint.checks.infrastructure.TestFiles.java
-import com.android.tools.lint.checks.infrastructure.TestLintResult
-import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
-import com.android.tools.lint.detector.api.Detector
-import com.android.tools.lint.detector.api.Issue
-
 import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
 
-class BanKeepAnnotationTest : LintDetectorTest() {
-    override fun getDetector(): Detector = BanKeepAnnotation()
+@RunWith(JUnit4::class)
+class BanKeepAnnotationTest : AbstractLintDetectorTest(
+    useDetector = BanKeepAnnotation(),
+    useIssues = listOf(BanKeepAnnotation.ISSUE),
+    stubs = arrayOf(Stubs.Keep),
+) {
 
-    override fun getIssues(): List<Issue> = listOf(
-        BanKeepAnnotation.ISSUE
-    )
-
-    private fun check(code: String): TestLintResult {
-        return lint().files(
-            java(annotationSource),
-            java(code)
+    @Test
+    fun `Detection of Keep annotation in Java sources`() {
+        val input = arrayOf(
+            javaSample("androidx.KeepAnnotationUsageJava"),
         )
-            .run()
-    }
 
-    private val annotationSource = """
-        package androidx.annotation;
-
-        public @interface Keep {
-        }
-    """
-
-    @Test fun testAnnotatedUnreferencedClass() {
-        val input = """
-            package androidx.sample;
-
-            import androidx.annotation.Keep;
-            @Keep
-            public class SampleClass {
-            }
-        """
+        /* ktlint-disable max-line-length */
         val expected = """
-            src/androidx/sample/SampleClass.java:4: Error: Uses @Keep annotation [BanKeepAnnotation]
-            @Keep
-            ~~~~~
-            1 errors, 0 warnings
-        """
-        check(input.trimIndent())
-            .expect(expected.trimIndent())
+src/androidx/KeepAnnotationUsageJava.java:21: Error: Uses @Keep annotation [BanKeepAnnotation]
+@Keep
+~~~~~
+1 errors, 0 warnings
+        """.trimIndent()
+        /* ktlint-enable max-line-length */
+
+        check(*input).expect(expected)
     }
 }
diff --git a/lint-checks/src/test/java/androidx/build/lint/BanParcelableUsageTest.kt b/lint-checks/src/test/java/androidx/build/lint/BanParcelableUsageTest.kt
new file mode 100644
index 0000000..b2cc7d4
--- /dev/null
+++ b/lint-checks/src/test/java/androidx/build/lint/BanParcelableUsageTest.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.build.lint
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class BanParcelableUsageTest : AbstractLintDetectorTest(
+    useDetector = BanParcelableUsage(),
+    useIssues = listOf(BanParcelableUsage.ISSUE),
+) {
+
+    @Test
+    fun `Detection of Parcelable usage in Java sources`() {
+        val input = arrayOf(
+            javaSample("androidx.ParcelableUsageJava"),
+        )
+
+        /* ktlint-disable max-line-length */
+        val expected = """
+src/androidx/ParcelableUsageJava.java:25: Error: Class implements android.os.Parcelable [BanParcelableUsage]
+public class ParcelableUsageJava implements Parcelable {
+             ~~~~~~~~~~~~~~~~~~~
+1 errors, 0 warnings
+        """.trimIndent()
+        /* ktlint-enable max-line-length */
+
+        check(*input).expect(expected)
+    }
+}
\ No newline at end of file
diff --git a/lint-checks/src/test/java/androidx/build/lint/BanSynchronizedMethodsTest.kt b/lint-checks/src/test/java/androidx/build/lint/BanSynchronizedMethodsTest.kt
new file mode 100644
index 0000000..702b681
--- /dev/null
+++ b/lint-checks/src/test/java/androidx/build/lint/BanSynchronizedMethodsTest.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.build.lint
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class BanSynchronizedMethodsTest : AbstractLintDetectorTest(
+    useDetector = BanSynchronizedMethods(),
+    useIssues = listOf(BanSynchronizedMethods.ISSUE),
+) {
+
+    @Test
+    fun `Detection of synchronized methods in Java sources`() {
+        val input = arrayOf(
+            javaSample("androidx.SynchronizedMethodJava"),
+        )
+
+        /* ktlint-disable max-line-length */
+        val expected = """
+src/androidx/SynchronizedMethodJava.java:22: Error: Use of synchronized methods is not recommended [BanSynchronizedMethods]
+    public synchronized void someMethod() {
+    ^
+1 errors, 0 warnings
+        """.trimIndent()
+        /* ktlint-enable max-line-length */
+
+        check(*input).expect(expected)
+    }
+}
diff --git a/lint-checks/src/test/java/androidx/build/lint/BanTargetApiAnnotationTest.kt b/lint-checks/src/test/java/androidx/build/lint/BanTargetApiAnnotationTest.kt
new file mode 100644
index 0000000..49429e0
--- /dev/null
+++ b/lint-checks/src/test/java/androidx/build/lint/BanTargetApiAnnotationTest.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.build.lint
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class BanTargetApiAnnotationTest : AbstractLintDetectorTest(
+    useDetector = BanTargetApiAnnotation(),
+    useIssues = listOf(BanTargetApiAnnotation.ISSUE),
+) {
+
+    @Test
+    fun `Detection of TargetApi usage in Java sources`() {
+        val input = arrayOf(
+            javaSample("androidx.TargetApiUsageJava"),
+        )
+
+        /* ktlint-disable max-line-length */
+        val expected = """
+src/androidx/TargetApiUsageJava.java:22: Error: Uses @TargetApi annotation [BanTargetApiAnnotation]
+@TargetApi(29)
+~~~~~~~~~~~~~~
+src/androidx/TargetApiUsageJava.java:25: Error: Uses @TargetApi annotation [BanTargetApiAnnotation]
+    @TargetApi(30)
+    ~~~~~~~~~~~~~~
+2 errors, 0 warnings
+        """.trimIndent()
+        /* ktlint-enable max-line-length */
+
+        check(*input).expect(expected)
+    }
+}
\ No newline at end of file
diff --git a/lint-checks/src/test/java/androidx/build/lint/BanUncheckedReflectionTest.kt b/lint-checks/src/test/java/androidx/build/lint/BanUncheckedReflectionTest.kt
new file mode 100644
index 0000000..6c66b16
--- /dev/null
+++ b/lint-checks/src/test/java/androidx/build/lint/BanUncheckedReflectionTest.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.build.lint
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class BanUncheckedReflectionTest : AbstractLintDetectorTest(
+    useDetector = BanUncheckedReflection(),
+    useIssues = listOf(BanUncheckedReflection.ISSUE),
+    stubs = arrayOf(Stubs.ChecksSdkIntAtLeast),
+) {
+
+    @Test
+    fun `Detection of unchecked reflection in real-world Java sources`() {
+        val input = arrayOf(
+            javaSample("androidx.sample.core.app.ActivityRecreator"),
+        )
+
+        /* ktlint-disable max-line-length */
+        val expected = """
+src/androidx/sample/core/app/ActivityRecreator.java:145: Error: Calling Method.invoke without an SDK check [BanUncheckedReflection]
+                    requestRelaunchActivityMethod.invoke(activityThread,
+                    ^
+src/androidx/sample/core/app/ActivityRecreator.java:262: Error: Calling Method.invoke without an SDK check [BanUncheckedReflection]
+                        performStopActivity3ParamsMethod.invoke(activityThread,
+                        ^
+src/androidx/sample/core/app/ActivityRecreator.java:265: Error: Calling Method.invoke without an SDK check [BanUncheckedReflection]
+                        performStopActivity2ParamsMethod.invoke(activityThread,
+                        ^
+3 errors, 0 warnings
+        """.trimIndent()
+        /* ktlint-enable max-line-length */
+
+        check(*input).expect(expected)
+    }
+
+    @Test
+    fun `Checked reflection in real-world Java sources`() {
+        val input = arrayOf(
+            javaSample("androidx.sample.core.app.ActivityRecreatorChecked"),
+        )
+
+        /* ktlint-disable max-line-length */
+        val expected = """
+No warnings.
+        """.trimIndent()
+        /* ktlint-enable max-line-length */
+
+        check(*input).expect(expected)
+    }
+}
diff --git a/lint-checks/src/test/java/androidx/build/lint/ClassVerificationFailureDetectorTest.kt b/lint-checks/src/test/java/androidx/build/lint/ClassVerificationFailureDetectorTest.kt
index 253df56..c1006d3 100644
--- a/lint-checks/src/test/java/androidx/build/lint/ClassVerificationFailureDetectorTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/ClassVerificationFailureDetectorTest.kt
@@ -14,38 +14,28 @@
  * limitations under the License.
  */
 
+@file:Suppress("UnstableApiUsage")
+
 package androidx.build.lint
 
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest.manifest
-import com.android.tools.lint.checks.infrastructure.TestFile
-import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintResult
-import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
-@Suppress("UnstableApiUsage")
 @RunWith(JUnit4::class)
-class ClassVerificationFailureDetectorTest {
-
-    private fun check(
-        vararg testFiles: TestFile,
-        minSdkVersion: Int = 14,
-    ): TestLintResult {
-        return lint()
-            .files(
-                manifest().minSdk(minSdkVersion),
-                *testFiles,
-            )
-            .issues(ClassVerificationFailureDetector.ISSUE)
-            .run()
-    }
+class ClassVerificationFailureDetectorTest : AbstractLintDetectorTest(
+    useDetector = ClassVerificationFailureDetector(),
+    useIssues = listOf(ClassVerificationFailureDetector.ISSUE),
+    stubs = arrayOf(
+        // AndroidManifest with minSdkVersion=14
+        manifest().minSdk(14),
+    ),
+) {
 
     @Test
     fun `Detection of unsafe references in Java sources`() {
         val input = arrayOf(
-            javaSample("androidx.ClassVerificationFailureFromJava")
+            javaSample("androidx.ClassVerificationFailureFromJava"),
         )
 
         /* ktlint-disable max-line-length */
@@ -72,22 +62,22 @@
     @Test
     fun `Detection and auto-fix of unsafe references in real-world Java sources`() {
         val input = arrayOf(
-            javaSample("androidx.core.widget.ListViewCompat")
+            javaSample("androidx.sample.core.widget.ListViewCompat"),
         )
 
         /* ktlint-disable max-line-length */
         val expected = """
-src/androidx/core/widget/ListViewCompat.java:39: Error: This call references a method added in API level 19; however, the containing class androidx.core.widget.ListViewCompat is reachable from earlier API levels and will fail run-time class verification. [ClassVerificationFailure]
+src/androidx/sample/core/widget/ListViewCompat.java:39: Error: This call references a method added in API level 19; however, the containing class androidx.sample.core.widget.ListViewCompat is reachable from earlier API levels and will fail run-time class verification. [ClassVerificationFailure]
             listView.scrollListBy(y);
                      ~~~~~~~~~~~~
-src/androidx/core/widget/ListViewCompat.java:69: Error: This call references a method added in API level 19; however, the containing class androidx.core.widget.ListViewCompat is reachable from earlier API levels and will fail run-time class verification. [ClassVerificationFailure]
+src/androidx/sample/core/widget/ListViewCompat.java:69: Error: This call references a method added in API level 19; however, the containing class androidx.sample.core.widget.ListViewCompat is reachable from earlier API levels and will fail run-time class verification. [ClassVerificationFailure]
             return listView.canScrollList(direction);
                             ~~~~~~~~~~~~~
 2 errors, 0 warnings
         """.trimIndent()
 
         val expectedFix = """
-Fix for src/androidx/core/widget/ListViewCompat.java line 39: Extract to static inner class:
+Fix for src/androidx/sample/core/widget/ListViewCompat.java line 39: Extract to static inner class:
 @@ -39 +39
 -             listView.scrollListBy(y);
 +             Api19Impl.scrollListBy(listView, y);
@@ -106,7 +96,7 @@
 @@ -93 +102
 + }}
 +
-Fix for src/androidx/core/widget/ListViewCompat.java line 69: Extract to static inner class:
+Fix for src/androidx/sample/core/widget/ListViewCompat.java line 69: Extract to static inner class:
 @@ -69 +69
 -             return listView.canScrollList(direction);
 +             return Api19Impl.canScrollList(listView, direction);
@@ -134,7 +124,7 @@
     @Test
     fun `Auto-fix unsafe void-type method reference in Java source`() {
         val input = arrayOf(
-            javaSample("androidx.AutofixUnsafeVoidMethodReferenceJava")
+            javaSample("androidx.AutofixUnsafeVoidMethodReferenceJava"),
         )
 
         /* ktlint-disable max-line-length */
@@ -167,7 +157,7 @@
     @Test
     fun `Auto-fix unsafe constructor reference in Java source`() {
         val input = arrayOf(
-            javaSample("androidx.AutofixUnsafeConstructorReferenceJava")
+            javaSample("androidx.AutofixUnsafeConstructorReferenceJava"),
         )
 
         /* ktlint-disable max-line-length */
@@ -200,7 +190,7 @@
     @Test
     fun `Auto-fix unsafe static method reference in Java source`() {
         val input = arrayOf(
-            javaSample("androidx.AutofixUnsafeStaticMethodReferenceJava")
+            javaSample("androidx.AutofixUnsafeStaticMethodReferenceJava"),
         )
 
         /* ktlint-disable max-line-length */
@@ -233,7 +223,7 @@
     @Test
     fun `Auto-fix unsafe generic-type method reference in Java source`() {
         val input = arrayOf(
-            javaSample("androidx.AutofixUnsafeGenericMethodReferenceJava")
+            javaSample("androidx.AutofixUnsafeGenericMethodReferenceJava"),
         )
 
         /* ktlint-disable max-line-length */
@@ -266,7 +256,7 @@
     @Test
     fun `Auto-fix unsafe reference in Java source with existing inner class`() {
         val input = arrayOf(
-            javaSample("androidx.AutofixUnsafeReferenceWithExistingClassJava")
+            javaSample("androidx.AutofixUnsafeReferenceWithExistingClassJava"),
         )
 
         /* ktlint-disable max-line-length */
@@ -295,18 +285,4 @@
 
         check(*input).expectFixDiffs(expectedFix)
     }
-
-    /**
-     * Loads a [TestFile] from Java source code included in the JAR resources.
-     */
-    private fun javaSample(className: String): TestFile = TestFiles.java(
-        javaClass.getResource("/java/${className.replace('.', '/')}.java").readText()
-    )
-
-    /**
-     * Loads a [TestFile] from Kotlin source code included in the JAR resources.
-     */
-    private fun ktSample(className: String): TestFile = TestFiles.kotlin(
-        javaClass.getResource("/java/${className.replace('.', '/')}.kt").readText()
-    )
 }
\ No newline at end of file
diff --git a/lint-checks/src/test/java/androidx/build/lint/IdeaSuppressionDetectorTest.kt b/lint-checks/src/test/java/androidx/build/lint/IdeaSuppressionDetectorTest.kt
index d3879db..0ed74a4 100644
--- a/lint-checks/src/test/java/androidx/build/lint/IdeaSuppressionDetectorTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/IdeaSuppressionDetectorTest.kt
@@ -14,30 +14,19 @@
  * limitations under the License.
  */
 
+@file:Suppress("UnstableApiUsage")
+
 package androidx.build.lint
 
-import com.android.tools.lint.checks.infrastructure.TestFile
-import com.android.tools.lint.checks.infrastructure.TestFiles
-import com.android.tools.lint.checks.infrastructure.TestLintResult
-import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
-@Suppress("UnstableApiUsage")
 @RunWith(JUnit4::class)
-class IdeaSuppressionDetectorTest {
-
-    private fun check(
-        vararg testFiles: TestFile,
-    ): TestLintResult {
-        return lint()
-            .files(
-                *testFiles,
-            )
-            .issues(IdeaSuppressionDetector.ISSUE)
-            .run()
-    }
+class IdeaSuppressionDetectorTest : AbstractLintDetectorTest(
+    useDetector = IdeaSuppressionDetector(),
+    useIssues = listOf(IdeaSuppressionDetector.ISSUE),
+) {
 
     @Test
     fun `Detection of IDEA-specific suppression in Java sources`() {
@@ -56,11 +45,4 @@
 
         check(*input).expect(expected)
     }
-
-    /**
-     * Loads a [TestFile] from Java source code included in the JAR resources.
-     */
-    private fun javaSample(className: String): TestFile = TestFiles.java(
-        javaClass.getResource("/java/${className.replace('.', '/')}.java").readText()
-    )
-}
\ No newline at end of file
+}
diff --git a/lint-checks/src/test/java/androidx/build/lint/MetadataTagInsideApplicationTagDetectorTest.kt b/lint-checks/src/test/java/androidx/build/lint/MetadataTagInsideApplicationTagDetectorTest.kt
new file mode 100644
index 0000000..f587614
--- /dev/null
+++ b/lint-checks/src/test/java/androidx/build/lint/MetadataTagInsideApplicationTagDetectorTest.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.build.lint
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class MetadataTagInsideApplicationTagDetectorTest : AbstractLintDetectorTest(
+    useDetector = MetadataTagInsideApplicationTagDetector(),
+    useIssues = listOf(MetadataTagInsideApplicationTagDetector.ISSUE),
+) {
+
+    @Test
+    fun `Detect usage of metadata tag insice application tag`() {
+        val input = arrayOf(
+            manifestSample()
+        )
+
+        /* ktlint-disable max-line-length */
+        val expected = """
+AndroidManifest.xml:20: Error: Detected <application>-level meta-data tag. [MetadataTagInsideApplicationTag]
+        <meta-data android:name="name" android:value="value" />
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+1 errors, 0 warnings
+        """.trimIndent()
+        /* ktlint-enable max-line-length */
+
+        check(*input).expect(expected)
+    }
+}
diff --git a/lint-checks/src/test/java/androidx/build/lint/ObsoleteBuildCompatUsageDetectorTest.kt b/lint-checks/src/test/java/androidx/build/lint/ObsoleteBuildCompatUsageDetectorTest.kt
index deaab57..874cc32 100644
--- a/lint-checks/src/test/java/androidx/build/lint/ObsoleteBuildCompatUsageDetectorTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/ObsoleteBuildCompatUsageDetectorTest.kt
@@ -14,40 +14,27 @@
  * limitations under the License.
  */
 
-package androidx.build.lint
+@file:Suppress("UnstableApiUsage")
 
-import com.android.tools.lint.checks.infrastructure.TestFiles.java
-import com.android.tools.lint.checks.infrastructure.TestLintResult
-import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
+package androidx.build.lint
 
 import org.junit.Ignore
 import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
 
 @Ignore("ANDROID_HOME not available on CI")
-class ObsoleteBuildCompatUsageDetectorTest {
-    private val buildCompatStub = java(
-        """
-        package androidx.core.os;
-        public class BuildCompat {
-          public static boolean isAtLeastN() { return false; }
-          public static boolean isAtLeastNMR1() { return false; }
-          public static boolean isAtLeastO() { return false; }
-          public static boolean isAtLeastOMR1() { return false; }
-          public static boolean isAtLeastP() { return false; }
-          public static boolean isAtLeastQ() { return false; }
-        }
-        """.trimIndent()
-    )
+@RunWith(JUnit4::class)
+class ObsoleteBuildCompatUsageDetectorTest : AbstractLintDetectorTest(
+    useDetector = ObsoleteBuildCompatUsageDetector(),
+    useIssues = listOf(ObsoleteBuildCompatUsageDetector.ISSUE),
+    stubs = arrayOf(BuildCompat),
+) {
 
-    private fun check(vararg code: String): TestLintResult {
-        return lint().files(buildCompatStub, *code.map(::java).toTypedArray())
-            .allowMissingSdk(true)
-            .issues(ObsoleteBuildCompatUsageDetector.ISSUE)
-            .run()
-    }
-
-    @Test fun isAtLeastN() {
-        val input = """
+    @Test
+    fun isAtLeastN() {
+        val input = java(
+            """
             package foo;
             import androidx.core.os.BuildCompat;
             public class Example {
@@ -57,26 +44,34 @@
                 }
               }
             }
-        """
+            """.trimIndent()
+        )
+
+        /* ktlint-disable max-line-length */
         val expected = """
             src/foo/Example.java:5: Error: Using deprecated BuildCompat methods [ObsoleteBuildCompat]
                 if (BuildCompat.isAtLeastN()) {
                     ~~~~~~~~~~~~~~~~~~~~~~~~
             1 errors, 0 warnings
         """
+
         val expectedDiff = """
             Fix for src/foo/Example.java line 5: Use SDK_INT >= 24:
             @@ -5 +5
             -     if (BuildCompat.isAtLeastN()) {
             +     if (Build.VERSION.SDK_INT >= 24) {
         """
-        check(input.trimIndent())
+        /* ktlint-enable max-line-length */
+
+        check(input)
             .expect(expected.trimIndent())
             .expectFixDiffs(expectedDiff.trimIndent())
     }
 
-    @Test fun isAtLeastNStaticImport() {
-        val input = """
+    @Test
+    fun isAtLeastNStaticImport() {
+        val input = java(
+            """
             package foo;
             import static androidx.core.os.BuildCompat.isAtLeastN;
             public class Example {
@@ -86,26 +81,34 @@
                 }
               }
             }
-        """
+            """.trimIndent()
+        )
+
+        /* ktlint-disable max-line-length */
         val expected = """
             src/foo/Example.java:5: Error: Using deprecated BuildCompat methods [ObsoleteBuildCompat]
                 if (isAtLeastN()) {
                     ~~~~~~~~~~~~
             1 errors, 0 warnings
         """
+
         val expectedDiff = """
             Fix for src/foo/Example.java line 5: Use SDK_INT >= 24:
             @@ -5 +5
             -     if (isAtLeastN()) {
             +     if (Build.VERSION.SDK_INT >= 24) {
         """
-        check(input.trimIndent())
+        /* ktlint-enable max-line-length */
+
+        check(input)
             .expect(expected.trimIndent())
             .expectFixDiffs(expectedDiff.trimIndent())
     }
 
-    @Test fun isAtLeastNMR1() {
-        val input = """
+    @Test
+    fun isAtLeastNMR1() {
+        val input = java(
+            """
             package foo;
             import androidx.core.os.BuildCompat;
             public class Example {
@@ -115,26 +118,34 @@
                 }
               }
             }
-        """
+            """.trimIndent()
+        )
+
+        /* ktlint-disable max-line-length */
         val expected = """
             src/foo/Example.java:5: Error: Using deprecated BuildCompat methods [ObsoleteBuildCompat]
                 if (BuildCompat.isAtLeastNMR1()) {
                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~
             1 errors, 0 warnings
         """
+
         val expectedDiff = """
             Fix for src/foo/Example.java line 5: Use SDK_INT >= 25:
             @@ -5 +5
             -     if (BuildCompat.isAtLeastNMR1()) {
             +     if (Build.VERSION.SDK_INT >= 25) {
         """
-        check(input.trimIndent())
+        /* ktlint-enable max-line-length */
+
+        check(input)
             .expect(expected.trimIndent())
             .expectFixDiffs(expectedDiff.trimIndent())
     }
 
-    @Test fun isAtLeastO() {
-        val input = """
+    @Test
+    fun isAtLeastO() {
+        val input = java(
+            """
             package foo;
             import androidx.core.os.BuildCompat;
             public class Example {
@@ -144,26 +155,34 @@
                 }
               }
             }
-        """
+            """.trimIndent()
+        )
+
+        /* ktlint-disable max-line-length */
         val expected = """
             src/foo/Example.java:5: Error: Using deprecated BuildCompat methods [ObsoleteBuildCompat]
                 if (BuildCompat.isAtLeastO()) {
                     ~~~~~~~~~~~~~~~~~~~~~~~~
             1 errors, 0 warnings
         """
+
         val expectedDiff = """
             Fix for src/foo/Example.java line 5: Use SDK_INT >= 26:
             @@ -5 +5
             -     if (BuildCompat.isAtLeastO()) {
             +     if (Build.VERSION.SDK_INT >= 26) {
         """
-        check(input.trimIndent())
+        /* ktlint-enable max-line-length */
+
+        check(input)
             .expect(expected.trimIndent())
             .expectFixDiffs(expectedDiff.trimIndent())
     }
 
-    @Test fun isAtLeastOMR1() {
-        val input = """
+    @Test
+    fun isAtLeastOMR1() {
+        val input = java(
+            """
             package foo;
             import androidx.core.os.BuildCompat;
             public class Example {
@@ -173,26 +192,34 @@
                 }
               }
             }
-        """
+            """.trimIndent()
+        )
+
+        /* ktlint-disable max-line-length */
         val expected = """
             src/foo/Example.java:5: Error: Using deprecated BuildCompat methods [ObsoleteBuildCompat]
                 if (BuildCompat.isAtLeastOMR1()) {
                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~
             1 errors, 0 warnings
         """
+
         val expectedDiff = """
             Fix for src/foo/Example.java line 5: Use SDK_INT >= 27:
             @@ -5 +5
             -     if (BuildCompat.isAtLeastOMR1()) {
             +     if (Build.VERSION.SDK_INT >= 27) {
         """
-        check(input.trimIndent())
+        /* ktlint-enable max-line-length */
+
+        check(input)
             .expect(expected.trimIndent())
             .expectFixDiffs(expectedDiff.trimIndent())
     }
 
-    @Test fun isAtLeastP() {
-        val input = """
+    @Test
+    fun isAtLeastP() {
+        val input = java(
+            """
             package foo;
             import androidx.core.os.BuildCompat;
             public class Example {
@@ -202,26 +229,34 @@
                 }
               }
             }
-        """
+            """.trimIndent()
+        )
+
+        /* ktlint-disable max-line-length */
         val expected = """
             src/foo/Example.java:5: Error: Using deprecated BuildCompat methods [ObsoleteBuildCompat]
                 if (BuildCompat.isAtLeastP()) {
                     ~~~~~~~~~~~~~~~~~~~~~~~~
             1 errors, 0 warnings
         """
+
         val expectedDiff = """
             Fix for src/foo/Example.java line 5: Use SDK_INT >= 28:
             @@ -5 +5
             -     if (BuildCompat.isAtLeastP()) {
             +     if (Build.VERSION.SDK_INT >= 28) {
         """
-        check(input.trimIndent())
+        /* ktlint-enable max-line-length */
+
+        check(input)
             .expect(expected.trimIndent())
             .expectFixDiffs(expectedDiff.trimIndent())
     }
 
-    @Test fun isAtLeastQ() {
-        val input = """
+    @Test
+    fun isAtLeastQ() {
+        val input = java(
+            """
             package foo;
             import androidx.core.os.BuildCompat;
             public class Example {
@@ -231,21 +266,43 @@
                 }
               }
             }
-        """
+            """.trimIndent()
+        )
+
+        /* ktlint-disable max-line-length */
         val expected = """
             src/foo/Example.java:5: Error: Using deprecated BuildCompat methods [ObsoleteBuildCompat]
                 if (BuildCompat.isAtLeastQ()) {
                     ~~~~~~~~~~~~~~~~~~~~~~~~
             1 errors, 0 warnings
         """
+
         val expectedDiff = """
             Fix for src/foo/Example.java line 5: Use SDK_INT >= 29:
             @@ -5 +5
             -     if (BuildCompat.isAtLeastQ()) {
             +     if (Build.VERSION.SDK_INT >= 29) {
         """
-        check(input.trimIndent())
+        /* ktlint-enable max-line-length */
+
+        check(input)
             .expect(expected.trimIndent())
             .expectFixDiffs(expectedDiff.trimIndent())
     }
+
+    companion object {
+        private val BuildCompat = java(
+            """
+            package androidx.core.os;
+            public class BuildCompat {
+              public static boolean isAtLeastN() { return false; }
+              public static boolean isAtLeastNMR1() { return false; }
+              public static boolean isAtLeastO() { return false; }
+              public static boolean isAtLeastOMR1() { return false; }
+              public static boolean isAtLeastP() { return false; }
+              public static boolean isAtLeastQ() { return false; }
+            }
+            """.trimIndent()
+        )
+    }
 }
diff --git a/lint-checks/src/test/java/androidx/build/lint/PrivateConstructorForUtilityClassDetectorTest.kt b/lint-checks/src/test/java/androidx/build/lint/PrivateConstructorForUtilityClassDetectorTest.kt
index 0d51308..b8ac0d9 100644
--- a/lint-checks/src/test/java/androidx/build/lint/PrivateConstructorForUtilityClassDetectorTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/PrivateConstructorForUtilityClassDetectorTest.kt
@@ -18,24 +18,15 @@
 
 package androidx.build.lint
 
-import com.android.tools.lint.checks.infrastructure.TestFile
-import com.android.tools.lint.checks.infrastructure.TestLintResult
-import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
 @RunWith(JUnit4::class)
-class PrivateConstructorForUtilityClassDetectorTest {
-
-    private fun check(
-        vararg testFiles: TestFile,
-    ): TestLintResult {
-        return lint()
-            .files(*testFiles)
-            .issues(PrivateConstructorForUtilityClassDetector.ISSUE)
-            .run()
-    }
+class PrivateConstructorForUtilityClassDetectorTest : AbstractLintDetectorTest(
+    useDetector = PrivateConstructorForUtilityClassDetector(),
+    useIssues = listOf(PrivateConstructorForUtilityClassDetector.ISSUE),
+) {
 
     @Test
     fun testInnerClassVisibilityJava() {
diff --git a/lint-checks/src/test/java/androidx/build/lint/SampledAnnotationEnforcerTest.kt b/lint-checks/src/test/java/androidx/build/lint/SampledAnnotationEnforcerTest.kt
index 2c638cb..0e6943f 100644
--- a/lint-checks/src/test/java/androidx/build/lint/SampledAnnotationEnforcerTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/SampledAnnotationEnforcerTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-@file:Suppress("KDocUnresolvedReference")
+@file:Suppress("KDocUnresolvedReference", "UnstableApiUsage")
 
 package androidx.build.lint
 
diff --git a/lint-checks/src/test/java/androidx/build/lint/Stubs.kt b/lint-checks/src/test/java/androidx/build/lint/Stubs.kt
new file mode 100644
index 0000000..06a6353
--- /dev/null
+++ b/lint-checks/src/test/java/androidx/build/lint/Stubs.kt
@@ -0,0 +1,164 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.build.lint
+
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestFiles
+
+class Stubs {
+
+    companion object {
+
+        /* ktlint-disable max-line-length */
+
+        /**
+         * [TestFile] containing Keep.java from the annotation library.
+         */
+        val Keep = TestFiles.java(
+            """
+package androidx.annotation;
+
+public @interface Keep {
+}
+            """
+        )
+
+        val RunWith = TestFiles.kotlin(
+            """
+package org.junit.runner
+
+annotation class RunWith(val value: KClass<*>)
+            """
+        )
+
+        val JUnit4Runner = TestFiles.kotlin(
+            """
+package org.junit.runners
+
+class JUnit4
+            """
+        )
+
+        val ParameterizedRunner = TestFiles.kotlin(
+            """
+package org.junit.runners
+
+class Parameterized
+            """
+        )
+
+        val AndroidJUnit4Runner = TestFiles.kotlin(
+            """
+package androidx.test.ext.junit.runners
+
+class AndroidJUnit4
+            """
+        )
+
+        val TestSizeAnnotations = TestFiles.kotlin(
+            """
+package androidx.test.filters
+
+annotation class SmallTest
+annotation class MediumTest
+annotation class LargeTest
+            """
+        )
+
+        val TestAnnotation = TestFiles.kotlin(
+            """
+package org.junit
+
+annotation class Test
+            """
+        )
+
+        /**
+         * [TestFile] containing OptIn.kt from the Kotlin standard library.
+         *
+         * This is a workaround for the Kotlin standard library used by the Lint test harness not
+         * including the Experimental annotation by default.
+         */
+        val OptIn = TestFiles.kotlin(
+            """
+package kotlin
+
+import kotlin.annotation.AnnotationRetention.BINARY
+import kotlin.annotation.AnnotationRetention.SOURCE
+import kotlin.annotation.AnnotationTarget.*
+import kotlin.internal.RequireKotlin
+import kotlin.internal.RequireKotlinVersionKind
+import kotlin.reflect.KClass
+
+@Target(ANNOTATION_CLASS)
+@Retention(BINARY)
+@SinceKotlin("1.3")
+@RequireKotlin("1.3.70", versionKind = RequireKotlinVersionKind.COMPILER_VERSION)
+public annotation class RequiresOptIn(
+    val message: String = "",
+    val level: Level = Level.ERROR
+) {
+    public enum class Level {
+        WARNING,
+        ERROR,
+    }
+}
+
+@Target(
+    CLASS, PROPERTY, LOCAL_VARIABLE, VALUE_PARAMETER, CONSTRUCTOR, FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER, EXPRESSION, FILE, TYPEALIAS
+)
+@Retention(SOURCE)
+@SinceKotlin("1.3")
+@RequireKotlin("1.3.70", versionKind = RequireKotlinVersionKind.COMPILER_VERSION)
+public annotation class OptIn(
+    vararg val markerClass: KClass<out Annotation>
+)
+            """
+        )
+
+        /**
+         * [TestFile] containing ChecksSdkIntAtLeast.java from the annotation library.
+         */
+        val ChecksSdkIntAtLeast = TestFiles.java(
+            """
+package androidx.annotation;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Documented
+@Retention(CLASS)
+@Target({METHOD, FIELD})
+public @interface ChecksSdkIntAtLeast {
+    int api() default -1;
+    String codename() default "";
+    int parameter() default -1;
+    int lambda() default -1;
+}
+            """
+        )
+
+        /* ktlint-enable max-line-length */
+    }
+}
diff --git a/lint-checks/src/test/java/androidx/build/lint/TestSizeAnnotationEnforcerTest.kt b/lint-checks/src/test/java/androidx/build/lint/TestSizeAnnotationEnforcerTest.kt
index 08933de3..fe6ce7d 100644
--- a/lint-checks/src/test/java/androidx/build/lint/TestSizeAnnotationEnforcerTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/TestSizeAnnotationEnforcerTest.kt
@@ -19,7 +19,6 @@
 package androidx.build.lint
 
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
-import com.android.tools.lint.checks.infrastructure.LintDetectorTest.kotlin
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
@@ -53,7 +52,7 @@
                 }
             """
             ).within("src/test"),
-            *Stubs
+            *StubClasses
         )
             .run()
             .expectClean()
@@ -85,7 +84,7 @@
                 }
             """
             ).within("src/test"),
-            *Stubs
+            *StubClasses
         )
             .run()
             .expect(
@@ -114,7 +113,7 @@
                 class Test
             """
             ).within("src/androidTest"),
-            *Stubs
+            *StubClasses
         )
             .run()
             .expect(
@@ -148,7 +147,7 @@
                 }
             """
             ).within("src/androidTest"),
-            *Stubs
+            *StubClasses
         )
             .run()
             .expectClean()
@@ -171,7 +170,7 @@
                 }
             """
             ).within("src/androidTest"),
-            *Stubs
+            *StubClasses
         )
             .run()
             .expectClean()
@@ -218,7 +217,7 @@
                 }
             """
             ).within("src/androidTest"),
-            *Stubs
+            *StubClasses
         )
             .run()
             .expectClean()
@@ -249,7 +248,7 @@
                 }
             """
             ).within("src/androidTest"),
-            *Stubs
+            *StubClasses
         )
             .run()
             .expect(
@@ -287,7 +286,7 @@
                 }
             """
             ).within("src/androidTest"),
-            *Stubs
+            *StubClasses
         )
             .run()
             .expect(
@@ -332,68 +331,18 @@
                 }
             """
             ).within("src/androidTest"),
-            *Stubs
+            *StubClasses
         )
             .run()
             .expectClean()
     }
+
+    private val StubClasses = arrayOf(
+        Stubs.RunWith,
+        Stubs.JUnit4Runner,
+        Stubs.ParameterizedRunner,
+        Stubs.AndroidJUnit4Runner,
+        Stubs.TestSizeAnnotations,
+        Stubs.TestAnnotation
+    )
 }
-
-private val RunWith = kotlin(
-    """
-        package org.junit.runner
-
-        annotation class RunWith(val value: KClass<*>)
-    """
-)
-
-private val JUnit4Runner = kotlin(
-    """
-        package org.junit.runners
-
-        class JUnit4
-    """
-)
-
-private val ParameterizedRunner = kotlin(
-    """
-        package org.junit.runners
-
-        class Parameterized
-    """
-)
-
-private val AndroidJUnit4Runner = kotlin(
-    """
-        package androidx.test.ext.junit.runners
-
-        class AndroidJUnit4
-    """
-)
-
-private val TestSizeAnnotations = kotlin(
-    """
-        package androidx.test.filters
-
-        annotation class SmallTest
-        annotation class MediumTest
-        annotation class LargeTest
-    """
-)
-
-private val TestAnnotation = kotlin(
-    """
-        package org.junit
-
-        annotation class Test
-    """
-)
-
-private val Stubs = arrayOf(
-    RunWith,
-    JUnit4Runner,
-    ParameterizedRunner,
-    AndroidJUnit4Runner,
-    TestSizeAnnotations,
-    TestAnnotation
-)
diff --git a/lint-checks/src/test/java/androidx/build/lint/TestUtils.kt b/lint-checks/src/test/java/androidx/build/lint/TestUtils.kt
deleted file mode 100644
index b863b8b..0000000
--- a/lint-checks/src/test/java/androidx/build/lint/TestUtils.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * 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.
- */
-
-@file:Suppress("UnstableApiUsage")
-
-package androidx.build.lint
-
-import com.android.tools.lint.checks.infrastructure.ProjectDescription
-import com.android.tools.lint.checks.infrastructure.TestFile
-import com.android.tools.lint.checks.infrastructure.TestFiles
-import java.io.FileNotFoundException
-
-private class TestUtils
-
-fun project(): ProjectDescription = ProjectDescription()
-
-/**
- * Loads a [TestFile] from Java source code included in the JAR resources.
- */
-fun javaSample(className: String): TestFile = TestFiles.java(
-    TestUtils::class.java.getResource(
-        "/java/${className.replace('.', '/')}.java"
-    )?.readText() ?: throw FileNotFoundException(
-        "Could not find Java sources for $className in the integration test project"
-    )
-)
-
-/**
- * Loads a [TestFile] from Kotlin source code included in the JAR resources.
- */
-fun ktSample(className: String): TestFile = TestFiles.kotlin(
-    TestUtils::class.java.getResource(
-        "/java/${className.replace('.', '/')}.kt"
-    )?.readText() ?: throw FileNotFoundException(
-        "Could not find Kotlin sources for $className in the integration test project"
-    )
-)
\ No newline at end of file