Add ExemptAidlInterfacesGenerator for PermissionAnnotationDetector

This CL is a prerequisite for migrating PermissionAnnotationDetector to
a global lint check. This will enforce newly added system services to
use @EnforcePermission annotations.

To determine the newly added system services, the global lint check will
use a pre-computed set of existing, i.e. exempt, AIDL interfaces in
AOSP. To compute the set, the CL introduces the following:
* ExemptAidlInterfacesGenerator, the Android Lint check that creates a
  set of AIDL Interfaces for a build target.
* generate-exempt-aidl-interfaces.sh, a Bash script that runs the lint
  check on the entire source tree and aggregates the results.

Bug: 363248121
Test: ExemptAidlInterfacesGeneratorTest
Flag: EXEMPT lint check
Change-Id: Id700fb74485e63c76bbdb163079dd90b08c100dc
diff --git a/tools/lint/utils/README.md b/tools/lint/utils/README.md
new file mode 100644
index 0000000..b5583c5
--- /dev/null
+++ b/tools/lint/utils/README.md
@@ -0,0 +1,11 @@
+# Utility Android Lint Checks for AOSP
+
+This directory contains scripts that execute utility Android Lint Checks for AOSP, specifically:
+* `enforce_permission_counter.py`: Provides statistics regarding the percentage of annotated/not
+  annotated `AIDL` methods with `@EnforcePermission` annotations.
+* `generate-exempt-aidl-interfaces.sh`: Provides a list of all `AIDL` interfaces in the entire
+  source tree.
+
+When adding a new utility Android Lint check to this directory, consider adding any utility or
+data processing tool you might require. Make sure that your contribution is documented in this
+README file.
diff --git a/tools/lint/utils/checks/src/main/java/com/google/android/lint/AndroidUtilsIssueRegistry.kt b/tools/lint/utils/checks/src/main/java/com/google/android/lint/AndroidUtilsIssueRegistry.kt
index fa61c42..9842881 100644
--- a/tools/lint/utils/checks/src/main/java/com/google/android/lint/AndroidUtilsIssueRegistry.kt
+++ b/tools/lint/utils/checks/src/main/java/com/google/android/lint/AndroidUtilsIssueRegistry.kt
@@ -19,6 +19,7 @@
 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.aidl.ExemptAidlInterfacesGenerator
 import com.google.android.lint.aidl.AnnotatedAidlCounter
 import com.google.auto.service.AutoService
 
@@ -27,6 +28,7 @@
 class AndroidUtilsIssueRegistry : IssueRegistry() {
     override val issues = listOf(
         AnnotatedAidlCounter.ISSUE_ANNOTATED_AIDL_COUNTER,
+        ExemptAidlInterfacesGenerator.ISSUE_PERMISSION_ANNOTATION_EXEMPT_AIDL_INTERFACES,
     )
 
     override val api: Int
@@ -38,6 +40,6 @@
     override val vendor: Vendor = Vendor(
         vendorName = "Android",
         feedbackUrl = "http://b/issues/new?component=315013",
-        contact = "tweek@google.com"
+        contact = "android-platform-abuse-prevention-withfriends@google.com"
     )
 }
diff --git a/tools/lint/utils/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfacesGenerator.kt b/tools/lint/utils/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfacesGenerator.kt
new file mode 100644
index 0000000..6ad223c
--- /dev/null
+++ b/tools/lint/utils/checks/src/main/java/com/google/android/lint/aidl/ExemptAidlInterfacesGenerator.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2024 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.aidl
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Context
+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 org.jetbrains.uast.UBlockExpression
+import org.jetbrains.uast.UMethod
+
+/**
+ * Generates a set of fully qualified AIDL Interface names present in the entire source tree with
+ * the following requirement: their implementations have to be inside directories whose path
+ * prefixes match `systemServicePathPrefixes`.
+ */
+class ExemptAidlInterfacesGenerator : AidlImplementationDetector() {
+    private val targetExemptAidlInterfaceNames = mutableSetOf<String>()
+    private val systemServicePathPrefixes = setOf(
+        "frameworks/base/services",
+        "frameworks/base/apex",
+        "frameworks/opt/wear",
+        "packages/modules"
+    )
+
+    // We could've improved performance by visiting classes rather than methods, however, this lint
+    // check won't be run regularly, hence we've decided not to add extra overrides to
+    // AidlImplementationDetector.
+    override fun visitAidlMethod(
+        context: JavaContext,
+        node: UMethod,
+        interfaceName: String,
+        body: UBlockExpression
+    ) {
+        val filePath = context.file.path
+
+        // We perform `filePath.contains` instead of `filePath.startsWith` since getting the
+        // relative path of a source file is non-trivial. That is because `context.file.path`
+        // returns the path to where soong builds the file (i.e. /out/soong/...). Moreover, the
+        // logic to extract the relative path would need to consider several /out/soong/...
+        // locations patterns.
+        if (systemServicePathPrefixes.none { filePath.contains(it) }) return
+
+        val fullyQualifiedInterfaceName =
+            getContainingAidlInterfaceQualified(context, node) ?: return
+
+        targetExemptAidlInterfaceNames.add("\"$fullyQualifiedInterfaceName\",")
+    }
+
+    override fun afterCheckEachProject(context: Context) {
+        if (targetExemptAidlInterfaceNames.isEmpty()) return
+
+        val message = targetExemptAidlInterfaceNames.joinToString("\n")
+
+        context.report(
+            ISSUE_PERMISSION_ANNOTATION_EXEMPT_AIDL_INTERFACES,
+            context.getLocation(context.project.dir),
+            "\n" + message + "\n",
+        )
+    }
+
+    companion object {
+        @JvmField
+        val ISSUE_PERMISSION_ANNOTATION_EXEMPT_AIDL_INTERFACES = Issue.create(
+            id = "PermissionAnnotationExemptAidlInterfaces",
+            briefDescription = "Returns a set of all AIDL interfaces",
+            explanation = """
+                Produces the exemptAidlInterfaces set used by PermissionAnnotationDetector
+            """.trimIndent(),
+            category = Category.SECURITY,
+            priority = 5,
+            severity = Severity.INFORMATIONAL,
+            implementation = Implementation(
+                ExemptAidlInterfacesGenerator::class.java,
+                Scope.JAVA_FILE_SCOPE
+            )
+        )
+    }
+}
diff --git a/tools/lint/utils/checks/src/test/java/com/google/android/lint/aidl/ExemptAidlInterfacesGeneratorTest.kt b/tools/lint/utils/checks/src/test/java/com/google/android/lint/aidl/ExemptAidlInterfacesGeneratorTest.kt
new file mode 100644
index 0000000..9a17bb4
--- /dev/null
+++ b/tools/lint/utils/checks/src/test/java/com/google/android/lint/aidl/ExemptAidlInterfacesGeneratorTest.kt
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2024 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.aidl
+
+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
+
+class ExemptAidlInterfacesGeneratorTest : LintDetectorTest() {
+    override fun getDetector(): Detector = ExemptAidlInterfacesGenerator()
+
+    override fun getIssues(): List<Issue> = listOf(
+        ExemptAidlInterfacesGenerator.ISSUE_PERMISSION_ANNOTATION_EXEMPT_AIDL_INTERFACES,
+    )
+
+    override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
+
+    fun testMultipleAidlInterfacesImplemented() {
+        lint()
+            .files(
+                java(
+                    createVisitedPath("TestClass1.java"),
+                    """
+                        package com.android.server;
+                        public class TestClass1 extends IFoo.Stub {
+                            public void testMethod() {}
+                        }
+                    """
+                )
+                    .indented(),
+                java(
+                    createVisitedPath("TestClass2.java"),
+                    """
+                        package com.android.server;
+                        public class TestClass2 extends IBar.Stub {
+                            public void testMethod() {}
+                        }
+                    """
+                )
+                    .indented(),
+                *stubs,
+            )
+            .run()
+            .expect(
+                """
+                    app: Information: "IFoo",
+                    "IBar", [PermissionAnnotationExemptAidlInterfaces]
+                    0 errors, 0 warnings
+                """
+            )
+    }
+
+    fun testSingleAidlInterfaceRepeated() {
+        lint()
+            .files(
+                java(
+                    createVisitedPath("TestClass1.java"),
+                    """
+                        package com.android.server;
+                        public class TestClass1 extends IFoo.Stub {
+                            public void testMethod() {}
+                        }
+                    """
+                )
+                    .indented(),
+                java(
+                    createVisitedPath("TestClass2.java"),
+                    """
+                        package com.android.server;
+                        public class TestClass2 extends IFoo.Stub {
+                            public void testMethod() {}
+                        }
+                    """
+                )
+                    .indented(),
+                *stubs,
+            )
+            .run()
+            .expect(
+                """
+                    app: Information: "IFoo", [PermissionAnnotationExemptAidlInterfaces]
+                    0 errors, 0 warnings
+                """
+            )
+    }
+
+    fun testAnonymousClassExtendsAidlStub() {
+        lint()
+            .files(
+                java(
+                    createVisitedPath("TestClass.java"),
+                    """
+                        package com.android.server;
+                        public class TestClass {
+                            private IBinder aidlImpl = new IFoo.Stub() {
+                                public void testMethod() {}
+                            };
+                        }
+                        """
+                )
+                    .indented(),
+                *stubs,
+            )
+            .run()
+            .expect(
+                """
+                    app: Information: "IFoo", [PermissionAnnotationExemptAidlInterfaces]
+                    0 errors, 0 warnings
+                """
+            )
+    }
+
+    fun testNoAidlInterfacesImplemented() {
+        lint()
+            .files(
+                java(
+                    createVisitedPath("TestClass.java"),
+                    """
+                        package com.android.server;
+                        public class TestClass {
+                            public void testMethod() {}
+                        }
+                    """
+                )
+                    .indented(),
+                *stubs
+            )
+            .run()
+            .expectClean()
+    }
+
+    fun testAidlInterfaceImplementedInIgnoredDirectory() {
+        lint()
+            .files(
+                java(
+                    ignoredPath,
+                    """
+                        package com.android.server;
+                        public class TestClass1 extends IFoo.Stub {
+                            public void testMethod() {}
+                        }
+                    """
+                )
+                    .indented(),
+                *stubs,
+            )
+            .run()
+            .expectClean()
+    }
+
+    private val interfaceIFoo: TestFile = java(
+        """
+            public interface IFoo extends android.os.IInterface {
+                public static abstract class Stub extends android.os.Binder implements IFoo {}
+                public void testMethod();
+            }
+        """
+    ).indented()
+
+    private val interfaceIBar: TestFile = java(
+        """
+            public interface IBar extends android.os.IInterface {
+                public static abstract class Stub extends android.os.Binder implements IBar {}
+                public void testMethod();
+            }
+        """
+    ).indented()
+
+    private val stubs = arrayOf(interfaceIFoo, interfaceIBar)
+
+    private fun createVisitedPath(filename: String) =
+        "src/frameworks/base/services/java/com/android/server/$filename"
+
+    private val ignoredPath = "src/test/pkg/TestClass.java"
+}
diff --git a/tools/lint/utils/generate-exempt-aidl-interfaces.sh b/tools/lint/utils/generate-exempt-aidl-interfaces.sh
new file mode 100755
index 0000000..44dcdd7
--- /dev/null
+++ b/tools/lint/utils/generate-exempt-aidl-interfaces.sh
@@ -0,0 +1,59 @@
+#
+# Copyright (C) 2024 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.
+#
+
+# Create a directory for the results and a nested temporary directory.
+mkdir -p $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/tmp
+
+# Create a copy of `AndroidGlobalLintChecker.jar` to restore it afterwards.
+cp $ANDROID_BUILD_TOP/prebuilts/cmdline-tools/AndroidGlobalLintChecker.jar \
+    $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/AndroidGlobalLintChecker.jar
+
+# Configure the environment variable required for running the lint check on the entire source tree.
+export ANDROID_LINT_CHECK=PermissionAnnotationExemptAidlInterfaces
+
+# Build the target corresponding to the lint checks present in the `utils` directory.
+m AndroidUtilsLintChecker
+
+# Replace `AndroidGlobalLintChecker.jar` with the newly built `jar` file.
+cp $ANDROID_BUILD_TOP/out/host/linux-x86/framework/AndroidUtilsLintChecker.jar \
+    $ANDROID_BUILD_TOP/prebuilts/cmdline-tools/AndroidGlobalLintChecker.jar;
+
+# Run the lint check on the entire source tree.
+m lint-check
+
+# Copy the archive containing the results of `lint-check` into the temporary directory.
+cp $ANDROID_BUILD_TOP/out/soong/lint-report-text.zip \
+    $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/tmp
+
+cd $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/tmp
+
+# Unzip the archive containing the results of `lint-check`.
+unzip lint-report-text.zip
+
+# Concatenate the results of `lint-check` into a single string.
+concatenated_reports=$(find . -type f | xargs cat)
+
+# Extract the fully qualified names of the AIDL Interfaces from the concatenated results. Output
+# this list into `out/soong/exempt_aidl_interfaces_generator_output/exempt_aidl_interfaces`.
+echo $concatenated_reports | grep -Eo '\"([a-zA-Z0-9_]*\.)+[a-zA-Z0-9_]*\",' | sort | uniq > ../exempt_aidl_interfaces
+
+# Remove the temporary directory.
+rm -rf $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/tmp
+
+# Restore the original copy of `AndroidGlobalLintChecker.jar` and delete the copy.
+cp $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/AndroidGlobalLintChecker.jar \
+    $ANDROID_BUILD_TOP/prebuilts/cmdline-tools/AndroidGlobalLintChecker.jar
+rm $ANDROID_BUILD_TOP/out/soong/exempt_aidl_interfaces_generator_output/AndroidGlobalLintChecker.jar