Update lint checks to their state on internal master

GlobalLintChecker was added as a prebuilt already on aosp, but we
could make it a source build if we had the source on aosp.

Bug: 264451752
Test: Presubmits
Change-Id: Iea5e5d2adef8c261490e14269cf6737c2a369e6d
diff --git a/tools/lint/framework/Android.bp b/tools/lint/framework/Android.bp
new file mode 100644
index 0000000..30a6daa
--- /dev/null
+++ b/tools/lint/framework/Android.bp
@@ -0,0 +1,64 @@
+// Copyright (C) 2022 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 {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_library_host {
+    name: "AndroidFrameworkLintChecker",
+    srcs: ["checks/src/main/java/**/*.kt"],
+    plugins: ["auto_service_plugin"],
+    libs: [
+        "auto_service_annotations",
+        "lint_api",
+    ],
+    static_libs: [
+        "AndroidCommonLint",
+    ],
+    kotlincflags: ["-Xjvm-default=all"],
+}
+
+java_test_host {
+    name: "AndroidFrameworkLintCheckerTest",
+    srcs: ["checks/src/test/java/**/*.kt"],
+    static_libs: [
+        "AndroidFrameworkLintChecker",
+        "junit",
+        "lint",
+        "lint_tests",
+    ],
+    test_options: {
+        unit_test: true,
+        tradefed_options: [
+            {
+                // lint bundles in some classes that were built with older versions
+                // of libraries, and no longer load. Since tradefed tries to load
+                // all classes in the jar to look for tests, it crashes loading them.
+                // Exclude these classes from tradefed's search.
+                name: "exclude-paths",
+                value: "org/apache",
+            },
+            {
+                name: "exclude-paths",
+                value: "META-INF",
+            },
+        ],
+    },
+}
diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt
new file mode 100644
index 0000000..b4f6a1c
--- /dev/null
+++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 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 com.google.android.lint
+
+import com.android.tools.lint.client.api.IssueRegistry
+import com.android.tools.lint.client.api.Vendor
+import com.android.tools.lint.detector.api.CURRENT_API
+import com.google.android.lint.parcel.SaferParcelChecker
+import com.google.auto.service.AutoService
+
+@AutoService(IssueRegistry::class)
+@Suppress("UnstableApiUsage")
+class AndroidFrameworkIssueRegistry : IssueRegistry() {
+    override val issues = listOf(
+        CallingIdentityTokenDetector.ISSUE_UNUSED_TOKEN,
+        CallingIdentityTokenDetector.ISSUE_NON_FINAL_TOKEN,
+        CallingIdentityTokenDetector.ISSUE_NESTED_CLEAR_IDENTITY_CALLS,
+        CallingIdentityTokenDetector.ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK,
+        CallingIdentityTokenDetector.ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY,
+        CallingIdentityTokenDetector.ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY,
+        CallingIdentityTokenDetector.ISSUE_RESULT_OF_CLEAR_IDENTITY_CALL_NOT_STORED_IN_VARIABLE,
+        CallingSettingsNonUserGetterMethodsDetector.ISSUE_NON_USER_GETTER_CALLED,
+        SaferParcelChecker.ISSUE_UNSAFE_API_USAGE,
+        PackageVisibilityDetector.ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS,
+        PermissionMethodDetector.ISSUE_PERMISSION_METHOD_USAGE,
+        PermissionMethodDetector.ISSUE_CAN_BE_PERMISSION_METHOD,
+    )
+
+    override val api: Int
+        get() = CURRENT_API
+
+    override val minApi: Int
+        get() = 8
+
+    override val vendor: Vendor = Vendor(
+        vendorName = "Android",
+        feedbackUrl = "http://b/issues/new?component=315013",
+        contact = "brufino@google.com"
+    )
+}
diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt
new file mode 100644
index 0000000..0c375c3
--- /dev/null
+++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingIdentityTokenDetector.kt
@@ -0,0 +1,648 @@
+/*
+ * Copyright (C) 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 com.google.android.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Context
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Location
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.search.PsiSearchScopeUtil
+import com.intellij.psi.search.SearchScope
+import org.jetbrains.uast.UBlockExpression
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UDeclarationsExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UIfExpression
+import org.jetbrains.uast.ULocalVariable
+import org.jetbrains.uast.USimpleNameReferenceExpression
+import org.jetbrains.uast.UTryExpression
+import org.jetbrains.uast.getParentOfType
+import org.jetbrains.uast.getQualifiedParentOrThis
+import org.jetbrains.uast.getUCallExpression
+import org.jetbrains.uast.skipParenthesizedExprDown
+import org.jetbrains.uast.skipParenthesizedExprUp
+
+/**
+ * Lint Detector that finds issues with improper usages of the token returned by
+ * Binder.clearCallingIdentity()
+ */
+@Suppress("UnstableApiUsage")
+class CallingIdentityTokenDetector : Detector(), SourceCodeScanner {
+    /** Map of <Token variable name, Token object> */
+    private val tokensMap = mutableMapOf<String, Token>()
+
+    override fun getApplicableUastTypes(): List<Class<out UElement?>> =
+        listOf(ULocalVariable::class.java, UCallExpression::class.java)
+
+    override fun createUastHandler(context: JavaContext): UElementHandler =
+        TokenUastHandler(context)
+
+    /** File analysis starts with a clear map */
+    override fun beforeCheckFile(context: Context) {
+        tokensMap.clear()
+    }
+
+    /**
+     * - If tokensMap has tokens after checking the file -> reports all locations as unused token
+     * issue incidents
+     * - File analysis ends with a clear map
+     */
+    override fun afterCheckFile(context: Context) {
+        for (token in tokensMap.values) {
+            context.report(
+                ISSUE_UNUSED_TOKEN,
+                token.location,
+                getIncidentMessageUnusedToken(token.variableName)
+            )
+        }
+        tokensMap.clear()
+    }
+
+    /** UAST handler that analyses elements and reports incidents */
+    private inner class TokenUastHandler(val context: JavaContext) : UElementHandler() {
+        /**
+         * For every variable initialization with Binder.clearCallingIdentity():
+         * - Checks for non-final token issue
+         * - Checks for unused token issue within different scopes
+         * - Checks for nested calls of clearCallingIdentity() issue
+         * - Checks for clearCallingIdentity() not followed by try-finally issue
+         * - Stores token variable name, scope in the file, location and finally block in tokensMap
+         */
+        override fun visitLocalVariable(node: ULocalVariable) {
+            val initializer = node.uastInitializer?.skipParenthesizedExprDown()
+            val rhsExpression = initializer?.getUCallExpression() ?: return
+            if (!isMethodCall(rhsExpression, Method.BINDER_CLEAR_CALLING_IDENTITY)) return
+            val location = context.getLocation(node as UElement)
+            val variableName = node.getName()
+            if (!node.isFinal) {
+                context.report(
+                    ISSUE_NON_FINAL_TOKEN,
+                    location,
+                    getIncidentMessageNonFinalToken(variableName)
+                )
+            }
+            // If there exists an unused variable with the same name in the map, we can imply that
+            // we left the scope of the previous declaration, so we need to report the unused token
+            val oldToken = tokensMap[variableName]
+            if (oldToken != null) {
+                context.report(
+                    ISSUE_UNUSED_TOKEN,
+                    oldToken.location,
+                    getIncidentMessageUnusedToken(oldToken.variableName)
+                )
+            }
+            // If there exists a token in the same scope as the current new token, it means that
+            // clearCallingIdentity() has been called at least twice without immediate restoration
+            // of identity, so we need to report the nested call of clearCallingIdentity()
+            val firstCallToken = findFirstTokenInScope(node)
+            if (firstCallToken != null) {
+                context.report(
+                    ISSUE_NESTED_CLEAR_IDENTITY_CALLS,
+                    createNestedLocation(firstCallToken, location),
+                    getIncidentMessageNestedClearIdentityCallsPrimary(
+                        firstCallToken.variableName,
+                        variableName
+                    )
+                )
+            }
+            // If the next statement in the tree is not a try-finally statement, we need to report
+            // the "clearCallingIdentity() is not followed by try-finally" issue
+            val finallyClause = (getNextStatementOfLocalVariable(node) as? UTryExpression)
+                ?.finallyClause
+            if (finallyClause == null) {
+                context.report(
+                    ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY,
+                    location,
+                    getIncidentMessageClearIdentityCallNotFollowedByTryFinally(variableName)
+                )
+            }
+            tokensMap[variableName] = Token(
+                variableName,
+                node.sourcePsi?.getUseScope(),
+                location,
+                finallyClause
+            )
+        }
+
+        override fun visitCallExpression(node: UCallExpression) {
+            when {
+                isMethodCall(node, Method.BINDER_CLEAR_CALLING_IDENTITY) -> {
+                    checkClearCallingIdentityCall(node)
+                }
+                isMethodCall(node, Method.BINDER_RESTORE_CALLING_IDENTITY) -> {
+                    checkRestoreCallingIdentityCall(node)
+                }
+                isCallerAwareMethod(node) -> checkCallerAwareMethod(node)
+            }
+        }
+
+        private fun checkClearCallingIdentityCall(node: UCallExpression) {
+            var firstNonQualifiedParent = getFirstNonQualifiedParent(node)
+            // if the call expression is inside a ternary, and the ternary is assigned
+            // to a variable, then we are still technically assigning
+            // any result of clearCallingIdentity to a variable
+            if (firstNonQualifiedParent is UIfExpression && firstNonQualifiedParent.isTernary) {
+                firstNonQualifiedParent = firstNonQualifiedParent.uastParent
+            }
+            if (firstNonQualifiedParent !is ULocalVariable) {
+                context.report(
+                    ISSUE_RESULT_OF_CLEAR_IDENTITY_CALL_NOT_STORED_IN_VARIABLE,
+                    context.getLocation(node),
+                    getIncidentMessageResultOfClearIdentityCallNotStoredInVariable(
+                        node.getQualifiedParentOrThis().asRenderString()
+                    )
+                )
+            }
+        }
+
+        private fun checkCallerAwareMethod(node: UCallExpression) {
+            val token = findFirstTokenInScope(node)
+            if (token != null) {
+                context.report(
+                    ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY,
+                    context.getLocation(node),
+                    getIncidentMessageUseOfCallerAwareMethodsWithClearedIdentity(
+                        token.variableName,
+                        node.asRenderString()
+                    )
+                )
+            }
+        }
+
+        /**
+         * - Checks for restoreCallingIdentity() not in the finally block issue
+         * - Removes token from tokensMap if token is within the scope of the method
+         */
+        private fun checkRestoreCallingIdentityCall(node: UCallExpression) {
+            val arg = node.valueArguments[0] as? USimpleNameReferenceExpression ?: return
+            val variableName = arg.identifier
+            val originalScope = tokensMap[variableName]?.scope ?: return
+            val psi = arg.sourcePsi ?: return
+            // Checks if Binder.restoreCallingIdentity(token) is called within the scope of the
+            // token declaration. If not within the scope, no action is needed because the token is
+            // irrelevant i.e. not in the same scope or was not declared with clearCallingIdentity()
+            if (!PsiSearchScopeUtil.isInScope(originalScope, psi)) return
+            // We do not report "restore identity call not in finally" issue when there is no
+            // finally block because that case is already handled by "clear identity call not
+            // followed by try-finally" issue
+            if (tokensMap[variableName]?.finallyBlock != null &&
+                getFirstNonQualifiedParent(node) !=
+                tokensMap[variableName]?.finallyBlock
+            ) {
+                context.report(
+                    ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK,
+                    context.getLocation(node),
+                    getIncidentMessageRestoreIdentityCallNotInFinallyBlock(variableName)
+                )
+            }
+            tokensMap.remove(variableName)
+        }
+
+        private fun getFirstNonQualifiedParent(expression: UCallExpression): UElement? {
+            // UCallExpression can be a child of UQualifiedReferenceExpression, i.e.
+            // receiver.selector, so to get the call's immediate parent we need to get the topmost
+            // parent qualified reference expression and access its parent
+            return skipParenthesizedExprUp(expression.getQualifiedParentOrThis().uastParent)
+        }
+
+        private fun isCallerAwareMethod(expression: UCallExpression): Boolean =
+            callerAwareMethods.any { method -> isMethodCall(expression, method) }
+
+        private fun isMethodCall(
+            expression: UCallExpression,
+            method: Method
+        ): Boolean {
+            val psiMethod = expression.resolve() ?: return false
+            return psiMethod.getName() == method.methodName &&
+                context.evaluator.methodMatches(
+                    psiMethod,
+                    method.className,
+                    /* allowInherit */ true,
+                    *method.args
+                )
+        }
+
+        /**
+         * ULocalVariable in the file tree:
+         *
+         * UBlockExpression
+         *     UDeclarationsExpression
+         *         ULocalVariable
+         *         ULocalVariable
+         *     UTryStatement
+         *     etc.
+         *
+         * To get the next statement of ULocalVariable:
+         * - If there exists a next sibling in UDeclarationsExpression, return the sibling
+         * - If there exists a next sibling of UDeclarationsExpression in UBlockExpression, return
+         *   the sibling
+         * - Otherwise, return null
+         *
+         * Example 1 - the next sibling is in UDeclarationsExpression:
+         * Code:
+         * {
+         *     int num1 = 0, num2 = methodThatThrowsException();
+         * }
+         * Returns: num2 = methodThatThrowsException()
+         *
+         * Example 2 - the next sibling is in UBlockExpression:
+         * Code:
+         * {
+         *     int num1 = 0;
+         *     methodThatThrowsException();
+         * }
+         * Returns: methodThatThrowsException()
+         *
+         * Example 3 - no next sibling;
+         * Code:
+         * {
+         *     int num1 = 0;
+         * }
+         * Returns: null
+         */
+        private fun getNextStatementOfLocalVariable(node: ULocalVariable): UElement? {
+            val declarationsExpression = node.uastParent as? UDeclarationsExpression ?: return null
+            val declarations = declarationsExpression.declarations
+            val indexInDeclarations = declarations.indexOf(node)
+            if (indexInDeclarations != -1 && declarations.size > indexInDeclarations + 1) {
+                return declarations[indexInDeclarations + 1]
+            }
+            val enclosingBlock = node
+                .getParentOfType<UBlockExpression>(strict = true) ?: return null
+            val expressions = enclosingBlock.expressions
+            val indexInBlock = expressions.indexOf(declarationsExpression as UElement)
+            return if (indexInBlock == -1) null else expressions.getOrNull(indexInBlock + 1)
+        }
+    }
+
+    private fun findFirstTokenInScope(node: UElement): Token? {
+        val psi = node.sourcePsi ?: return null
+        for (token in tokensMap.values) {
+            if (token.scope != null && PsiSearchScopeUtil.isInScope(token.scope, psi)) {
+                return token
+            }
+        }
+        return null
+    }
+
+    /**
+     * Creates a new instance of the primary location with the secondary location
+     *
+     * Here, secondary location is the helper location that shows where the issue originated
+     *
+     * The detector reports locations as objects, so when we add a secondary location to a location
+     * that has multiple issues, the secondary location gets displayed every time a location is
+     * referenced.
+     *
+     * Example:
+     * 1: final long token1 = Binder.clearCallingIdentity();
+     * 2: long token2 = Binder.clearCallingIdentity();
+     * 3: Binder.restoreCallingIdentity(token1);
+     * 4: Binder.restoreCallingIdentity(token2);
+     *
+     * Explanation:
+     * token2 has 2 issues: NonFinal and NestedCalls
+     *
+     *     Lint report without cloning                        Lint report with cloning
+     * line 2: [NonFinalIssue]                            line 2: [NonFinalIssue]
+     *     line 1: [NestedCallsIssue]
+     * line 2: [NestedCallsIssue]                            line 2: [NestedCallsIssue]
+     *     line 1: [NestedCallsIssue]                           line 1: [NestedCallsIssue]
+     */
+    private fun createNestedLocation(
+        firstCallToken: Token,
+        secondCallTokenLocation: Location
+    ): Location {
+        return cloneLocation(secondCallTokenLocation)
+            .withSecondary(
+                cloneLocation(firstCallToken.location),
+                getIncidentMessageNestedClearIdentityCallsSecondary(
+                    firstCallToken.variableName
+                )
+            )
+    }
+
+    private fun cloneLocation(location: Location): Location {
+        // smart cast of location.start to 'Position' is impossible, because 'location.start' is a
+        // public API property declared in different module
+        val locationStart = location.start
+        return if (locationStart == null) {
+            Location.create(location.file)
+        } else {
+            Location.create(location.file, locationStart, location.end)
+        }
+    }
+
+    private enum class Method(
+        val className: String,
+        val methodName: String,
+        val args: Array<String>
+    ) {
+        BINDER_CLEAR_CALLING_IDENTITY(CLASS_BINDER, "clearCallingIdentity", emptyArray()),
+        BINDER_RESTORE_CALLING_IDENTITY(CLASS_BINDER, "restoreCallingIdentity", arrayOf("long")),
+        BINDER_GET_CALLING_PID(CLASS_BINDER, "getCallingPid", emptyArray()),
+        BINDER_GET_CALLING_UID(CLASS_BINDER, "getCallingUid", emptyArray()),
+        BINDER_GET_CALLING_UID_OR_THROW(CLASS_BINDER, "getCallingUidOrThrow", emptyArray()),
+        BINDER_GET_CALLING_USER_HANDLE(CLASS_BINDER, "getCallingUserHandle", emptyArray()),
+        USER_HANDLE_GET_CALLING_APP_ID(CLASS_USER_HANDLE, "getCallingAppId", emptyArray()),
+        USER_HANDLE_GET_CALLING_USER_ID(CLASS_USER_HANDLE, "getCallingUserId", emptyArray())
+    }
+
+    private data class Token(
+        val variableName: String,
+        val scope: SearchScope?,
+        val location: Location,
+        val finallyBlock: UElement?
+    )
+
+    companion object {
+        const val CLASS_BINDER = "android.os.Binder"
+        const val CLASS_USER_HANDLE = "android.os.UserHandle"
+
+        private val callerAwareMethods = listOf(
+            Method.BINDER_GET_CALLING_PID,
+            Method.BINDER_GET_CALLING_UID,
+            Method.BINDER_GET_CALLING_UID_OR_THROW,
+            Method.BINDER_GET_CALLING_USER_HANDLE,
+            Method.USER_HANDLE_GET_CALLING_APP_ID,
+            Method.USER_HANDLE_GET_CALLING_USER_ID
+        )
+
+        /** Issue: unused token from Binder.clearCallingIdentity() */
+        @JvmField
+        val ISSUE_UNUSED_TOKEN: Issue = Issue.create(
+            id = "UnusedTokenOfOriginalCallingIdentity",
+            briefDescription = "Unused token of Binder.clearCallingIdentity()",
+            explanation = """
+                    You cleared the original calling identity with \
+                    `Binder.clearCallingIdentity()`, but have not used the returned token to \
+                    restore the identity.
+
+                    Call `Binder.restoreCallingIdentity(token)` in the `finally` block, at the end \
+                    of the method or when you need to restore the identity.
+
+                    `token` is the result of `Binder.clearCallingIdentity()`
+                    """,
+            category = Category.SECURITY,
+            priority = 6,
+            severity = Severity.WARNING,
+            implementation = Implementation(
+                CallingIdentityTokenDetector::class.java,
+                Scope.JAVA_FILE_SCOPE
+            )
+        )
+
+        private fun getIncidentMessageUnusedToken(variableName: String) = "`$variableName` has " +
+            "not been used to restore the calling identity. Introduce a `try`-`finally` " +
+            "after the declaration and call `Binder.restoreCallingIdentity($variableName)` " +
+            "in `finally` or remove `$variableName`."
+
+        /** Issue: non-final token from Binder.clearCallingIdentity() */
+        @JvmField
+        val ISSUE_NON_FINAL_TOKEN: Issue = Issue.create(
+            id = "NonFinalTokenOfOriginalCallingIdentity",
+            briefDescription = "Non-final token of Binder.clearCallingIdentity()",
+            explanation = """
+                    You cleared the original calling identity with \
+                    `Binder.clearCallingIdentity()`, but have not made the returned token `final`.
+
+                    The token should be `final` in order to prevent it from being overwritten, \
+                    which can cause problems when restoring the identity with \
+                    `Binder.restoreCallingIdentity(token)`.
+                    """,
+            category = Category.SECURITY,
+            priority = 6,
+            severity = Severity.WARNING,
+            implementation = Implementation(
+                CallingIdentityTokenDetector::class.java,
+                Scope.JAVA_FILE_SCOPE
+            )
+        )
+
+        private fun getIncidentMessageNonFinalToken(variableName: String) = "`$variableName` is " +
+            "a non-final token from `Binder.clearCallingIdentity()`. Add `final` keyword to " +
+            "`$variableName`."
+
+        /** Issue: nested calls of Binder.clearCallingIdentity() */
+        @JvmField
+        val ISSUE_NESTED_CLEAR_IDENTITY_CALLS: Issue = Issue.create(
+            id = "NestedClearCallingIdentityCalls",
+            briefDescription = "Nested calls of Binder.clearCallingIdentity()",
+            explanation = """
+                    You cleared the original calling identity with \
+                    `Binder.clearCallingIdentity()` twice without restoring identity with the \
+                    result of the first call.
+
+                    Make sure to restore the identity after each clear identity call.
+                    """,
+            category = Category.SECURITY,
+            priority = 6,
+            severity = Severity.WARNING,
+            implementation = Implementation(
+                CallingIdentityTokenDetector::class.java,
+                Scope.JAVA_FILE_SCOPE
+            )
+        )
+
+        private fun getIncidentMessageNestedClearIdentityCallsPrimary(
+            firstCallVariableName: String,
+            secondCallVariableName: String
+        ): String = "The calling identity has already been cleared and returned into " +
+            "`$firstCallVariableName`. Move `$secondCallVariableName` declaration after " +
+            "restoring the calling identity with " +
+            "`Binder.restoreCallingIdentity($firstCallVariableName)`."
+
+        private fun getIncidentMessageNestedClearIdentityCallsSecondary(
+            firstCallVariableName: String
+        ): String = "Location of the `$firstCallVariableName` declaration."
+
+        /** Issue: Binder.clearCallingIdentity() is not followed by `try-finally` statement */
+        @JvmField
+        val ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY: Issue = Issue.create(
+            id = "ClearIdentityCallNotFollowedByTryFinally",
+            briefDescription = "Binder.clearCallingIdentity() is not followed by try-finally " +
+                "statement",
+            explanation = """
+                    You cleared the original calling identity with \
+                    `Binder.clearCallingIdentity()`, but the next statement is not a `try` \
+                    statement.
+
+                    Use the following pattern for running operations with your own identity:
+
+                    ```
+                    final long token = Binder.clearCallingIdentity();
+                    try {
+                        // Code using your own identity
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
+                    ```
+
+                    Any calls/operations between `Binder.clearCallingIdentity()` and `try` \
+                    statement risk throwing an exception without doing a safe and unconditional \
+                    restore of the identity with `Binder.restoreCallingIdentity()` as an immediate \
+                    child of the `finally` block. If you do not follow the pattern, you may run \
+                    code with your identity that was originally intended to run with the calling \
+                    application's identity.
+                    """,
+            category = Category.SECURITY,
+            priority = 6,
+            severity = Severity.WARNING,
+            implementation = Implementation(
+                CallingIdentityTokenDetector::class.java,
+                Scope.JAVA_FILE_SCOPE
+            )
+        )
+
+        private fun getIncidentMessageClearIdentityCallNotFollowedByTryFinally(
+            variableName: String
+        ): String = "You cleared the calling identity and returned the result into " +
+            "`$variableName`, but the next statement is not a `try`-`finally` statement. " +
+            "Define a `try`-`finally` block after `$variableName` declaration to ensure a " +
+            "safe restore of the calling identity by calling " +
+            "`Binder.restoreCallingIdentity($variableName)` and making it an immediate child " +
+            "of the `finally` block."
+
+        /** Issue: Binder.restoreCallingIdentity() is not in finally block */
+        @JvmField
+        val ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK: Issue = Issue.create(
+            id = "RestoreIdentityCallNotInFinallyBlock",
+            briefDescription = "Binder.restoreCallingIdentity() is not in finally block",
+            explanation = """
+                    You are restoring the original calling identity with \
+                    `Binder.restoreCallingIdentity()`, but the call is not an immediate child of \
+                    the `finally` block of the `try` statement.
+
+                    Use the following pattern for running operations with your own identity:
+
+                    ```
+                    final long token = Binder.clearCallingIdentity();
+                    try {
+                        // Code using your own identity
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
+                    ```
+
+                    If you do not surround the code using your identity with the `try` statement \
+                    and call `Binder.restoreCallingIdentity()` as an immediate child of the \
+                    `finally` block, you may run code with your identity that was originally \
+                    intended to run with the calling application's identity.
+                    """,
+            category = Category.SECURITY,
+            priority = 6,
+            severity = Severity.WARNING,
+            implementation = Implementation(
+                CallingIdentityTokenDetector::class.java,
+                Scope.JAVA_FILE_SCOPE
+            )
+        )
+
+        private fun getIncidentMessageRestoreIdentityCallNotInFinallyBlock(
+            variableName: String
+        ): String = "`Binder.restoreCallingIdentity($variableName)` is not an immediate child of " +
+            "the `finally` block of the try statement after `$variableName` declaration. " +
+            "Surround the call with `finally` block and call it unconditionally."
+
+        /** Issue: Use of caller-aware methods after Binder.clearCallingIdentity() */
+        @JvmField
+        val ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY: Issue = Issue.create(
+            id = "UseOfCallerAwareMethodsWithClearedIdentity",
+            briefDescription = "Use of caller-aware methods after " +
+                "Binder.clearCallingIdentity()",
+            explanation = """
+                    You cleared the original calling identity with \
+                    `Binder.clearCallingIdentity()`, but used one of the methods below before \
+                    restoring the identity. These methods will use your own identity instead of \
+                    the caller's identity, so if this is expected replace them with methods that \
+                    explicitly query your own identity such as `Process.myUid()`, \
+                    `Process.myPid()` and `UserHandle.myUserId()`, otherwise move those methods \
+                    out of the `Binder.clearCallingIdentity()` / `Binder.restoreCallingIdentity()` \
+                    section.
+
+                    ```
+                    Binder.getCallingPid()
+                    Binder.getCallingUid()
+                    Binder.getCallingUidOrThrow()
+                    Binder.getCallingUserHandle()
+                    UserHandle.getCallingAppId()
+                    UserHandle.getCallingUserId()
+                    ```
+                    """,
+            category = Category.SECURITY,
+            priority = 6,
+            severity = Severity.WARNING,
+            implementation = Implementation(
+                CallingIdentityTokenDetector::class.java,
+                Scope.JAVA_FILE_SCOPE
+            )
+        )
+
+        private fun getIncidentMessageUseOfCallerAwareMethodsWithClearedIdentity(
+            variableName: String,
+            methodName: String
+        ): String = "You cleared the original identity with `Binder.clearCallingIdentity()` " +
+            "and returned into `$variableName`, so `$methodName` will be using your own " +
+            "identity instead of the caller's. Either explicitly query your own identity or " +
+            "move it after restoring the identity with " +
+            "`Binder.restoreCallingIdentity($variableName)`."
+
+        /** Issue: Result of Binder.clearCallingIdentity() is not stored in a variable */
+        @JvmField
+        val ISSUE_RESULT_OF_CLEAR_IDENTITY_CALL_NOT_STORED_IN_VARIABLE: Issue = Issue.create(
+            id = "ResultOfClearIdentityCallNotStoredInVariable",
+            briefDescription = "Result of Binder.clearCallingIdentity() is not stored in a " +
+                "variable",
+            explanation = """
+           You cleared the original calling identity with \
+           `Binder.clearCallingIdentity()`, but did not store the result of the method \
+           call in a variable. You need to store the result in a variable and restore it later.
+
+           Use the following pattern for running operations with your own identity:
+
+           ```
+           final long token = Binder.clearCallingIdentity();
+           try {
+               // Code using your own identity
+           } finally {
+               Binder.restoreCallingIdentity(token);
+           }
+           ```
+           """,
+            category = Category.SECURITY,
+            priority = 6,
+            severity = Severity.WARNING,
+            implementation = Implementation(
+                CallingIdentityTokenDetector::class.java,
+                Scope.JAVA_FILE_SCOPE
+            )
+        )
+
+        private fun getIncidentMessageResultOfClearIdentityCallNotStoredInVariable(
+            methodName: String
+        ): String = "You cleared the original identity with `$methodName` but did not store the " +
+            "result in a variable. You need to store the result in a variable and restore it " +
+            "later."
+    }
+}
diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsDetector.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsDetector.kt
new file mode 100644
index 0000000..fe567da
--- /dev/null
+++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsDetector.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 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 com.google.android.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
+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.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+
+/**
+ * Lint Detector that finds issues with improper usages of the non-user getter methods of Settings
+ */
+@Suppress("UnstableApiUsage")
+class CallingSettingsNonUserGetterMethodsDetector : Detector(), SourceCodeScanner {
+    override fun getApplicableMethodNames(): List<String> = listOf(
+            "getString",
+            "getInt",
+            "getLong",
+            "getFloat"
+    )
+
+    override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+        val evaluator = context.evaluator
+        if (evaluator.isMemberInClass(method, "android.provider.Settings.Secure") ||
+                evaluator.isMemberInClass(method, "android.provider.Settings.System")
+        ) {
+            val message = getIncidentMessageNonUserGetterMethods(getMethodSignature(method))
+            context.report(ISSUE_NON_USER_GETTER_CALLED, node, context.getNameLocation(node),
+                    message)
+        }
+    }
+
+    private fun getMethodSignature(method: PsiMethod) =
+            method.containingClass
+                    ?.qualifiedName
+                    ?.let { "$it#${method.name}" }
+                    ?: method.name
+
+    companion object {
+        @JvmField
+        val ISSUE_NON_USER_GETTER_CALLED: Issue = Issue.create(
+                id = "NonUserGetterCalled",
+                briefDescription = "Non-ForUser Getter Method called to Settings",
+                explanation = """
+                    System process should not call the non-ForUser getter methods of \
+                    `Settings.Secure` or `Settings.System`. For example, instead of \
+                    `Settings.Secure.getInt()`, use `Settings.Secure.getIntForUser()` instead. \
+                    This will make sure that the correct Settings value is retrieved.
+                    """,
+                category = Category.CORRECTNESS,
+                priority = 6,
+                severity = Severity.ERROR,
+                implementation = Implementation(
+                        CallingSettingsNonUserGetterMethodsDetector::class.java,
+                        Scope.JAVA_FILE_SCOPE
+                )
+        )
+
+        fun getIncidentMessageNonUserGetterMethods(methodSignature: String) =
+                "`$methodSignature()` called from system process. " +
+                        "Please call `${methodSignature}ForUser()` instead. "
+    }
+}
diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/PackageVisibilityDetector.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/PackageVisibilityDetector.kt
new file mode 100644
index 0000000..48540b1d
--- /dev/null
+++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/PackageVisibilityDetector.kt
@@ -0,0 +1,515 @@
+/*
+ * Copyright (C) 2022 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 com.google.android.lint
+
+import com.android.tools.lint.client.api.UastParser
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Context
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.android.tools.lint.detector.api.interprocedural.CallGraph
+import com.android.tools.lint.detector.api.interprocedural.CallGraphResult
+import com.android.tools.lint.detector.api.interprocedural.searchForPaths
+import com.intellij.psi.PsiAnonymousClass
+import com.intellij.psi.PsiMethod
+import java.util.LinkedList
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.UParameter
+import org.jetbrains.uast.USimpleNameReferenceExpression
+import org.jetbrains.uast.visitor.AbstractUastVisitor
+
+/**
+ * A lint checker to detect potential package visibility issues for system's APIs. APIs working
+ * in the system_server and taking the package name as a parameter may have chance to reveal
+ * package existence status on the device, and break the
+ * <a href="https://developer.android.com/about/versions/11/privacy/package-visibility">
+ * Package Visibility</a> that we introduced in Android 11.
+ * <p>
+ * Take an example of the API `boolean setFoo(String packageName)`, a malicious app may have chance
+ * to detect package existence state on the device from the result of the API, if there is no
+ * package visibility filtering rule or uid identify checks applying to the parameter of the
+ * package name.
+ */
+class PackageVisibilityDetector : Detector(), SourceCodeScanner {
+
+    // Enables call graph analysis
+    override fun isCallGraphRequired(): Boolean = true
+
+    override fun analyzeCallGraph(
+        context: Context,
+        callGraph: CallGraphResult
+    ) {
+        val systemServerApiNodes = callGraph.callGraph.nodes.filter(::isSystemServerApi)
+        val sinkMethodNodes = callGraph.callGraph.nodes.filter {
+            // TODO(b/228285232): Remove enforce permission sink methods
+            isNodeInList(it, ENFORCE_PERMISSION_METHODS) || isNodeInList(it, APPOPS_METHODS)
+        }
+        val parser = context.client.getUastParser(context.project)
+        analyzeApisContainPackageNameParameters(
+            context, parser, systemServerApiNodes, sinkMethodNodes)
+    }
+
+    /**
+     * Looking for API contains package name parameters, report the lint issue if the API does not
+     * invoke any sink methods.
+     */
+    private fun analyzeApisContainPackageNameParameters(
+        context: Context,
+        parser: UastParser,
+        systemServerApiNodes: List<CallGraph.Node>,
+        sinkMethodNodes: List<CallGraph.Node>
+    ) {
+        for (apiNode in systemServerApiNodes) {
+            val apiMethod = apiNode.getUMethod() ?: continue
+            val pkgNameParamIndexes = apiMethod.uastParameters.mapIndexedNotNull { index, param ->
+                if (Parameter(param) in PACKAGE_NAME_PATTERNS && apiNode.isArgumentInUse(index)) {
+                    index
+                } else {
+                    null
+                }
+            }.takeIf(List<Int>::isNotEmpty) ?: continue
+
+            for (pkgNameParamIndex in pkgNameParamIndexes) {
+                // Trace the call path of the method's argument, pass the lint checks if a sink
+                // method is found
+                if (traceArgumentCallPath(
+                        apiNode, pkgNameParamIndex, PACKAGE_NAME_SINK_METHOD_LIST)) {
+                    continue
+                }
+                // Pass the check if one of the sink methods is invoked
+                if (hasValidPath(
+                        searchForPaths(
+                            sources = listOf(apiNode),
+                            isSink = { it in sinkMethodNodes },
+                            getNeighbors = { node -> node.edges.map { it.node!! } }
+                        )
+                    )
+                ) continue
+
+                // Report issue
+                val reportElement = apiMethod.uastParameters[pkgNameParamIndex] as UElement
+                val location = parser.createLocation(reportElement)
+                context.report(
+                    ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS,
+                    location,
+                    getMsgPackageNameNoPackageVisibilityFilters(apiMethod, pkgNameParamIndex)
+                )
+            }
+        }
+    }
+
+    /**
+     * Returns {@code true} if the method associated with the given node is a system server's
+     * public API that extends from Stub class.
+     */
+    private fun isSystemServerApi(
+        node: CallGraph.Node
+    ): Boolean {
+        val method = node.getUMethod() ?: return false
+        if (!method.hasModifierProperty("public") ||
+            method.uastBody == null ||
+            method.containingClass is PsiAnonymousClass) {
+            return false
+        }
+        val className = method.containingClass?.qualifiedName ?: return false
+        if (!className.startsWith(SYSTEM_PACKAGE_PREFIX)) {
+            return false
+        }
+        return (method.containingClass ?: return false).supers
+            .filter { it.name == CLASS_STUB }
+            .filter { it.qualifiedName !in BYPASS_STUBS }
+            .any { it.findMethodBySignature(method, /* checkBases */ true) != null }
+    }
+
+    /**
+     * Returns {@code true} if the list contains the node of the call graph.
+     */
+    private fun isNodeInList(
+        node: CallGraph.Node,
+        filters: List<Method>
+    ): Boolean {
+        val method = node.getUMethod() ?: return false
+        return Method(method) in filters
+    }
+
+    /**
+     * Trace the call paths of the argument of the method in the start entry. Return {@code true}
+     * if one of methods in the sink call list is invoked.
+     * Take an example of the call path:
+     * foo(packageName) -> a(packageName) -> b(packageName) -> filterAppAccess()
+     * It returns {@code true} if the filterAppAccess() is in the sink call list.
+     */
+    private fun traceArgumentCallPath(
+        apiNode: CallGraph.Node,
+        pkgNameParamIndex: Int,
+        sinkList: List<Method>
+    ): Boolean {
+        val startEntry = TraceEntry(apiNode, pkgNameParamIndex)
+        val traceQueue = LinkedList<TraceEntry>().apply { add(startEntry) }
+        val allVisits = mutableSetOf<TraceEntry>().apply { add(startEntry) }
+        while (!traceQueue.isEmpty()) {
+            val entry = traceQueue.poll()
+            val entryNode = entry.node
+            val entryMethod = entryNode.getUMethod() ?: continue
+            val entryArgumentName = entryMethod.uastParameters[entry.argumentIndex].name
+            for (outEdge in entryNode.edges) {
+                val outNode = outEdge.node ?: continue
+                val outMethod = outNode.getUMethod() ?: continue
+                val outArgumentIndex =
+                    outEdge.call?.findArgumentIndex(
+                        entryArgumentName, outMethod.uastParameters.size)
+                val sinkMethod = findInSinkList(outMethod, sinkList)
+                if (sinkMethod == null) {
+                    if (outArgumentIndex == null) {
+                        // Path is not relevant to the sink method and argument
+                        continue
+                    }
+                    // Path is relevant to the argument, add a new trace entry if never visit before
+                    val newEntry = TraceEntry(outNode, outArgumentIndex)
+                    if (newEntry !in allVisits) {
+                        traceQueue.add(newEntry)
+                        allVisits.add(newEntry)
+                    }
+                    continue
+                }
+                if (sinkMethod.matchArgument && outArgumentIndex == null) {
+                    // The sink call is required to match the argument, but not found
+                    continue
+                }
+                if (sinkMethod.checkCaller &&
+                    entryMethod.isInClearCallingIdentityScope(outEdge.call!!)) {
+                    // The sink call is in the scope of Binder.clearCallingIdentify
+                    continue
+                }
+                // A sink method is matched
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * Returns the UMethod associated with the given node of call graph.
+     */
+    private fun CallGraph.Node.getUMethod(): UMethod? = this.target.element as? UMethod
+
+    /**
+     * Returns the system module name (e.g. com.android.server.pm) of the method of the
+     * call graph node.
+     */
+    private fun CallGraph.Node.getModuleName(): String? {
+        val method = getUMethod() ?: return null
+        val className = method.containingClass?.qualifiedName ?: return null
+        if (!className.startsWith(SYSTEM_PACKAGE_PREFIX)) {
+            return null
+        }
+        val dotPos = className.indexOf(".", SYSTEM_PACKAGE_PREFIX.length)
+        if (dotPos == -1) {
+            return SYSTEM_PACKAGE_PREFIX
+        }
+        return className.substring(0, dotPos)
+    }
+
+    /**
+     * Return {@code true} if the argument in the method's body is in-use.
+     */
+    private fun CallGraph.Node.isArgumentInUse(argIndex: Int): Boolean {
+        val method = getUMethod() ?: return false
+        val argumentName = method.uastParameters[argIndex].name
+        var foundArg = false
+        val methodVisitor = object : AbstractUastVisitor() {
+            override fun visitSimpleNameReferenceExpression(
+                node: USimpleNameReferenceExpression
+            ): Boolean {
+                if (node.identifier == argumentName) {
+                    foundArg = true
+                }
+                return true
+            }
+        }
+        method.uastBody?.accept(methodVisitor)
+        return foundArg
+    }
+
+    /**
+     * Given an argument name, returns the index of argument in the call expression.
+     */
+    private fun UCallExpression.findArgumentIndex(
+        argumentName: String,
+        parameterSize: Int
+    ): Int? {
+        if (valueArgumentCount == 0 || parameterSize == 0) {
+            return null
+        }
+        var match = false
+        val argVisitor = object : AbstractUastVisitor() {
+            override fun visitSimpleNameReferenceExpression(
+                node: USimpleNameReferenceExpression
+            ): Boolean {
+                if (node.identifier == argumentName) {
+                    match = true
+                }
+                return true
+            }
+            override fun visitCallExpression(node: UCallExpression): Boolean {
+                return true
+            }
+        }
+        valueArguments.take(parameterSize).forEachIndexed { index, argument ->
+            argument.accept(argVisitor)
+            if (match) {
+                return index
+            }
+        }
+        return null
+    }
+
+    /**
+     * Given a UMethod, returns a method from the sink method list.
+     */
+    private fun findInSinkList(
+        uMethod: UMethod,
+        sinkCallList: List<Method>
+    ): Method? {
+        return sinkCallList.find {
+            it == Method(uMethod) ||
+                    it == Method(uMethod.containingClass?.qualifiedName ?: "", "*")
+        }
+    }
+
+    /**
+     * Returns {@code true} if the call expression is in the scope of the
+     * Binder.clearCallingIdentify.
+     */
+    private fun UMethod.isInClearCallingIdentityScope(call: UCallExpression): Boolean {
+        var isInScope = false
+        val methodVisitor = object : AbstractUastVisitor() {
+            private var clearCallingIdentity = 0
+            override fun visitCallExpression(node: UCallExpression): Boolean {
+                if (call == node && clearCallingIdentity != 0) {
+                    isInScope = true
+                    return true
+                }
+                val visitMethod = Method(node.resolve() ?: return false)
+                if (visitMethod == METHOD_CLEAR_CALLING_IDENTITY) {
+                    clearCallingIdentity++
+                } else if (visitMethod == METHOD_RESTORE_CALLING_IDENTITY) {
+                    clearCallingIdentity--
+                }
+                return false
+            }
+        }
+        accept(methodVisitor)
+        return isInScope
+    }
+
+    /**
+     * Checks the module name of the start node and the last node that invokes the sink method
+     * (e.g. checkPermission) in a path, returns {@code true} if one of the paths has the same
+     * module name for both nodes.
+     */
+    private fun hasValidPath(paths: Collection<List<CallGraph.Node>>): Boolean {
+        for (pathNodes in paths) {
+            if (pathNodes.size < VALID_CALL_PATH_NODES_SIZE) {
+                continue
+            }
+            val startModule = pathNodes[0].getModuleName() ?: continue
+            val lastCallModule = pathNodes[pathNodes.size - 2].getModuleName() ?: continue
+            if (startModule == lastCallModule) {
+                return true
+            }
+        }
+        return false
+    }
+
+    /**
+     * A data class to represent the method.
+     */
+    private data class Method(
+        val clazz: String,
+        val name: String
+    ) {
+        // Used by traceArgumentCallPath to indicate that the method is required to match the
+        // argument name
+        var matchArgument = true
+
+        // Used by traceArgumentCallPath to indicate that the method is required to check whether
+        // the Binder.clearCallingIdentity is invoked.
+        var checkCaller = false
+
+        constructor(
+            clazz: String,
+            name: String,
+            matchArgument: Boolean = true,
+            checkCaller: Boolean = false
+        ) : this(clazz, name) {
+            this.matchArgument = matchArgument
+            this.checkCaller = checkCaller
+        }
+
+        constructor(
+            method: PsiMethod
+        ) : this(method.containingClass?.qualifiedName ?: "", method.name)
+
+        constructor(
+            method: com.google.android.lint.model.Method
+        ) : this(method.clazz, method.name)
+    }
+
+    /**
+     * A data class to represent the parameter of the method. The parameter name is converted to
+     * lower case letters for comparison.
+     */
+    private data class Parameter private constructor(
+        val typeName: String,
+        val parameterName: String
+    ) {
+        constructor(uParameter: UParameter) : this(
+            uParameter.type.canonicalText,
+            uParameter.name.lowercase()
+        )
+
+        companion object {
+            fun create(typeName: String, parameterName: String) =
+                Parameter(typeName, parameterName.lowercase())
+        }
+    }
+
+    /**
+     * A data class wraps a method node of the call graph and an index that indicates an
+     * argument of the method to record call trace information.
+     */
+    private data class TraceEntry(
+        val node: CallGraph.Node,
+        val argumentIndex: Int
+    )
+
+    companion object {
+        private const val SYSTEM_PACKAGE_PREFIX = "com.android.server."
+        // A valid call path list needs to contain a start node and a sink node
+        private const val VALID_CALL_PATH_NODES_SIZE = 2
+
+        private const val CLASS_STRING = "java.lang.String"
+        private const val CLASS_PACKAGE_MANAGER = "android.content.pm.PackageManager"
+        private const val CLASS_IPACKAGE_MANAGER = "android.content.pm.IPackageManager"
+        private const val CLASS_APPOPS_MANAGER = "android.app.AppOpsManager"
+        private const val CLASS_BINDER = "android.os.Binder"
+        private const val CLASS_PACKAGE_MANAGER_INTERNAL =
+            "android.content.pm.PackageManagerInternal"
+
+        // Patterns of package name parameter
+        private val PACKAGE_NAME_PATTERNS = setOf(
+            Parameter.create(CLASS_STRING, "packageName"),
+            Parameter.create(CLASS_STRING, "callingPackage"),
+            Parameter.create(CLASS_STRING, "callingPackageName"),
+            Parameter.create(CLASS_STRING, "pkgName"),
+            Parameter.create(CLASS_STRING, "callingPkg"),
+            Parameter.create(CLASS_STRING, "pkg")
+        )
+
+        // Package manager APIs
+        private val PACKAGE_NAME_SINK_METHOD_LIST = listOf(
+            Method(CLASS_PACKAGE_MANAGER_INTERNAL, "filterAppAccess", matchArgument = false),
+            Method(CLASS_PACKAGE_MANAGER_INTERNAL, "canQueryPackage"),
+            Method(CLASS_PACKAGE_MANAGER_INTERNAL, "isSameApp"),
+            Method(CLASS_PACKAGE_MANAGER, "*", checkCaller = true),
+            Method(CLASS_IPACKAGE_MANAGER, "*", checkCaller = true),
+            Method(CLASS_PACKAGE_MANAGER, "getPackagesForUid", matchArgument = false),
+            Method(CLASS_IPACKAGE_MANAGER, "getPackagesForUid", matchArgument = false)
+        )
+
+        // AppOps APIs which include uid and package visibility filters checks
+        private val APPOPS_METHODS = listOf(
+            Method(CLASS_APPOPS_MANAGER, "noteOp"),
+            Method(CLASS_APPOPS_MANAGER, "noteOpNoThrow"),
+            Method(CLASS_APPOPS_MANAGER, "noteOperation"),
+            Method(CLASS_APPOPS_MANAGER, "noteProxyOp"),
+            Method(CLASS_APPOPS_MANAGER, "noteProxyOpNoThrow"),
+            Method(CLASS_APPOPS_MANAGER, "startOp"),
+            Method(CLASS_APPOPS_MANAGER, "startOpNoThrow"),
+            Method(CLASS_APPOPS_MANAGER, "FinishOp"),
+            Method(CLASS_APPOPS_MANAGER, "finishProxyOp"),
+            Method(CLASS_APPOPS_MANAGER, "checkPackage")
+        )
+
+        // Enforce permission APIs
+        private val ENFORCE_PERMISSION_METHODS =
+                com.google.android.lint.ENFORCE_PERMISSION_METHODS
+                        .map(PackageVisibilityDetector::Method)
+
+        private val BYPASS_STUBS = listOf(
+            "android.content.pm.IPackageDataObserver.Stub",
+            "android.content.pm.IPackageDeleteObserver.Stub",
+            "android.content.pm.IPackageDeleteObserver2.Stub",
+            "android.content.pm.IPackageInstallObserver2.Stub",
+            "com.android.internal.app.IAppOpsCallback.Stub",
+
+            // TODO(b/228285637): Do not bypass PackageManagerService API
+            "android.content.pm.IPackageManager.Stub",
+            "android.content.pm.IPackageManagerNative.Stub"
+        )
+
+        private val METHOD_CLEAR_CALLING_IDENTITY =
+            Method(CLASS_BINDER, "clearCallingIdentity")
+        private val METHOD_RESTORE_CALLING_IDENTITY =
+            Method(CLASS_BINDER, "restoreCallingIdentity")
+
+        private fun getMsgPackageNameNoPackageVisibilityFilters(
+            method: UMethod,
+            argumentIndex: Int
+        ): String = "Api: ${method.name} contains a package name parameter: " +
+                "${method.uastParameters[argumentIndex].name} does not apply " +
+                "package visibility filtering rules."
+
+        private val EXPLANATION = """
+            APIs working in the system_server and taking the package name as a parameter may have
+            chance to reveal package existence status on the device, and break the package
+            visibility that we introduced in Android 11.
+            (https://developer.android.com/about/versions/11/privacy/package-visibility)
+
+            Take an example of the API `boolean setFoo(String packageName)`, a malicious app may
+            have chance to get package existence state on the device from the result of the API,
+            if there is no package visibility filtering rule or uid identify checks applying to
+            the parameter of the package name.
+
+            To resolve it, you could apply package visibility filtering rules to the package name
+            via PackageManagerInternal.filterAppAccess API, before starting to use the package name.
+            If the parameter is a calling package name, use the PackageManager API such as
+            PackageManager.getPackagesForUid to verify the calling identify.
+            """
+
+        val ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS = Issue.create(
+            id = "ApiMightLeakAppVisibility",
+            briefDescription = "Api takes package name parameter doesn't apply " +
+                    "package visibility filters",
+            explanation = EXPLANATION,
+            category = Category.SECURITY,
+            priority = 1,
+            severity = Severity.WARNING,
+            implementation = Implementation(
+                PackageVisibilityDetector::class.java,
+                Scope.JAVA_FILE_SCOPE
+            )
+        )
+    }
+}
diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt
new file mode 100644
index 0000000..e12ec3d
--- /dev/null
+++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2022 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 com.google.android.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+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.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.android.tools.lint.detector.api.SourceCodeScanner
+import com.android.tools.lint.detector.api.getUMethod
+import com.intellij.psi.PsiType
+import org.jetbrains.uast.UAnnotation
+import org.jetbrains.uast.UBlockExpression
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UExpression
+import org.jetbrains.uast.UIfExpression
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.UQualifiedReferenceExpression
+import org.jetbrains.uast.UReturnExpression
+import org.jetbrains.uast.getContainingUMethod
+
+/**
+ * Stops incorrect usage of {@link PermissionMethod}
+ * TODO: add tests once re-enabled (b/240445172, b/247542171)
+ */
+class PermissionMethodDetector : Detector(), SourceCodeScanner {
+
+    override fun getApplicableUastTypes(): List<Class<out UElement>> =
+        listOf(UAnnotation::class.java, UMethod::class.java)
+
+    override fun createUastHandler(context: JavaContext): UElementHandler =
+        PermissionMethodHandler(context)
+
+    private inner class PermissionMethodHandler(val context: JavaContext) : UElementHandler() {
+        override fun visitMethod(node: UMethod) {
+            if (hasPermissionMethodAnnotation(node)) return
+            if (onlyCallsPermissionMethod(node)) {
+                val location = context.getLocation(node.javaPsi.modifierList)
+                val fix = fix()
+                    .annotate(ANNOTATION_PERMISSION_METHOD)
+                    .range(location)
+                    .autoFix()
+                    .build()
+
+                context.report(
+                    ISSUE_CAN_BE_PERMISSION_METHOD,
+                    location,
+                    "Annotate method with @PermissionMethod",
+                    fix
+                )
+            }
+        }
+
+        override fun visitAnnotation(node: UAnnotation) {
+            if (node.qualifiedName != ANNOTATION_PERMISSION_METHOD) return
+            val method = node.getContainingUMethod() ?: return
+
+            if (!isPermissionMethodReturnType(method)) {
+                context.report(
+                    ISSUE_PERMISSION_METHOD_USAGE,
+                    context.getLocation(node),
+                    """
+                            Methods annotated with `@PermissionMethod` should return `void`, \
+                            `boolean`, or `@PackageManager.PermissionResult int`."
+                    """.trimIndent()
+                )
+            }
+
+            if (method.returnType == PsiType.INT &&
+                method.annotations.none { it.hasQualifiedName(ANNOTATION_PERMISSION_RESULT) }
+            ) {
+                context.report(
+                    ISSUE_PERMISSION_METHOD_USAGE,
+                    context.getLocation(node),
+                    """
+                            Methods annotated with `@PermissionMethod` that return `int` should \
+                            also be annotated with `@PackageManager.PermissionResult.`"
+                    """.trimIndent()
+                )
+            }
+        }
+    }
+
+    companion object {
+
+        private val EXPLANATION_PERMISSION_METHOD_USAGE = """
+            `@PermissionMethod` should annotate methods that ONLY perform permission lookups. \
+            Said methods should return `boolean`, `@PackageManager.PermissionResult int`, or return \
+            `void` and potentially throw `SecurityException`.
+        """.trimIndent()
+
+        @JvmField
+        val ISSUE_PERMISSION_METHOD_USAGE = Issue.create(
+            id = "PermissionMethodUsage",
+            briefDescription = "@PermissionMethod used incorrectly",
+            explanation = EXPLANATION_PERMISSION_METHOD_USAGE,
+            category = Category.CORRECTNESS,
+            priority = 5,
+            severity = Severity.ERROR,
+            implementation = Implementation(
+                PermissionMethodDetector::class.java,
+                Scope.JAVA_FILE_SCOPE
+            ),
+            enabledByDefault = true
+        )
+
+        private val EXPLANATION_CAN_BE_PERMISSION_METHOD = """
+            Methods that only call other methods annotated with @PermissionMethod (and do NOTHING else) can themselves \
+            be annotated with @PermissionMethod.  For example:
+            ```
+            void wrapperHelper() {
+              // Context.enforceCallingPermission is annotated with @PermissionMethod
+              context.enforceCallingPermission(SOME_PERMISSION)
+            }
+            ```
+        """.trimIndent()
+
+        @JvmField
+        val ISSUE_CAN_BE_PERMISSION_METHOD = Issue.create(
+            id = "CanBePermissionMethod",
+            briefDescription = "Method can be annotated with @PermissionMethod",
+            explanation = EXPLANATION_CAN_BE_PERMISSION_METHOD,
+            category = Category.SECURITY,
+            priority = 5,
+            severity = Severity.WARNING,
+            implementation = Implementation(
+                PermissionMethodDetector::class.java,
+                Scope.JAVA_FILE_SCOPE
+            ),
+            enabledByDefault = false
+        )
+
+        private fun isPermissionMethodReturnType(method: UMethod): Boolean =
+            listOf(PsiType.VOID, PsiType.INT, PsiType.BOOLEAN).contains(method.returnType)
+
+        /**
+         * Identifies methods that...
+         * DO call other methods annotated with @PermissionMethod
+         * DO NOT do anything else
+         */
+        private fun onlyCallsPermissionMethod(method: UMethod): Boolean {
+            val body = method.uastBody as? UBlockExpression ?: return false
+            if (body.expressions.isEmpty()) return false
+            for (expression in body.expressions) {
+                when (expression) {
+                    is UQualifiedReferenceExpression -> {
+                        if (!isPermissionMethodCall(expression.selector)) return false
+                    }
+                    is UReturnExpression -> {
+                        if (!isPermissionMethodCall(expression.returnExpression)) return false
+                    }
+                    is UCallExpression -> {
+                        if (!isPermissionMethodCall(expression)) return false
+                    }
+                    is UIfExpression -> {
+                        if (expression.thenExpression !is UReturnExpression) return false
+                        if (!isPermissionMethodCall(expression.condition)) return false
+                    }
+                    else -> return false
+                }
+            }
+            return true
+        }
+
+        private fun isPermissionMethodCall(expression: UExpression?): Boolean {
+            return when (expression) {
+                is UQualifiedReferenceExpression ->
+                    return isPermissionMethodCall(expression.selector)
+                is UCallExpression -> {
+                    val calledMethod = expression.resolve()?.getUMethod() ?: return false
+                    return hasPermissionMethodAnnotation(calledMethod)
+                }
+                else -> false
+            }
+        }
+
+        private fun hasPermissionMethodAnnotation(method: UMethod): Boolean = method.annotations
+                .any { it.hasQualifiedName(ANNOTATION_PERMISSION_METHOD) }
+    }
+}
diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/CallMigrators.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/CallMigrators.kt
new file mode 100644
index 0000000..06c098d
--- /dev/null
+++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/CallMigrators.kt
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2022 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 com.google.android.lint.parcel
+
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.LintFix
+import com.android.tools.lint.detector.api.Location
+import com.intellij.psi.PsiArrayType
+import com.intellij.psi.PsiCallExpression
+import com.intellij.psi.PsiClassType
+import com.intellij.psi.PsiIntersectionType
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiType
+import com.intellij.psi.PsiTypeParameter
+import com.intellij.psi.PsiWildcardType
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UExpression
+import org.jetbrains.uast.UVariable
+
+/**
+ * Subclass this class and override {@link #getBoundingClass} to report an unsafe Parcel API issue
+ * with a fix that migrates towards the new safer API by appending an argument in the form of
+ * {@code com.package.ItemType.class} coming from the result of the overridden method.
+ */
+abstract class CallMigrator(
+        val method: Method,
+        private val rejects: Set<String> = emptySet(),
+) {
+    open fun report(context: JavaContext, call: UCallExpression, method: PsiMethod) {
+        val location = context.getLocation(call)
+        val itemType = filter(getBoundingClass(context, call, method))
+        val fix = (itemType as? PsiClassType)?.let { type ->
+            getParcelFix(location, this.method.name, getArgumentSuffix(type))
+        }
+        val message = "Unsafe `${this.method.className}.${this.method.name}()` API usage"
+        context.report(SaferParcelChecker.ISSUE_UNSAFE_API_USAGE, call, location, message, fix)
+    }
+
+    protected open fun getArgumentSuffix(type: PsiClassType) =
+            ", ${type.rawType().canonicalText}.class"
+
+    protected open fun getBoundingClass(
+            context: JavaContext,
+            call: UCallExpression,
+            method: PsiMethod,
+    ): PsiType? = null
+
+    protected fun getItemType(type: PsiType, container: String): PsiClassType? {
+        val supers = getParentTypes(type).mapNotNull { it as? PsiClassType }
+        val containerType = supers.firstOrNull { it.rawType().canonicalText == container }
+                ?: return null
+        val itemType = containerType.parameters.getOrNull(0) ?: return null
+        // TODO: Expand to other types, see PsiTypeVisitor
+        return when (itemType) {
+            is PsiClassType -> itemType
+            is PsiWildcardType -> itemType.bound as PsiClassType
+            else -> null
+        }
+    }
+
+    /**
+     * Tries to obtain the type expected by the "receiving" end given a certain {@link UElement}.
+     *
+     * This could be an assignment, an argument passed to a method call, to a constructor call, a
+     * type cast, etc. If no receiving end is found, the type of the UExpression itself is returned.
+     */
+    protected fun getReceivingType(expression: UElement): PsiType? {
+        val parent = expression.uastParent
+        var type = when (parent) {
+            is UCallExpression -> {
+                val i = parent.valueArguments.indexOf(expression)
+                val psiCall = parent.sourcePsi as? PsiCallExpression ?: return null
+                val typeSubstitutor = psiCall.resolveMethodGenerics().substitutor
+                val method = psiCall.resolveMethod()!!
+                method.getSignature(typeSubstitutor).parameterTypes[i]
+            }
+            is UVariable -> parent.type
+            is UExpression -> parent.getExpressionType()
+            else -> null
+        }
+        if (type == null && expression is UExpression) {
+            type = expression.getExpressionType()
+        }
+        return type
+    }
+
+    protected fun filter(type: PsiType?): PsiType? {
+        // It's important that PsiIntersectionType case is above the one that check the type in
+        // rejects, because for intersect types, the canonicalText is one of the terms.
+        if (type is PsiIntersectionType) {
+            return type.conjuncts.mapNotNull(this::filter).firstOrNull()
+        }
+        if (type == null || type.canonicalText in rejects) {
+            return null
+        }
+        if (type is PsiClassType && type.resolve() is PsiTypeParameter) {
+            return null
+        }
+        return type
+    }
+
+    private fun getParentTypes(type: PsiType): Set<PsiType> =
+            type.superTypes.flatMap(::getParentTypes).toSet() + type
+
+    protected fun getParcelFix(location: Location, method: String, arguments: String) =
+            LintFix
+                    .create()
+                    .name("Migrate to safer Parcel.$method() API")
+                    .replace()
+                    .range(location)
+                    .pattern("$method\\s*\\(((?:.|\\n)*)\\)")
+                    .with("\\k<1>$arguments")
+                    .autoFix()
+                    .build()
+}
+
+/**
+ * This class derives the type to be appended by inferring the generic type of the {@code container}
+ * type (eg. "java.util.List") of the {@code argument}-th argument.
+ */
+class ContainerArgumentMigrator(
+        method: Method,
+        private val argument: Int,
+        private val container: String,
+        rejects: Set<String> = emptySet(),
+) : CallMigrator(method, rejects) {
+    override fun getBoundingClass(
+            context: JavaContext, call: UCallExpression, method: PsiMethod
+    ): PsiType? {
+        val firstParamType = call.valueArguments[argument].getExpressionType() ?: return null
+        return getItemType(firstParamType, container)!!
+    }
+
+    /**
+     * We need to insert a casting construct in the class parameter. For example:
+     *   (Class<Foo<Bar>>) (Class<?>) Foo.class.
+     * This is needed for when the arguments of the conflict (eg. when there is List<Foo<Bar>> and
+     * class type is Class<Foo?).
+     */
+    override fun getArgumentSuffix(type: PsiClassType): String {
+        if (type.parameters.isNotEmpty()) {
+            val rawType = type.rawType()
+            return ", (Class<${type.canonicalText}>) (Class<?>) ${rawType.canonicalText}.class"
+        }
+        return super.getArgumentSuffix(type)
+    }
+}
+
+/**
+ * This class derives the type to be appended by inferring the generic type of the {@code container}
+ * type (eg. "java.util.List") of the return type of the method.
+ */
+class ContainerReturnMigrator(
+        method: Method,
+        private val container: String,
+        rejects: Set<String> = emptySet(),
+) : CallMigrator(method, rejects) {
+    override fun getBoundingClass(
+            context: JavaContext, call: UCallExpression, method: PsiMethod
+    ): PsiType? {
+        val type = getReceivingType(call.uastParent!!) ?: return null
+        return getItemType(type, container)
+    }
+}
+
+/**
+ * This class derives the type to be appended by inferring the expected type for the method result.
+ */
+class ReturnMigrator(
+        method: Method,
+        rejects: Set<String> = emptySet(),
+) : CallMigrator(method, rejects) {
+    override fun getBoundingClass(
+            context: JavaContext, call: UCallExpression, method: PsiMethod
+    ): PsiType? {
+        return getReceivingType(call.uastParent!!)
+    }
+}
+
+/**
+ * This class appends the class loader and the class object by deriving the type from the method
+ * result.
+ */
+class ReturnMigratorWithClassLoader(
+        method: Method,
+        rejects: Set<String> = emptySet(),
+) : CallMigrator(method, rejects) {
+    override fun getBoundingClass(
+            context: JavaContext, call: UCallExpression, method: PsiMethod
+    ): PsiType? {
+        return getReceivingType(call.uastParent!!)
+    }
+
+    override fun getArgumentSuffix(type: PsiClassType): String =
+            "${type.rawType().canonicalText}.class.getClassLoader(), " +
+                    "${type.rawType().canonicalText}.class"
+
+}
+
+/**
+ * This class derives the type to be appended by inferring the expected array type
+ * for the method result.
+ */
+class ArrayReturnMigrator(
+    method: Method,
+    rejects: Set<String> = emptySet(),
+) : CallMigrator(method, rejects) {
+    override fun getBoundingClass(
+           context: JavaContext, call: UCallExpression, method: PsiMethod
+    ): PsiType? {
+        val type = getReceivingType(call.uastParent!!)
+        return (type as? PsiArrayType)?.componentType
+    }
+}
diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/Method.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/Method.kt
new file mode 100644
index 0000000..0826e8e
--- /dev/null
+++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/Method.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2022 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 com.google.android.lint.parcel
+
+data class Method(
+    val params: List<String>,
+    val clazz: String,
+    val name: String,
+    val parameters: List<String>
+) {
+    constructor(
+        clazz: String,
+        name: String,
+        parameters: List<String>
+    ) : this(
+            listOf(), clazz, name, parameters
+    )
+
+    val signature: String
+        get() {
+            val prefix = if (params.isEmpty()) "" else "${params.joinToString(", ", "<", ">")} "
+            return "$prefix$clazz.$name(${parameters.joinToString()})"
+        }
+
+    val className: String by lazy {
+        clazz.split(".").last()
+    }
+}
diff --git a/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/SaferParcelChecker.kt b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/SaferParcelChecker.kt
new file mode 100644
index 0000000..f928263
--- /dev/null
+++ b/tools/lint/framework/checks/src/main/java/com/google/android/lint/parcel/SaferParcelChecker.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2022 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 com.google.android.lint.parcel
+
+import com.android.tools.lint.detector.api.*
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiSubstitutor
+import com.intellij.psi.PsiType
+import com.intellij.psi.PsiTypeParameter
+import org.jetbrains.uast.UCallExpression
+import java.util.*
+
+@Suppress("UnstableApiUsage")
+class SaferParcelChecker : Detector(), SourceCodeScanner {
+    override fun getApplicableMethodNames(): List<String> =
+            MIGRATORS
+                    .map(CallMigrator::method)
+                    .map(Method::name)
+
+    override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+        if (!isAtLeastT(context)) return
+        val signature = getSignature(method)
+        val migrator = MIGRATORS.firstOrNull { it.method.signature == signature } ?: return
+        migrator.report(context, node, method)
+    }
+
+    private fun getSignature(method: PsiMethod): String {
+        val name = UastLintUtils.getQualifiedName(method)
+        val signature = method.getSignature(PsiSubstitutor.EMPTY)
+        val parameters =
+                signature.parameterTypes.joinToString(transform = PsiType::getCanonicalText)
+        val types = signature.typeParameters.map(PsiTypeParameter::getName)
+        val prefix = if (types.isEmpty()) "" else types.joinToString(", ", "<", ">") + " "
+        return "$prefix$name($parameters)"
+    }
+
+    private fun isAtLeastT(context: Context): Boolean {
+        val project = if (context.isGlobalAnalysis()) context.mainProject else context.project
+        return project.isAndroidProject && project.minSdkVersion.featureLevel >= 33
+    }
+
+    companion object {
+        @JvmField
+        val ISSUE_UNSAFE_API_USAGE: Issue = Issue.create(
+                id = "UnsafeParcelApi",
+                briefDescription = "Use of unsafe deserialization API",
+                explanation = """
+                    You are using a deprecated deserialization API that doesn't accept the expected class as\
+                     a parameter. This means that unexpected classes could be instantiated and\
+                     unexpected code executed.
+
+                    Please migrate to the safer alternative that takes an extra Class<T> parameter.
+                    """,
+                category = Category.SECURITY,
+                priority = 8,
+                severity = Severity.WARNING,
+
+                implementation = Implementation(
+                        SaferParcelChecker::class.java,
+                        Scope.JAVA_FILE_SCOPE
+                )
+        )
+
+        // Parcel
+        private val PARCEL_METHOD_READ_SERIALIZABLE = Method("android.os.Parcel", "readSerializable", listOf())
+        private val PARCEL_METHOD_READ_ARRAY_LIST = Method("android.os.Parcel", "readArrayList", listOf("java.lang.ClassLoader"))
+        private val PARCEL_METHOD_READ_LIST = Method("android.os.Parcel", "readList", listOf("java.util.List", "java.lang.ClassLoader"))
+        private val PARCEL_METHOD_READ_PARCELABLE = Method(listOf("T"), "android.os.Parcel", "readParcelable", listOf("java.lang.ClassLoader"))
+        private val PARCEL_METHOD_READ_PARCELABLE_LIST = Method(listOf("T"), "android.os.Parcel", "readParcelableList", listOf("java.util.List<T>", "java.lang.ClassLoader"))
+        private val PARCEL_METHOD_READ_SPARSE_ARRAY = Method(listOf("T"), "android.os.Parcel", "readSparseArray", listOf("java.lang.ClassLoader"))
+        private val PARCEL_METHOD_READ_ARRAY = Method("android.os.Parcel", "readArray", listOf("java.lang.ClassLoader"))
+        private val PARCEL_METHOD_READ_PARCELABLE_ARRAY = Method("android.os.Parcel", "readParcelableArray", listOf("java.lang.ClassLoader"))
+
+        // Bundle
+        private val BUNDLE_METHOD_GET_SERIALIZABLE = Method("android.os.Bundle", "getSerializable", listOf("java.lang.String"))
+        private val BUNDLE_METHOD_GET_PARCELABLE = Method(listOf("T"), "android.os.Bundle", "getParcelable", listOf("java.lang.String"))
+        private val BUNDLE_METHOD_GET_PARCELABLE_ARRAY_LIST = Method(listOf("T"), "android.os.Bundle", "getParcelableArrayList", listOf("java.lang.String"))
+        private val BUNDLE_METHOD_GET_PARCELABLE_ARRAY = Method("android.os.Bundle", "getParcelableArray", listOf("java.lang.String"))
+        private val BUNDLE_METHOD_GET_SPARSE_PARCELABLE_ARRAY = Method(listOf("T"), "android.os.Bundle", "getSparseParcelableArray", listOf("java.lang.String"))
+
+        // Intent
+        private val INTENT_METHOD_GET_SERIALIZABLE_EXTRA = Method("android.content.Intent", "getSerializableExtra", listOf("java.lang.String"))
+        private val INTENT_METHOD_GET_PARCELABLE_EXTRA = Method(listOf("T"), "android.content.Intent", "getParcelableExtra", listOf("java.lang.String"))
+        private val INTENT_METHOD_GET_PARCELABLE_ARRAY_EXTRA = Method("android.content.Intent", "getParcelableArrayExtra", listOf("java.lang.String"))
+        private val INTENT_METHOD_GET_PARCELABLE_ARRAY_LIST_EXTRA = Method(listOf("T"), "android.content.Intent", "getParcelableArrayListExtra", listOf("java.lang.String"))
+
+        // TODO: Write migrators for methods below
+        private val PARCEL_METHOD_READ_PARCELABLE_CREATOR = Method("android.os.Parcel", "readParcelableCreator", listOf("java.lang.ClassLoader"))
+
+        private val MIGRATORS = listOf(
+            ReturnMigrator(PARCEL_METHOD_READ_PARCELABLE, setOf("android.os.Parcelable")),
+            ContainerArgumentMigrator(PARCEL_METHOD_READ_LIST, 0, "java.util.List"),
+            ContainerReturnMigrator(PARCEL_METHOD_READ_ARRAY_LIST, "java.util.Collection"),
+            ContainerReturnMigrator(PARCEL_METHOD_READ_SPARSE_ARRAY, "android.util.SparseArray"),
+            ContainerArgumentMigrator(PARCEL_METHOD_READ_PARCELABLE_LIST, 0, "java.util.List"),
+            ReturnMigratorWithClassLoader(PARCEL_METHOD_READ_SERIALIZABLE),
+            ArrayReturnMigrator(PARCEL_METHOD_READ_ARRAY, setOf("java.lang.Object")),
+            ArrayReturnMigrator(PARCEL_METHOD_READ_PARCELABLE_ARRAY, setOf("android.os.Parcelable")),
+
+            ReturnMigrator(BUNDLE_METHOD_GET_PARCELABLE, setOf("android.os.Parcelable")),
+            ContainerReturnMigrator(BUNDLE_METHOD_GET_PARCELABLE_ARRAY_LIST, "java.util.Collection", setOf("android.os.Parcelable")),
+            ArrayReturnMigrator(BUNDLE_METHOD_GET_PARCELABLE_ARRAY, setOf("android.os.Parcelable")),
+            ContainerReturnMigrator(BUNDLE_METHOD_GET_SPARSE_PARCELABLE_ARRAY, "android.util.SparseArray", setOf("android.os.Parcelable")),
+            ReturnMigrator(BUNDLE_METHOD_GET_SERIALIZABLE, setOf("java.io.Serializable")),
+
+            ReturnMigrator(INTENT_METHOD_GET_PARCELABLE_EXTRA, setOf("android.os.Parcelable")),
+            ContainerReturnMigrator(INTENT_METHOD_GET_PARCELABLE_ARRAY_LIST_EXTRA, "java.util.Collection", setOf("android.os.Parcelable")),
+            ArrayReturnMigrator(INTENT_METHOD_GET_PARCELABLE_ARRAY_EXTRA, setOf("android.os.Parcelable")),
+            ReturnMigrator(INTENT_METHOD_GET_SERIALIZABLE_EXTRA, setOf("java.io.Serializable")),
+        )
+    }
+}
diff --git a/tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt b/tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt
new file mode 100644
index 0000000..d90f3e3
--- /dev/null
+++ b/tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingIdentityTokenDetectorTest.kt
@@ -0,0 +1,867 @@
+/*
+ * Copyright (C) 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 com.google.android.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+
+@Suppress("UnstableApiUsage")
+class CallingIdentityTokenDetectorTest : LintDetectorTest() {
+    override fun getDetector(): Detector = CallingIdentityTokenDetector()
+
+    override fun getIssues(): List<Issue> = listOf(
+        CallingIdentityTokenDetector.ISSUE_UNUSED_TOKEN,
+        CallingIdentityTokenDetector.ISSUE_NON_FINAL_TOKEN,
+        CallingIdentityTokenDetector.ISSUE_NESTED_CLEAR_IDENTITY_CALLS,
+        CallingIdentityTokenDetector.ISSUE_RESTORE_IDENTITY_CALL_NOT_IN_FINALLY_BLOCK,
+        CallingIdentityTokenDetector.ISSUE_USE_OF_CALLER_AWARE_METHODS_WITH_CLEARED_IDENTITY,
+        CallingIdentityTokenDetector.ISSUE_CLEAR_IDENTITY_CALL_NOT_FOLLOWED_BY_TRY_FINALLY,
+        CallingIdentityTokenDetector.ISSUE_RESULT_OF_CLEAR_IDENTITY_CALL_NOT_STORED_IN_VARIABLE
+    )
+
+    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
+
+    /** No issue scenario */
+
+    fun testDoesNotDetectIssuesInCorrectScenario() {
+        lint().files(
+            java(
+                """
+                    package test.pkg;
+                    import android.os.Binder;
+                    public class TestClass1 extends Binder {
+                        private void testMethod() {
+                            final long token1 = Binder.clearCallingIdentity();
+                            try {
+                            } finally {
+                                Binder.restoreCallingIdentity(token1);
+                            }
+                            final long token2 = android.os.Binder.clearCallingIdentity();
+                            try {
+                            } finally {
+                                android.os.Binder.restoreCallingIdentity(token2);
+                            }
+                            final long token3 = clearCallingIdentity();
+                            try {
+                            } finally {
+                                restoreCallingIdentity(token3);
+                            }
+                            final Long token4 = true ? Binder.clearCallingIdentity() : null;
+                            try {
+                            } finally {
+                                if (token4 != null) {
+                                    restoreCallingIdentity(token4);
+                                }
+                            }
+                        }
+                    }
+                   """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expectClean()
+    }
+
+    /** Unused token issue tests */
+
+    fun testDetectsUnusedTokens() {
+        lint().files(
+            java(
+                """
+                    package test.pkg;
+                    import android.os.Binder;
+                    public class TestClass1 extends Binder {
+                        private void testMethodImported() {
+                            final long token1 = Binder.clearCallingIdentity();
+                            try {
+                            } finally {
+                            }
+                        }
+                        private void testMethodFullClass() {
+                            final long token2 = android.os.Binder.clearCallingIdentity();
+                            try {
+                            } finally {
+                            }
+                        }
+                        private void testMethodChildOfBinder() {
+                            final long token3 = clearCallingIdentity();
+                            try {
+                            } finally {
+                            }
+                        }
+                    }
+                    """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expect(
+                """
+                        src/test/pkg/TestClass1.java:5: Warning: token1 has not been used to \
+                        restore the calling identity. Introduce a try-finally after the \
+                        declaration and call Binder.restoreCallingIdentity(token1) in finally or \
+                        remove token1. [UnusedTokenOfOriginalCallingIdentity]
+                                final long token1 = Binder.clearCallingIdentity();
+                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:11: Warning: token2 has not been used to \
+                        restore the calling identity. Introduce a try-finally after the \
+                        declaration and call Binder.restoreCallingIdentity(token2) in finally or \
+                        remove token2. [UnusedTokenOfOriginalCallingIdentity]
+                                final long token2 = android.os.Binder.clearCallingIdentity();
+                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:17: Warning: token3 has not been used to \
+                        restore the calling identity. Introduce a try-finally after the \
+                        declaration and call Binder.restoreCallingIdentity(token3) in finally or \
+                        remove token3. [UnusedTokenOfOriginalCallingIdentity]
+                                final long token3 = clearCallingIdentity();
+                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        0 errors, 3 warnings
+                        """.addLineContinuation()
+            )
+    }
+
+    fun testDetectsUnusedTokensInScopes() {
+        lint().files(
+            java(
+                """
+                    package test.pkg;
+                    import android.os.Binder;
+                    public class TestClass1 {
+                        private void testMethodTokenFromClearIdentity() {
+                            final long token = Binder.clearCallingIdentity();
+                            try {
+                            } finally {
+                            }
+                        }
+                        private void testMethodTokenNotFromClearIdentity() {
+                            long token = 0;
+                            try {
+                            } finally {
+                                Binder.restoreCallingIdentity(token);
+                            }
+                        }
+                    }
+                    """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expect(
+                """
+                        src/test/pkg/TestClass1.java:5: Warning: token has not been used to \
+                        restore the calling identity. Introduce a try-finally after the \
+                        declaration and call Binder.restoreCallingIdentity(token) in finally or \
+                        remove token. [UnusedTokenOfOriginalCallingIdentity]
+                                final long token = Binder.clearCallingIdentity();
+                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        0 errors, 1 warnings
+                        """.addLineContinuation()
+            )
+    }
+
+    fun testDoesNotDetectUsedTokensInScopes() {
+        lint().files(
+            java(
+                """
+                    package test.pkg;
+                    import android.os.Binder;
+                    public class TestClass1 {
+                        private void testMethodTokenFromClearIdentity() {
+                            final long token = Binder.clearCallingIdentity();
+                            try {
+                            } finally {
+                                Binder.restoreCallingIdentity(token);
+                            }
+                        }
+                        private void testMethodTokenNotFromClearIdentity() {
+                            long token = 0;
+                            try {
+                            } finally {
+                                Binder.restoreCallingIdentity(token);
+                            }
+                        }
+                    }
+                    """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expectClean()
+    }
+
+    fun testDetectsUnusedTokensWithSimilarNamesInScopes() {
+        lint().files(
+            java(
+                """
+                    package test.pkg;
+                    import android.os.Binder;
+                    public class TestClass1 {
+                        private void testMethod1() {
+                            final long token = Binder.clearCallingIdentity();
+                            try {
+                            } finally {
+                            }
+                        }
+                        private void testMethod2() {
+                            final long token = Binder.clearCallingIdentity();
+                            try {
+                            } finally {
+                            }
+                        }
+                    }
+                    """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expect(
+                """
+                        src/test/pkg/TestClass1.java:5: Warning: token has not been used to \
+                        restore the calling identity. Introduce a try-finally after the \
+                        declaration and call Binder.restoreCallingIdentity(token) in finally or \
+                        remove token. [UnusedTokenOfOriginalCallingIdentity]
+                                final long token = Binder.clearCallingIdentity();
+                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:11: Warning: token has not been used to \
+                        restore the calling identity. Introduce a try-finally after the \
+                        declaration and call Binder.restoreCallingIdentity(token) in finally or \
+                        remove token. [UnusedTokenOfOriginalCallingIdentity]
+                                final long token = Binder.clearCallingIdentity();
+                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        0 errors, 2 warnings
+                        """.addLineContinuation()
+            )
+    }
+
+    /** Non-final token issue tests */
+
+    fun testDetectsNonFinalTokens() {
+        lint().files(
+            java(
+                """
+                    package test.pkg;
+                    import android.os.Binder;
+                    public class TestClass1 extends Binder {
+                        private void testMethod() {
+                            long token1 = Binder.clearCallingIdentity();
+                            try {
+                            } finally {
+                                Binder.restoreCallingIdentity(token1);
+                            }
+                            long token2 = android.os.Binder.clearCallingIdentity();
+                            try {
+                            } finally {
+                                android.os.Binder.restoreCallingIdentity(token2);
+                            }
+                            long token3 = clearCallingIdentity();
+                            try {
+                            } finally {
+                                restoreCallingIdentity(token3);
+                            }
+                        }
+                    }
+                    """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expect(
+                """
+                        src/test/pkg/TestClass1.java:5: Warning: token1 is a non-final token from \
+                        Binder.clearCallingIdentity(). Add final keyword to token1. \
+                        [NonFinalTokenOfOriginalCallingIdentity]
+                                long token1 = Binder.clearCallingIdentity();
+                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:10: Warning: token2 is a non-final token from \
+                        Binder.clearCallingIdentity(). Add final keyword to token2. \
+                        [NonFinalTokenOfOriginalCallingIdentity]
+                                long token2 = android.os.Binder.clearCallingIdentity();
+                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:15: Warning: token3 is a non-final token from \
+                        Binder.clearCallingIdentity(). Add final keyword to token3. \
+                        [NonFinalTokenOfOriginalCallingIdentity]
+                                long token3 = clearCallingIdentity();
+                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        0 errors, 3 warnings
+                        """.addLineContinuation()
+            )
+    }
+
+    /** Nested clearCallingIdentity() calls issue tests */
+
+    fun testDetectsNestedClearCallingIdentityCalls() {
+        // Pattern: clear - clear - clear - restore - restore - restore
+        lint().files(
+            java(
+                """
+                    package test.pkg;
+                    import android.os.Binder;
+                    public class TestClass1 extends Binder {
+                        private void testMethod() {
+                            final long token1 = Binder.clearCallingIdentity();
+                            try {
+                                final long token2 = android.os.Binder.clearCallingIdentity();
+                                try {
+                                    final long token3 = clearCallingIdentity();
+                                    try {
+                                    } finally {
+                                        restoreCallingIdentity(token3);
+                                    }
+                                } finally {
+                                    android.os.Binder.restoreCallingIdentity(token2);
+                                }
+                            } finally {
+                                Binder.restoreCallingIdentity(token1);
+                            }
+                        }
+                    }
+                    """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expect(
+                """
+                        src/test/pkg/TestClass1.java:7: Warning: The calling identity has already \
+                        been cleared and returned into token1. Move token2 declaration after \
+                        restoring the calling identity with Binder.restoreCallingIdentity(token1). \
+                        [NestedClearCallingIdentityCalls]
+                                    final long token2 = android.os.Binder.clearCallingIdentity();
+                                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                            src/test/pkg/TestClass1.java:5: Location of the token1 declaration.
+                        src/test/pkg/TestClass1.java:9: Warning: The calling identity has already \
+                        been cleared and returned into token1. Move token3 declaration after \
+                        restoring the calling identity with Binder.restoreCallingIdentity(token1). \
+                        [NestedClearCallingIdentityCalls]
+                                        final long token3 = clearCallingIdentity();
+                                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                            src/test/pkg/TestClass1.java:5: Location of the token1 declaration.
+                        0 errors, 2 warnings
+                        """.addLineContinuation()
+            )
+    }
+
+    /** clearCallingIdentity() not followed by try-finally issue tests */
+
+    fun testDetectsClearIdentityCallNotFollowedByTryFinally() {
+        lint().files(
+            java(
+                """
+                    package test.pkg;
+                    import android.os.Binder;
+                    public class TestClass1 extends Binder{
+                        private void testMethodNoTry() {
+                            final long token = Binder.clearCallingIdentity();
+                            Binder.restoreCallingIdentity(token);
+                        }
+                        private void testMethodSomethingBetweenClearAndTry() {
+                            final long token = Binder.clearCallingIdentity();
+                            int pid = 0;
+                            try {
+                            } finally {
+                                Binder.restoreCallingIdentity(token);
+                            }
+                        }
+                        private void testMethodLocalVariableBetweenClearAndTry() {
+                            final long token = clearCallingIdentity(), num = 0;
+                            try {
+                            } finally {
+                                restoreCallingIdentity(token);
+                            }
+                        }
+                        private void testMethodTryCatch() {
+                            final long token = android.os.Binder.clearCallingIdentity();
+                            try {
+                            } catch (Exception e) {
+                            }
+                            Binder.restoreCallingIdentity(token);
+                        }
+                        private void testMethodTryCatchInScopes() {
+                            final long token = android.os.Binder.clearCallingIdentity();
+                            {
+                                try {
+                                } catch (Exception e) {
+                                }
+                            }
+                            Binder.restoreCallingIdentity(token);
+                        }
+                    }
+                    """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expect(
+                """
+                        src/test/pkg/TestClass1.java:5: Warning: You cleared the calling identity \
+                        and returned the result into token, but the next statement is not a \
+                        try-finally statement. Define a try-finally block after token declaration \
+                        to ensure a safe restore of the calling identity by calling \
+                        Binder.restoreCallingIdentity(token) and making it an immediate child of \
+                        the finally block. [ClearIdentityCallNotFollowedByTryFinally]
+                                final long token = Binder.clearCallingIdentity();
+                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:9: Warning: You cleared the calling identity \
+                        and returned the result into token, but the next statement is not a \
+                        try-finally statement. Define a try-finally block after token declaration \
+                        to ensure a safe restore of the calling identity by calling \
+                        Binder.restoreCallingIdentity(token) and making it an immediate child of \
+                        the finally block. [ClearIdentityCallNotFollowedByTryFinally]
+                                final long token = Binder.clearCallingIdentity();
+                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:17: Warning: You cleared the calling identity \
+                        and returned the result into token, but the next statement is not a \
+                        try-finally statement. Define a try-finally block after token declaration \
+                        to ensure a safe restore of the calling identity by calling \
+                        Binder.restoreCallingIdentity(token) and making it an immediate child of \
+                        the finally block. [ClearIdentityCallNotFollowedByTryFinally]
+                                final long token = clearCallingIdentity(), num = 0;
+                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:24: Warning: You cleared the calling identity \
+                        and returned the result into token, but the next statement is not a \
+                        try-finally statement. Define a try-finally block after token declaration \
+                        to ensure a safe restore of the calling identity by calling \
+                        Binder.restoreCallingIdentity(token) and making it an immediate child of \
+                        the finally block. [ClearIdentityCallNotFollowedByTryFinally]
+                                final long token = android.os.Binder.clearCallingIdentity();
+                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:31: Warning: You cleared the calling identity \
+                        and returned the result into token, but the next statement is not a \
+                        try-finally statement. Define a try-finally block after token declaration \
+                        to ensure a safe restore of the calling identity by calling \
+                        Binder.restoreCallingIdentity(token) and making it an immediate child of \
+                        the finally block. [ClearIdentityCallNotFollowedByTryFinally]
+                                final long token = android.os.Binder.clearCallingIdentity();
+                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        0 errors, 5 warnings
+                        """.addLineContinuation()
+            )
+    }
+
+    /** restoreCallingIdentity() call not in finally block issue tests */
+
+    fun testDetectsRestoreCallingIdentityCallNotInFinally() {
+        lint().files(
+            java(
+                """
+                    package test.pkg;
+                    import android.os.Binder;
+                    public class TestClass1 extends Binder {
+                        private void testMethodImported() {
+                            final long token = Binder.clearCallingIdentity();
+                            try {
+                            } catch (Exception e) {
+                            } finally {
+                            }
+                            Binder.restoreCallingIdentity(token);
+                        }
+                        private void testMethodFullClass() {
+                            final long token = android.os.Binder.clearCallingIdentity();
+                            try {
+                            } finally {
+                            }
+                            android.os.Binder.restoreCallingIdentity(token);
+                        }
+                        private void testMethodRestoreInCatch() {
+                            final long token = clearCallingIdentity();
+                            try {
+                            } catch (Exception e) {
+                                restoreCallingIdentity(token);
+                            } finally {
+                            }
+                        }
+                    }
+                    """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expect(
+                """
+                        src/test/pkg/TestClass1.java:10: Warning: \
+                        Binder.restoreCallingIdentity(token) is not an immediate child of the \
+                        finally block of the try statement after token declaration. Surround the c\
+                        all with finally block and call it unconditionally. \
+                        [RestoreIdentityCallNotInFinallyBlock]
+                                Binder.restoreCallingIdentity(token);
+                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:17: Warning: \
+                        Binder.restoreCallingIdentity(token) is not an immediate child of the \
+                        finally block of the try statement after token declaration. Surround the c\
+                        all with finally block and call it unconditionally. \
+                        [RestoreIdentityCallNotInFinallyBlock]
+                                android.os.Binder.restoreCallingIdentity(token);
+                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:23: Warning: \
+                        Binder.restoreCallingIdentity(token) is not an immediate child of the \
+                        finally block of the try statement after token declaration. Surround the c\
+                        all with finally block and call it unconditionally. \
+                        [RestoreIdentityCallNotInFinallyBlock]
+                                    restoreCallingIdentity(token);
+                                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        0 errors, 3 warnings
+                        """.addLineContinuation()
+            )
+    }
+
+    fun testDetectsRestoreCallingIdentityCallNotInFinallyInScopes() {
+        lint().files(
+            java(
+                """
+                    package test.pkg;
+                    import android.os.Binder;
+                    public class TestClass1 extends Binder {
+                        private void testMethodOutsideFinally() {
+                            final long token1 = Binder.clearCallingIdentity();
+                            try {
+                            } catch (Exception e) {
+                            } finally {
+                            }
+                            {
+                                Binder.restoreCallingIdentity(token1);
+                            }
+                            final long token2 = android.os.Binder.clearCallingIdentity();
+                            try {
+                            } finally {
+                            }
+                            {
+                                {
+                                    {
+                                        android.os.Binder.restoreCallingIdentity(token2);
+                                    }
+                                }
+                            }
+                        }
+                        private void testMethodInsideFinallyInScopes() {
+                            final long token1 = Binder.clearCallingIdentity();
+                            try {
+                            } finally {
+                                {
+                                    {
+                                        Binder.restoreCallingIdentity(token1);
+                                    }
+                                }
+                            }
+                            final long token2 = clearCallingIdentity();
+                            try {
+                            } finally {
+                                if (true) restoreCallingIdentity(token2);
+                            }
+                        }
+                    }
+                    """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expect(
+                """
+                        src/test/pkg/TestClass1.java:11: Warning: \
+                        Binder.restoreCallingIdentity(token1) is not an immediate child of the \
+                        finally block of the try statement after token1 declaration. Surround the \
+                        call with finally block and call it unconditionally. \
+                        [RestoreIdentityCallNotInFinallyBlock]
+                                    Binder.restoreCallingIdentity(token1);
+                                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:20: Warning: \
+                        Binder.restoreCallingIdentity(token2) is not an immediate child of the \
+                        finally block of the try statement after token2 declaration. Surround the \
+                        call with finally block and call it unconditionally. \
+                        [RestoreIdentityCallNotInFinallyBlock]
+                                            android.os.Binder.restoreCallingIdentity(token2);
+                                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:31: Warning: \
+                        Binder.restoreCallingIdentity(token1) is not an immediate child of the \
+                        finally block of the try statement after token1 declaration. Surround the \
+                        call with finally block and call it unconditionally. \
+                        [RestoreIdentityCallNotInFinallyBlock]
+                                            Binder.restoreCallingIdentity(token1);
+                                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:38: Warning: \
+                        Binder.restoreCallingIdentity(token2) is not an immediate child of the \
+                        finally block of the try statement after token2 declaration. Surround the \
+                        call with finally block and call it unconditionally. \
+                        [RestoreIdentityCallNotInFinallyBlock]
+                                    if (true) restoreCallingIdentity(token2);
+                                              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        0 errors, 4 warnings
+                        """.addLineContinuation()
+            )
+    }
+
+    /** Use of caller-aware methods after clearCallingIdentity() issue tests */
+
+    fun testDetectsUseOfCallerAwareMethodsWithClearedIdentityIssuesInScopes() {
+        lint().files(
+            java(
+                """
+                    package test.pkg;
+                    import android.os.Binder;
+                    import android.os.UserHandle;
+                    public class TestClass1 {
+                        private void testMethod() {
+                            final long token = Binder.clearCallingIdentity();
+                            try {
+                                int pid1 = Binder.getCallingPid();
+                                int pid2 = android.os.Binder.getCallingPid();
+                                int uid1 = Binder.getCallingUid();
+                                int uid2 = android.os.Binder.getCallingUid();
+                                int uid3 = Binder.getCallingUidOrThrow();
+                                int uid4 = android.os.Binder.getCallingUidOrThrow();
+                                UserHandle uh1 = Binder.getCallingUserHandle();
+                                UserHandle uh2 = android.os.Binder.getCallingUserHandle();
+                                {
+                                    int appId1 = UserHandle.getCallingAppId();
+                                    int appId2 = android.os.UserHandle.getCallingAppId();
+                                    int userId1 = UserHandle.getCallingUserId();
+                                    int userId2 = android.os.UserHandle.getCallingUserId();
+                                }
+                            } finally {
+                            Binder.restoreCallingIdentity(token);
+                            }
+                        }
+                    }
+                    """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expect(
+                """
+                        src/test/pkg/TestClass1.java:8: Warning: You cleared the original identity \
+                        with Binder.clearCallingIdentity() and returned into token, so \
+                        getCallingPid() will be using your own identity instead of the \
+                        caller's. Either explicitly query your own identity or move it after \
+                        restoring the identity with Binder.restoreCallingIdentity(token). \
+                        [UseOfCallerAwareMethodsWithClearedIdentity]
+                                    int pid1 = Binder.getCallingPid();
+                                               ~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:9: Warning: You cleared the original identity \
+                        with Binder.clearCallingIdentity() and returned into token, so \
+                        getCallingPid() will be using your own identity instead \
+                        of the caller's. Either explicitly query your own identity or move it \
+                        after restoring the identity with Binder.restoreCallingIdentity(token). \
+                        [UseOfCallerAwareMethodsWithClearedIdentity]
+                                    int pid2 = android.os.Binder.getCallingPid();
+                                               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:10: Warning: You cleared the original \
+                        identity with Binder.clearCallingIdentity() and returned into token, so \
+                        getCallingUid() will be using your own identity instead of the \
+                        caller's. Either explicitly query your own identity or move it after \
+                        restoring the identity with Binder.restoreCallingIdentity(token). \
+                        [UseOfCallerAwareMethodsWithClearedIdentity]
+                                    int uid1 = Binder.getCallingUid();
+                                               ~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:11: Warning: You cleared the original \
+                        identity with Binder.clearCallingIdentity() and returned into token, so \
+                        getCallingUid() will be using your own identity instead \
+                        of the caller's. Either explicitly query your own identity or move it \
+                        after restoring the identity with Binder.restoreCallingIdentity(token). \
+                        [UseOfCallerAwareMethodsWithClearedIdentity]
+                                    int uid2 = android.os.Binder.getCallingUid();
+                                               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:12: Warning: You cleared the original \
+                        identity with Binder.clearCallingIdentity() and returned into token, so \
+                        getCallingUidOrThrow() will be using your own identity instead of \
+                        the caller's. Either explicitly query your own identity or move it after \
+                        restoring the identity with Binder.restoreCallingIdentity(token). \
+                        [UseOfCallerAwareMethodsWithClearedIdentity]
+                                    int uid3 = Binder.getCallingUidOrThrow();
+                                               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:13: Warning: You cleared the original \
+                        identity with Binder.clearCallingIdentity() and returned into token, so \
+                        getCallingUidOrThrow() will be using your own identity \
+                        instead of the caller's. Either explicitly query your own identity or move \
+                        it after restoring the identity with Binder.restoreCallingIdentity(token). \
+                        [UseOfCallerAwareMethodsWithClearedIdentity]
+                                    int uid4 = android.os.Binder.getCallingUidOrThrow();
+                                               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:14: Warning: You cleared the original \
+                        identity with Binder.clearCallingIdentity() and returned into token, so \
+                        getCallingUserHandle() will be using your own identity instead of \
+                        the caller's. Either explicitly query your own identity or move it after \
+                        restoring the identity with Binder.restoreCallingIdentity(token). \
+                        [UseOfCallerAwareMethodsWithClearedIdentity]
+                                    UserHandle uh1 = Binder.getCallingUserHandle();
+                                                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:15: Warning: You cleared the original \
+                        identity with Binder.clearCallingIdentity() and returned into token, so \
+                        getCallingUserHandle() will be using your own identity \
+                        instead of the caller's. Either explicitly query your own identity or move \
+                        it after restoring the identity with Binder.restoreCallingIdentity(token). \
+                        [UseOfCallerAwareMethodsWithClearedIdentity]
+                                    UserHandle uh2 = android.os.Binder.getCallingUserHandle();
+                                                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:17: Warning: You cleared the original \
+                        identity with Binder.clearCallingIdentity() and returned into token, so \
+                        getCallingAppId() will be using your own identity instead of \
+                        the caller's. Either explicitly query your own identity or move it after \
+                        restoring the identity with Binder.restoreCallingIdentity(token). \
+                        [UseOfCallerAwareMethodsWithClearedIdentity]
+                                        int appId1 = UserHandle.getCallingAppId();
+                                                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:18: Warning: You cleared the original \
+                        identity with Binder.clearCallingIdentity() and returned into token, so \
+                        getCallingAppId() will be using your own identity \
+                        instead of the caller's. Either explicitly query your own identity or move \
+                        it after restoring the identity with Binder.restoreCallingIdentity(token). \
+                        [UseOfCallerAwareMethodsWithClearedIdentity]
+                                        int appId2 = android.os.UserHandle.getCallingAppId();
+                                                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:19: Warning: You cleared the original \
+                        identity with Binder.clearCallingIdentity() and returned into token, so \
+                        getCallingUserId() will be using your own identity instead of \
+                        the caller's. Either explicitly query your own identity or move it after \
+                        restoring the identity with Binder.restoreCallingIdentity(token). \
+                        [UseOfCallerAwareMethodsWithClearedIdentity]
+                                        int userId1 = UserHandle.getCallingUserId();
+                                                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:20: Warning: You cleared the original \
+                        identity with Binder.clearCallingIdentity() and returned into token, so \
+                        getCallingUserId() will be using your own identity \
+                        instead of the caller's. Either explicitly query your own identity or move \
+                        it after restoring the identity with Binder.restoreCallingIdentity(token). \
+                        [UseOfCallerAwareMethodsWithClearedIdentity]
+                                        int userId2 = android.os.UserHandle.getCallingUserId();
+                                                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        0 errors, 12 warnings
+                        """.addLineContinuation()
+            )
+    }
+
+    /** Result of Binder.clearCallingIdentity() is not stored in a variable issue tests */
+
+    fun testDetectsResultOfClearIdentityCallNotStoredInVariable() {
+        lint().files(
+            java(
+                """
+                    package test.pkg;
+                    import android.os.Binder;
+                    public class TestClass1 extends Binder {
+                        private void testMethod() {
+                            Binder.clearCallingIdentity();
+                            android.os.Binder.clearCallingIdentity();
+                            clearCallingIdentity();
+                        }
+                    }
+                    """
+            ).indented(),
+            *stubs
+        )
+            .run()
+            .expect(
+                """
+                        src/test/pkg/TestClass1.java:5: Warning: You cleared the original identity \
+                        with Binder.clearCallingIdentity() but did not store the result in a \
+                        variable. You need to store the result in a variable and restore it later. \
+                        [ResultOfClearIdentityCallNotStoredInVariable]
+                                Binder.clearCallingIdentity();
+                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:6: Warning: You cleared the original identity \
+                        with android.os.Binder.clearCallingIdentity() but did not store the result \
+                        in a variable. You need to store the result in a variable and restore it \
+                        later. [ResultOfClearIdentityCallNotStoredInVariable]
+                                android.os.Binder.clearCallingIdentity();
+                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        src/test/pkg/TestClass1.java:7: Warning: You cleared the original identity \
+                        with clearCallingIdentity() but did not store the result in a variable. \
+                        You need to store the result in a variable and restore it later. \
+                        [ResultOfClearIdentityCallNotStoredInVariable]
+                                clearCallingIdentity();
+                                ~~~~~~~~~~~~~~~~~~~~~~
+                        0 errors, 3 warnings
+                        """.addLineContinuation()
+            )
+    }
+
+    /** Stubs for classes used for testing */
+
+    private val binderStub: TestFile = java(
+        """
+            package android.os;
+            public class Binder {
+                public static final native long clearCallingIdentity() {
+                    return 0;
+                }
+                public static final native void restoreCallingIdentity(long token) {
+                }
+                public static final native int getCallingPid() {
+                    return 0;
+                }
+                public static final native int getCallingUid() {
+                    return 0;
+                }
+                public static final int getCallingUidOrThrow() {
+                    return 0;
+                }
+                public static final @NonNull UserHandle getCallingUserHandle() {
+                    return UserHandle.of(UserHandle.getUserId(getCallingUid()));
+                }
+            }
+            """
+    ).indented()
+
+    private val userHandleStub: TestFile = java(
+        """
+            package android.os;
+            import android.annotation.AppIdInt;
+            import android.annotation.UserIdInt;
+            public class UserHandle {
+                public static @AppIdInt int getCallingAppId() {
+                    return getAppId(Binder.getCallingUid());
+                }
+                public static @UserIdInt int getCallingUserId() {
+                    return getUserId(Binder.getCallingUid());
+                }
+                public static @UserIdInt int getUserId(int uid) {
+                    return 0;
+                }
+                public static @AppIdInt int getAppId(int uid) {
+                    return 0;
+                }
+                public static UserHandle of(@UserIdInt int userId) {
+                    return new UserHandle();
+                }
+            }
+            """
+    ).indented()
+
+    private val userIdIntStub: TestFile = java(
+        """
+            package android.annotation;
+            public @interface UserIdInt {
+            }
+            """
+    ).indented()
+
+    private val appIdIntStub: TestFile = java(
+        """
+            package android.annotation;
+            public @interface AppIdInt {
+            }
+            """
+    ).indented()
+
+    private val stubs = arrayOf(binderStub, userHandleStub, userIdIntStub, appIdIntStub)
+
+    // Substitutes "backslash + new line" with an empty string to imitate line continuation
+    private fun String.addLineContinuation(): String = this.trimIndent().replace("\\\n", "")
+}
diff --git a/tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsIssueDetectorTest.kt b/tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsIssueDetectorTest.kt
new file mode 100644
index 0000000..e72f384
--- /dev/null
+++ b/tools/lint/framework/checks/src/test/java/com/google/android/lint/CallingSettingsNonUserGetterMethodsIssueDetectorTest.kt
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 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 com.google.android.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+
+@Suppress("UnstableApiUsage")
+class CallingSettingsNonUserGetterMethodsIssueDetectorTest : LintDetectorTest() {
+    override fun getDetector(): Detector = CallingSettingsNonUserGetterMethodsDetector()
+
+    override fun getIssues(): List<Issue> = listOf(
+            CallingSettingsNonUserGetterMethodsDetector.ISSUE_NON_USER_GETTER_CALLED
+    )
+
+    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
+
+    fun testDoesNotDetectIssues() {
+        lint().files(
+                java(
+                        """
+                    package test.pkg;
+                    import android.provider.Settings.Secure;
+                    public class TestClass1 {
+                        private void testMethod(Context context) {
+                            final int value = Secure.getIntForUser(context.getContentResolver(),
+                                Settings.Secure.KEY1, 0, 0);
+                        }
+                    }
+                    """
+                ).indented(),
+                *stubs
+        )
+                .run()
+                .expectClean()
+    }
+
+    fun testDetectsNonUserGetterCalledFromSecure() {
+        lint().files(
+                java(
+                        """
+                    package test.pkg;
+                    import android.provider.Settings.Secure;
+                    public class TestClass1 {
+                        private void testMethod(Context context) {
+                            final int value = Secure.getInt(context.getContentResolver(),
+                                Settings.Secure.KEY1);
+                        }
+                    }
+                    """
+                ).indented(),
+                *stubs
+        )
+                .run()
+                .expect(
+                        """
+                        src/test/pkg/TestClass1.java:5: Error: \
+                        android.provider.Settings.Secure#getInt() called from system process. \
+                        Please call android.provider.Settings.Secure#getIntForUser() instead.  \
+                        [NonUserGetterCalled]
+                                final int value = Secure.getInt(context.getContentResolver(),
+                                                         ~~~~~~
+                        1 errors, 0 warnings
+                        """.addLineContinuation()
+                )
+    }
+    fun testDetectsNonUserGetterCalledFromSystem() {
+        lint().files(
+                java(
+                        """
+                    package test.pkg;
+                    import android.provider.Settings.System;
+                    public class TestClass1 {
+                        private void testMethod(Context context) {
+                            final float value = System.getFloat(context.getContentResolver(),
+                                Settings.System.KEY1);
+                        }
+                    }
+                    """
+                ).indented(),
+                *stubs
+        )
+                .run()
+                .expect(
+                        """
+                        src/test/pkg/TestClass1.java:5: Error: \
+                        android.provider.Settings.System#getFloat() called from system process. \
+                        Please call android.provider.Settings.System#getFloatForUser() instead.  \
+                        [NonUserGetterCalled]
+                                final float value = System.getFloat(context.getContentResolver(),
+                                                           ~~~~~~~~
+                        1 errors, 0 warnings
+                        """.addLineContinuation()
+                )
+    }
+
+    fun testDetectsNonUserGetterCalledFromSettings() {
+        lint().files(
+                java(
+                        """
+                    package test.pkg;
+                    import android.provider.Settings;
+                    public class TestClass1 {
+                        private void testMethod(Context context) {
+                            float value = Settings.System.getFloat(context.getContentResolver(),
+                                Settings.System.KEY1);
+                        }
+                    }
+                    """
+                ).indented(),
+                *stubs
+        )
+                .run()
+                .expect(
+                        """
+                        src/test/pkg/TestClass1.java:5: Error: \
+                        android.provider.Settings.System#getFloat() called from system process. \
+                        Please call android.provider.Settings.System#getFloatForUser() instead.  \
+                        [NonUserGetterCalled]
+                                float value = Settings.System.getFloat(context.getContentResolver(),
+                                                              ~~~~~~~~
+                        1 errors, 0 warnings
+                        """.addLineContinuation()
+                )
+    }
+
+    fun testDetectsNonUserGettersCalledFromSystemAndSecure() {
+        lint().files(
+                java(
+                        """
+                    package test.pkg;
+                    import android.provider.Settings.Secure;
+                    import android.provider.Settings.System;
+                    public class TestClass1 {
+                        private void testMethod(Context context) {
+                            final long value1 = Secure.getLong(context.getContentResolver(),
+                                Settings.Secure.KEY1, 0);
+                            final String value2 = System.getString(context.getContentResolver(),
+                                Settings.System.KEY2);
+                        }
+                    }
+                    """
+                ).indented(),
+                *stubs
+        )
+                .run()
+                .expect(
+                        """
+                        src/test/pkg/TestClass1.java:6: Error: \
+                        android.provider.Settings.Secure#getLong() called from system process. \
+                        Please call android.provider.Settings.Secure#getLongForUser() instead.  \
+                        [NonUserGetterCalled]
+                                final long value1 = Secure.getLong(context.getContentResolver(),
+                                                           ~~~~~~~
+                        src/test/pkg/TestClass1.java:8: Error: \
+                        android.provider.Settings.System#getString() called from system process. \
+                        Please call android.provider.Settings.System#getStringForUser() instead.  \
+                        [NonUserGetterCalled]
+                                final String value2 = System.getString(context.getContentResolver(),
+                                                             ~~~~~~~~~
+                        2 errors, 0 warnings
+                        """.addLineContinuation()
+                )
+    }
+
+    private val SettingsStub: TestFile = java(
+            """
+            package android.provider;
+            public class Settings {
+                public class Secure {
+                    float getFloat(ContentResolver cr, String key) {
+                        return 0.0f;
+                    }
+                    long getLong(ContentResolver cr, String key) {
+                        return 0l;
+                    }
+                    int getInt(ContentResolver cr, String key) {
+                        return 0;
+                    }
+                }
+                public class System {
+                    float getFloat(ContentResolver cr, String key) {
+                        return 0.0f;
+                    }
+                    long getLong(ContentResolver cr, String key) {
+                        return 0l;
+                    }
+                    String getString(ContentResolver cr, String key) {
+                        return null;
+                    }
+                }
+            }
+            """
+    ).indented()
+
+    private val stubs = arrayOf(SettingsStub)
+
+    // Substitutes "backslash + new line" with an empty string to imitate line continuation
+    private fun String.addLineContinuation(): String = this.trimIndent().replace("\\\n", "")
+}
diff --git a/tools/lint/framework/checks/src/test/java/com/google/android/lint/PackageVisibilityDetectorTest.kt b/tools/lint/framework/checks/src/test/java/com/google/android/lint/PackageVisibilityDetectorTest.kt
new file mode 100644
index 0000000..a70644a
--- /dev/null
+++ b/tools/lint/framework/checks/src/test/java/com/google/android/lint/PackageVisibilityDetectorTest.kt
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2022 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 com.google.android.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestFile
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+
+@Suppress("UnstableApiUsage")
+class PackageVisibilityDetectorTest : LintDetectorTest() {
+    override fun getDetector(): Detector = PackageVisibilityDetector()
+
+    override fun getIssues(): MutableList<Issue> = mutableListOf(
+        PackageVisibilityDetector.ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS
+    )
+
+    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
+
+    fun testDetectIssuesParameterDoesNotApplyPackageVisibilityFilters() {
+        lint().files(java(
+            """
+            package com.android.server.lint.test;
+            import android.internal.test.IFoo;
+
+            public class TestClass extends IFoo.Stub {
+                @Override
+                public boolean hasPackage(String packageName) {
+                    return packageName != null;
+                }
+            }
+            """).indented(), *stubs
+        ).run().expect(
+                """
+                src/com/android/server/lint/test/TestClass.java:6: Warning: \
+                Api: hasPackage contains a package name parameter: packageName does not apply \
+                package visibility filtering rules. \
+                [ApiMightLeakAppVisibility]
+                    public boolean hasPackage(String packageName) {
+                                              ~~~~~~~~~~~~~~~~~~
+                0 errors, 1 warnings
+                """.addLineContinuation()
+        )
+    }
+
+    fun testDoesNotDetectIssuesApiInvokesAppOps() {
+        lint().files(java(
+            """
+            package com.android.server.lint.test;
+            import android.app.AppOpsManager;
+            import android.os.Binder;
+            import android.internal.test.IFoo;
+
+            public class TestClass extends IFoo.Stub {
+                private AppOpsManager mAppOpsManager;
+
+                @Override
+                public boolean hasPackage(String packageName) {
+                    checkPackage(packageName);
+                    return packageName != null;
+                }
+
+                private void checkPackage(String packageName) {
+                    mAppOpsManager.checkPackage(Binder.getCallingUid(), packageName);
+                }
+            }
+            """
+        ).indented(), *stubs).run().expectClean()
+    }
+
+    fun testDoesNotDetectIssuesApiInvokesEnforcePermission() {
+        lint().files(java(
+            """
+            package com.android.server.lint.test;
+            import android.content.Context;
+            import android.internal.test.IFoo;
+
+            public class TestClass extends IFoo.Stub {
+                private Context mContext;
+
+                @Override
+                public boolean hasPackage(String packageName) {
+                    enforcePermission();
+                    return packageName != null;
+                }
+
+                private void enforcePermission() {
+                    mContext.checkCallingPermission(
+                            android.Manifest.permission.ACCESS_INPUT_FLINGER);
+                }
+            }
+            """
+        ).indented(), *stubs).run().expectClean()
+    }
+
+    fun testDoesNotDetectIssuesApiInvokesPackageManager() {
+        lint().files(java(
+            """
+            package com.android.server.lint.test;
+            import android.content.pm.PackageInfo;
+            import android.content.pm.PackageManager;
+            import android.internal.test.IFoo;
+
+            public class TestClass extends IFoo.Stub {
+                private PackageManager mPackageManager;
+
+                @Override
+                public boolean hasPackage(String packageName) {
+                    return getPackageInfo(packageName) != null;
+                }
+
+                private PackageInfo getPackageInfo(String packageName) {
+                    try {
+                        return mPackageManager.getPackageInfo(packageName, 0);
+                    } catch (PackageManager.NameNotFoundException e) {
+                        return null;
+                    }
+                }
+            }
+            """
+        ).indented(), *stubs).run().expectClean()
+    }
+
+    fun testDetectIssuesApiInvokesPackageManagerAndClearCallingIdentify() {
+        lint().files(java(
+            """
+            package com.android.server.lint.test;
+            import android.content.pm.PackageInfo;
+            import android.content.pm.PackageManager;
+            import android.internal.test.IFoo;import android.os.Binder;
+
+            public class TestClass extends IFoo.Stub {
+                private PackageManager mPackageManager;
+
+                @Override
+                public boolean hasPackage(String packageName) {
+                    return getPackageInfo(packageName) != null;
+                }
+
+                private PackageInfo getPackageInfo(String packageName) {
+                    long token = Binder.clearCallingIdentity();
+                    try {
+                        try {
+                            return mPackageManager.getPackageInfo(packageName, 0);
+                        } catch (PackageManager.NameNotFoundException e) {
+                            return null;
+                        }
+                    } finally{
+                        Binder.restoreCallingIdentity(token);
+                    }
+                }
+            }
+            """).indented(), *stubs
+        ).run().expect(
+                """
+                src/com/android/server/lint/test/TestClass.java:10: Warning: \
+                Api: hasPackage contains a package name parameter: packageName does not apply \
+                package visibility filtering rules. \
+                [ApiMightLeakAppVisibility]
+                    public boolean hasPackage(String packageName) {
+                                              ~~~~~~~~~~~~~~~~~~
+                0 errors, 1 warnings
+                """.addLineContinuation()
+        )
+    }
+
+    fun testDoesNotDetectIssuesApiNotSystemPackagePrefix() {
+        lint().files(java(
+            """
+            package com.test.not.system.prefix;
+            import android.internal.test.IFoo;
+
+            public class TestClass extends IFoo.Stub {
+                @Override
+                public boolean hasPackage(String packageName) {
+                    return packageName != null;
+                }
+            }
+            """
+        ).indented(), *stubs).run().expectClean()
+    }
+
+    private val contextStub: TestFile = java(
+        """
+        package android.content;
+
+        public abstract class Context {
+            public abstract int checkCallingPermission(String permission);
+        }
+        """
+    ).indented()
+
+    private val appOpsManagerStub: TestFile = java(
+        """
+        package android.app;
+
+        public class AppOpsManager {
+            public void checkPackage(int uid, String packageName) {
+            }
+        }
+        """
+    ).indented()
+
+    private val packageManagerStub: TestFile = java(
+        """
+        package android.content.pm;
+        import android.content.pm.PackageInfo;
+
+        public abstract class PackageManager {
+            public static class NameNotFoundException extends AndroidException {
+            }
+
+            public abstract PackageInfo getPackageInfo(String packageName, int flags)
+                    throws NameNotFoundException;
+        }
+        """
+    ).indented()
+
+    private val packageInfoStub: TestFile = java(
+        """
+        package android.content.pm;
+        public class PackageInfo {}
+        """
+    ).indented()
+
+    private val binderStub: TestFile = java(
+        """
+        package android.os;
+
+        public class Binder {
+            public static final native long clearCallingIdentity();
+            public static final native void restoreCallingIdentity(long token);
+            public static final native int getCallingUid();
+        }
+        """
+    ).indented()
+
+    private val interfaceIFooStub: TestFile = java(
+        """
+        package android.internal.test;
+        import android.os.Binder;
+
+        public interface IFoo {
+            boolean hasPackage(String packageName);
+            public abstract static class Stub extends Binder implements IFoo {
+            }
+        }
+        """
+    ).indented()
+
+    private val stubs = arrayOf(contextStub, appOpsManagerStub, packageManagerStub,
+        packageInfoStub, binderStub, interfaceIFooStub)
+
+    // Substitutes "backslash + new line" with an empty string to imitate line continuation
+    private fun String.addLineContinuation(): String = this.trimIndent().replace("\\\n", "")
+}
diff --git a/tools/lint/framework/checks/src/test/java/com/google/android/lint/parcel/SaferParcelCheckerTest.kt b/tools/lint/framework/checks/src/test/java/com/google/android/lint/parcel/SaferParcelCheckerTest.kt
new file mode 100644
index 0000000..e686695
--- /dev/null
+++ b/tools/lint/framework/checks/src/test/java/com/google/android/lint/parcel/SaferParcelCheckerTest.kt
@@ -0,0 +1,823 @@
+/*
+ * Copyright (C) 2022 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 com.google.android.lint.parcel
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import com.android.tools.lint.checks.infrastructure.TestMode
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+
+@Suppress("UnstableApiUsage")
+class SaferParcelCheckerTest : LintDetectorTest() {
+    override fun getDetector(): Detector = SaferParcelChecker()
+
+    override fun getIssues(): List<Issue> = listOf(
+        SaferParcelChecker.ISSUE_UNSAFE_API_USAGE
+    )
+
+    override fun lint(): TestLintTask =
+        super.lint()
+            .allowMissingSdk(true)
+            // We don't do partial analysis in the platform
+            .skipTestModes(TestMode.PARTIAL)
+
+    /** Parcel Tests */
+
+    fun testParcelDetectUnsafeReadSerializable() {
+        lint()
+            .files(
+                java(
+                    """
+                        package test.pkg;
+                        import android.os.Parcel;
+                        import java.io.Serializable;
+
+                        public class TestClass {
+                            private TestClass(Parcel p) {
+                                Serializable ans = p.readSerializable();
+                            }
+                        }
+                        """
+                ).indented(),
+                *includes
+            )
+            .expectIdenticalTestModeOutput(false)
+            .run()
+            .expect(
+                """
+                        src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readSerializable() \
+                        API usage [UnsafeParcelApi]
+                                Serializable ans = p.readSerializable();
+                                                   ~~~~~~~~~~~~~~~~~~~~
+                        0 errors, 1 warnings
+                        """.addLineContinuation()
+            )
+    }
+
+    fun testParcelDoesNotDetectSafeReadSerializable() {
+        lint()
+            .files(
+                java(
+                    """
+                        package test.pkg;
+                        import android.os.Parcel;
+                        import java.io.Serializable;
+
+                        public class TestClass {
+                            private TestClass(Parcel p) {
+                                String ans = p.readSerializable(null, String.class);
+                            }
+                        }
+                        """
+                ).indented(),
+                *includes
+            )
+            .run()
+            .expect("No warnings.")
+    }
+
+    fun testParcelDetectUnsafeReadArrayList() {
+        lint()
+            .files(
+                java(
+                    """
+                        package test.pkg;
+                        import android.os.Parcel;
+
+                        public class TestClass {
+                            private TestClass(Parcel p) {
+                                ArrayList ans = p.readArrayList(null);
+                            }
+                        }
+                        """
+                ).indented(),
+                *includes
+            )
+            .run()
+            .expect(
+                """
+                        src/test/pkg/TestClass.java:6: Warning: Unsafe Parcel.readArrayList() API \
+                        usage [UnsafeParcelApi]
+                                ArrayList ans = p.readArrayList(null);
+                                                ~~~~~~~~~~~~~~~~~~~~~
+                        0 errors, 1 warnings
+                        """.addLineContinuation()
+            )
+    }
+
+    fun testParcelDoesNotDetectSafeReadArrayList() {
+        lint()
+            .files(
+                java(
+                    """
+                                package test.pkg;
+                                import android.content.Intent;
+                                import android.os.Parcel;
+
+                                public class TestClass {
+                                    private TestClass(Parcel p) {
+                                        ArrayList<Intent> ans = p.readArrayList(null, Intent.class);
+                                    }
+                                }
+                                """
+                ).indented(),
+                *includes
+            )
+            .run()
+            .expect("No warnings.")
+    }
+
+    fun testParcelDetectUnsafeReadList() {
+        lint()
+            .files(
+                java(
+                    """
+                                package test.pkg;
+                                import android.content.Intent;
+                                import android.os.Parcel;
+                                import java.util.List;
+
+                                public class TestClass {
+                                    private TestClass(Parcel p) {
+                                        List<Intent> list = new ArrayList<Intent>();
+                                        p.readList(list, null);
+                                    }
+                                }
+                                """
+                ).indented(),
+                *includes
+            )
+            .run()
+            .expect(
+                """
+                        src/test/pkg/TestClass.java:9: Warning: Unsafe Parcel.readList() API usage \
+                        [UnsafeParcelApi]
+                                p.readList(list, null);
+                                ~~~~~~~~~~~~~~~~~~~~~~
+                        0 errors, 1 warnings
+                        """.addLineContinuation()
+            )
+    }
+
+    fun testDParceloesNotDetectSafeReadList() {
+        lint()
+            .files(
+                java(
+                    """
+                                package test.pkg;
+                                import android.content.Intent;
+                                import android.os.Parcel;
+                                import java.util.List;
+
+                                public class TestClass {
+                                    private TestClass(Parcel p) {
+                                        List<Intent> list = new ArrayList<Intent>();
+                                        p.readList(list, null, Intent.class);
+                                    }
+                                }
+                                """
+                ).indented(),
+                *includes
+            )
+            .run()
+            .expect("No warnings.")
+    }
+
+    fun testParcelDetectUnsafeReadParcelable() {
+        lint()
+            .files(
+                java(
+                    """
+                                package test.pkg;
+                                import android.content.Intent;
+                                import android.os.Parcel;
+
+                                public class TestClass {
+                                    private TestClass(Parcel p) {
+                                        Intent ans = p.readParcelable(null);
+                                    }
+                                }
+                                """
+                ).indented(),
+                *includes
+            )
+            .run()
+            .expect(
+                """
+                        src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readParcelable() API \
+                        usage [UnsafeParcelApi]
+                                Intent ans = p.readParcelable(null);
+                                             ~~~~~~~~~~~~~~~~~~~~~~
+                        0 errors, 1 warnings
+                        """.addLineContinuation()
+            )
+    }
+
+    fun testParcelDoesNotDetectSafeReadParcelable() {
+        lint()
+            .files(
+                java(
+                    """
+                                package test.pkg;
+                                import android.content.Intent;
+                                import android.os.Parcel;
+
+                                public class TestClass {
+                                    private TestClass(Parcel p) {
+                                        Intent ans = p.readParcelable(null, Intent.class);
+                                    }
+                                }
+                                """
+                ).indented(),
+                *includes
+            )
+            .run()
+            .expect("No warnings.")
+    }
+
+    fun testParcelDetectUnsafeReadParcelableList() {
+        lint()
+            .files(
+                java(
+                    """
+                                package test.pkg;
+                                import android.content.Intent;
+                                import android.os.Parcel;
+                                import java.util.List;
+
+                                public class TestClass {
+                                    private TestClass(Parcel p) {
+                                        List<Intent> list = new ArrayList<Intent>();
+                                        List<Intent> ans = p.readParcelableList(list, null);
+                                    }
+                                }
+                                """
+                ).indented(),
+                *includes
+            )
+            .run()
+            .expect(
+                """
+                        src/test/pkg/TestClass.java:9: Warning: Unsafe Parcel.readParcelableList() \
+                        API usage [UnsafeParcelApi]
+                                List<Intent> ans = p.readParcelableList(list, null);
+                                                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        0 errors, 1 warnings
+                        """.addLineContinuation()
+            )
+    }
+
+    fun testParcelDoesNotDetectSafeReadParcelableList() {
+        lint()
+            .files(
+                java(
+                    """
+                                package test.pkg;
+                                import android.content.Intent;
+                                import android.os.Parcel;
+                                import java.util.List;
+
+                                public class TestClass {
+                                    private TestClass(Parcel p) {
+                                        List<Intent> list = new ArrayList<Intent>();
+                                        List<Intent> ans =
+                                                p.readParcelableList(list, null, Intent.class);
+                                    }
+                                }
+                                """
+                ).indented(),
+                *includes
+            )
+            .run()
+            .expect("No warnings.")
+    }
+
+    fun testParcelDetectUnsafeReadSparseArray() {
+        lint()
+            .files(
+                java(
+                    """
+                                package test.pkg;
+                                import android.content.Intent;
+                                import android.os.Parcel;
+                                import android.util.SparseArray;
+
+                                public class TestClass {
+                                    private TestClass(Parcel p) {
+                                        SparseArray<Intent> ans = p.readSparseArray(null);
+                                    }
+                                }
+                                """
+                ).indented(),
+                *includes
+            )
+            .run()
+            .expect(
+                """
+                        src/test/pkg/TestClass.java:8: Warning: Unsafe Parcel.readSparseArray() API\
+                         usage [UnsafeParcelApi]
+                                SparseArray<Intent> ans = p.readSparseArray(null);
+                                                          ~~~~~~~~~~~~~~~~~~~~~~~
+                        0 errors, 1 warnings
+                        """.addLineContinuation()
+            )
+    }
+
+    fun testParcelDoesNotDetectSafeReadSparseArray() {
+        lint()
+            .files(
+                java(
+                    """
+                                package test.pkg;
+                                import android.content.Intent;
+                                import android.os.Parcel;
+                                import android.util.SparseArray;
+
+                                public class TestClass {
+                                    private TestClass(Parcel p) {
+                                        SparseArray<Intent> ans =
+                                                p.readSparseArray(null, Intent.class);
+                                    }
+                                }
+                                """
+                ).indented(),
+                *includes
+            )
+            .run()
+            .expect("No warnings.")
+    }
+
+    fun testParcelDetectUnsafeReadSArray() {
+        lint()
+            .files(
+                java(
+                    """
+                                package test.pkg;
+                                import android.content.Intent;
+                                import android.os.Parcel;
+
+                                public class TestClass {
+                                    private TestClass(Parcel p) {
+                                        Intent[] ans = p.readArray(null);
+                                    }
+                                }
+                                """
+                ).indented(),
+                *includes
+            )
+            .run()
+            .expect(
+                """
+                        src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readArray() API\
+                         usage [UnsafeParcelApi]
+                                Intent[] ans = p.readArray(null);
+                                               ~~~~~~~~~~~~~~~~~
+                        0 errors, 1 warnings
+                        """.addLineContinuation()
+            )
+    }
+
+    fun testParcelDoesNotDetectSafeReadArray() {
+        lint()
+            .files(
+                java(
+                    """
+                                package test.pkg;
+                                import android.content.Intent;
+                                import android.os.Parcel;
+
+                                public class TestClass {
+                                    private TestClass(Parcel p) {
+                                        Intent[] ans = p.readArray(null, Intent.class);
+                                    }
+                                }
+                                """
+                ).indented(),
+                *includes
+            )
+            .run()
+            .expect("No warnings.")
+    }
+
+    fun testParcelDetectUnsafeReadParcelableSArray() {
+        lint()
+            .files(
+                java(
+                    """
+                                package test.pkg;
+                                import android.content.Intent;
+                                import android.os.Parcel;
+
+                                public class TestClass {
+                                    private TestClass(Parcel p) {
+                                        Intent[] ans = p.readParcelableArray(null);
+                                    }
+                                }
+                                """
+                ).indented(),
+                *includes
+            )
+            .run()
+            .expect(
+                """
+                        src/test/pkg/TestClass.java:7: Warning: Unsafe Parcel.readParcelableArray() API\
+                         usage [UnsafeParcelApi]
+                                Intent[] ans = p.readParcelableArray(null);
+                                               ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                        0 errors, 1 warnings
+                        """.addLineContinuation()
+            )
+    }
+
+    fun testParcelDoesNotDetectSafeReadParcelableArray() {
+        lint()
+            .files(
+                java(
+                    """
+                                package test.pkg;
+                                import android.content.Intent;
+                                import android.os.Parcel;
+
+                                public class TestClass {
+                                    private TestClass(Parcel p) {
+                                        Intent[] ans = p.readParcelableArray(null, Intent.class);
+                                    }
+                                }
+                                """
+                ).indented(),
+                *includes
+            )
+            .run()
+            .expect("No warnings.")
+    }
+
+    /** Bundle Tests */
+
+    fun testBundleDetectUnsafeGetParcelable() {
+        lint()
+            .files(
+                java(
+                    """
+                                package test.pkg;
+                                import android.content.Intent;
+                                import android.os.Bundle;
+
+                                public class TestClass {
+                                    private TestClass(Bundle b) {
+                                        Intent ans = b.getParcelable("key");
+                                    }
+                                }
+                                """
+                ).indented(),
+                *includes
+            )
+            .run()
+            .expect(
+                """
+                    src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getParcelable() API usage [UnsafeParcelApi]
+                            Intent ans = b.getParcelable("key");
+                                         ~~~~~~~~~~~~~~~~~~~~~~
+                    0 errors, 1 warnings
+                        """.addLineContinuation()
+            )
+    }
+
+    fun testBundleDoesNotDetectSafeGetParcelable() {
+        lint()
+            .files(
+                java(
+                    """
+                                package test.pkg;
+                                import android.content.Intent;
+                                import android.os.Bundle;
+
+                                public class TestClass {
+                                    private TestClass(Bundle b) {
+                                        Intent ans = b.getParcelable("key", Intent.class);
+                                    }
+                                }
+                                """
+                ).indented(),
+                *includes
+            )
+            .run()
+            .expect("No warnings.")
+    }
+
+    fun testBundleDetectUnsafeGetParcelableArrayList() {
+        lint()
+            .files(
+                java(
+                    """
+                                package test.pkg;
+                                import android.content.Intent;
+                                import android.os.Bundle;
+
+                                public class TestClass {
+                                    private TestClass(Bundle b) {
+                                        ArrayList<Intent> ans = b.getParcelableArrayList("key");
+                                    }
+                                }
+                                """
+                ).indented(),
+                *includes
+            )
+            .run()
+            .expect(
+                """
+                    src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getParcelableArrayList() API usage [UnsafeParcelApi]
+                            ArrayList<Intent> ans = b.getParcelableArrayList("key");
+                                                    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                    0 errors, 1 warnings
+                        """.addLineContinuation()
+            )
+    }
+
+    fun testBundleDoesNotDetectSafeGetParcelableArrayList() {
+        lint()
+            .files(
+                java(
+                    """
+                                package test.pkg;
+                                import android.content.Intent;
+                                import android.os.Bundle;
+
+                                public class TestClass {
+                                    private TestClass(Bundle b) {
+                                        ArrayList<Intent> ans = b.getParcelableArrayList("key", Intent.class);
+                                    }
+                                }
+                                """
+                ).indented(),
+                *includes
+            )
+            .run()
+            .expect("No warnings.")
+    }
+
+    fun testBundleDetectUnsafeGetParcelableArray() {
+        lint()
+            .files(
+                java(
+                    """
+                                package test.pkg;
+                                import android.content.Intent;
+                                import android.os.Bundle;
+
+                                public class TestClass {
+                                    private TestClass(Bundle b) {
+                                        Intent[] ans = b.getParcelableArray("key");
+                                    }
+                                }
+                                """
+                ).indented(),
+                *includes
+            )
+            .run()
+            .expect(
+                """
+                    src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getParcelableArray() API usage [UnsafeParcelApi]
+                            Intent[] ans = b.getParcelableArray("key");
+                                           ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                    0 errors, 1 warnings
+                        """.addLineContinuation()
+            )
+    }
+
+    fun testBundleDoesNotDetectSafeGetParcelableArray() {
+        lint()
+            .files(
+                java(
+                    """
+                                package test.pkg;
+                                import android.content.Intent;
+                                import android.os.Bundle;
+
+                                public class TestClass {
+                                    private TestClass(Bundle b) {
+                                        Intent[] ans = b.getParcelableArray("key", Intent.class);
+                                    }
+                                }
+                                """
+                ).indented(),
+                *includes
+            )
+            .run()
+            .expect("No warnings.")
+    }
+
+    fun testBundleDetectUnsafeGetSparseParcelableArray() {
+        lint()
+            .files(
+                java(
+                    """
+                                package test.pkg;
+                                import android.content.Intent;
+                                import android.os.Bundle;
+
+                                public class TestClass {
+                                    private TestClass(Bundle b) {
+                                        SparseArray<Intent> ans = b.getSparseParcelableArray("key");
+                                    }
+                                }
+                                """
+                ).indented(),
+                *includes
+            )
+            .run()
+            .expect(
+                """
+                    src/test/pkg/TestClass.java:7: Warning: Unsafe Bundle.getSparseParcelableArray() API usage [UnsafeParcelApi]
+                            SparseArray<Intent> ans = b.getSparseParcelableArray("key");
+                                                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                    0 errors, 1 warnings
+                        """.addLineContinuation()
+            )
+    }
+
+    fun testBundleDoesNotDetectSafeGetSparseParcelableArray() {
+        lint()
+            .files(
+                java(
+                    """
+                                package test.pkg;
+                                import android.content.Intent;
+                                import android.os.Bundle;
+
+                                public class TestClass {
+                                    private TestClass(Bundle b) {
+                                        SparseArray<Intent> ans = b.getSparseParcelableArray("key", Intent.class);
+                                    }
+                                }
+                                """
+                ).indented(),
+                *includes
+            )
+            .run()
+            .expect("No warnings.")
+    }
+
+    /** Intent Tests */
+
+    fun testIntentDetectUnsafeGetParcelableExtra() {
+        lint()
+            .files(
+                java(
+                    """
+                                package test.pkg;
+                                import android.content.Intent;
+
+                                public class TestClass {
+                                    private TestClass(Intent i) {
+                                        Intent ans = i.getParcelableExtra("name");
+                                    }
+                                }
+                                """
+                ).indented(),
+                *includes
+            )
+            .run()
+            .expect(
+                """
+                    src/test/pkg/TestClass.java:6: Warning: Unsafe Intent.getParcelableExtra() API usage [UnsafeParcelApi]
+                            Intent ans = i.getParcelableExtra("name");
+                                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+                    0 errors, 1 warnings
+                        """.addLineContinuation()
+            )
+    }
+
+    fun testIntentDoesNotDetectSafeGetParcelableExtra() {
+        lint()
+            .files(
+                java(
+                    """
+                                package test.pkg;
+                                import android.content.Intent;
+
+                                public class TestClass {
+                                    private TestClass(Intent i) {
+                                        Intent ans = i.getParcelableExtra("name", Intent.class);
+                                    }
+                                }
+                                """
+                ).indented(),
+                *includes
+            )
+            .run()
+            .expect("No warnings.")
+    }
+
+
+    /** Stubs for classes used for testing */
+
+
+    private val includes =
+        arrayOf(
+            manifest().minSdk("33"),
+            java(
+                """
+                        package android.os;
+                        import java.util.ArrayList;
+                        import java.util.List;
+                        import java.util.Map;
+                        import java.util.HashMap;
+
+                        public final class Parcel {
+                            // Deprecated
+                            public Object[] readArray(ClassLoader loader) { return null; }
+                            public ArrayList readArrayList(ClassLoader loader) { return null; }
+                            public HashMap readHashMap(ClassLoader loader) { return null; }
+                            public void readList(List outVal, ClassLoader loader) {}
+                            public void readMap(Map outVal, ClassLoader loader) {}
+                            public <T extends Parcelable> T readParcelable(ClassLoader loader) { return null; }
+                            public Parcelable[] readParcelableArray(ClassLoader loader) { return null; }
+                            public Parcelable.Creator<?> readParcelableCreator(ClassLoader loader) { return null; }
+                            public <T extends Parcelable> List<T> readParcelableList(List<T> list, ClassLoader cl) { return null; }
+                            public Serializable readSerializable() { return null; }
+                            public <T> SparseArray<T> readSparseArray(ClassLoader loader) { return null; }
+
+                            // Replacements
+                            public <T> T[] readArray(ClassLoader loader, Class<T> clazz) { return null; }
+                            public <T> ArrayList<T> readArrayList(ClassLoader loader, Class<? extends T> clazz) { return null; }
+                            public <K, V> HashMap<K,V> readHashMap(ClassLoader loader, Class<? extends K> clazzKey, Class<? extends V> clazzValue) { return null; }
+                            public <T> void readList(List<? super T> outVal, ClassLoader loader, Class<T> clazz) {}
+                            public <K, V> void readMap(Map<? super K, ? super V> outVal, ClassLoader loader, Class<K> clazzKey, Class<V> clazzValue) {}
+                            public <T> T readParcelable(ClassLoader loader, Class<T> clazz) { return null; }
+                            public <T> T[] readParcelableArray(ClassLoader loader, Class<T> clazz) { return null; }
+                            public <T> Parcelable.Creator<T> readParcelableCreator(ClassLoader loader, Class<T> clazz) { return null; }
+                            public <T> List<T> readParcelableList(List<T> list, ClassLoader cl, Class<T> clazz) { return null; }
+                            public <T> T readSerializable(ClassLoader loader, Class<T> clazz) { return null; }
+                            public <T> SparseArray<T> readSparseArray(ClassLoader loader, Class<? extends T> clazz) { return null; }
+                        }
+                        """
+            ).indented(),
+            java(
+                """
+                        package android.os;
+                        import java.util.ArrayList;
+                        import java.util.List;
+                        import java.util.Map;
+                        import java.util.HashMap;
+
+                        public final class Bundle {
+                            // Deprecated
+                            public <T extends Parcelable> T getParcelable(String key) { return  null; }
+                            public <T extends Parcelable> ArrayList<T> getParcelableArrayList(String key) { return null; }
+                            public Parcelable[] getParcelableArray(String key) { return null; }
+                            public <T extends Parcelable> SparseArray<T> getSparseParcelableArray(String key) { return null; }
+
+                            // Replacements
+                            public <T> T getParcelable(String key, Class<T> clazz) { return  null; }
+                            public <T> ArrayList<T> getParcelableArrayList(String key, Class<? extends T> clazz) { return null; }
+                            public <T> T[] getParcelableArray(String key, Class<T> clazz) { return null; }
+                            public <T> SparseArray<T> getSparseParcelableArray(String key, Class<? extends T> clazz) { return null; }
+
+                        }
+                        """
+            ).indented(),
+            java(
+                """
+                        package android.os;
+                        public interface Parcelable {}
+                        """
+            ).indented(),
+            java(
+                """
+                        package android.content;
+                        public class Intent implements Parcelable, Cloneable {
+                            // Deprecated
+                            public <T extends Parcelable> T getParcelableExtra(String name) { return null; }
+
+                            // Replacements
+                            public <T> T getParcelableExtra(String name, Class<T> clazz) { return null; }
+
+                        }
+                        """
+            ).indented(),
+            java(
+                """
+                        package android.util;
+                        public class SparseArray<E> implements Cloneable {}
+                        """
+            ).indented(),
+        )
+
+    // Substitutes "backslash + new line" with an empty string to imitate line continuation
+    private fun String.addLineContinuation(): String = this.trimIndent().replace("\\\n", "")
+}