Launcher3: Introduce lint checks for Launcher3.

  As part of the initial check in, add a test to ensure
  that custom Dialogs are not utilized inside of the codebase.

Bug: 389709580
Test: DialogDetectorTest
Flag: NONE Lint checks
Change-Id: I7e3f98c729cdbf4d062419c53a209d12a23b1806
diff --git a/Android.bp b/Android.bp
index 5b986ab..1e1e0ad 100644
--- a/Android.bp
+++ b/Android.bp
@@ -452,6 +452,7 @@
         "AndroidManifest-common.xml",
     ],
     lint: {
+        extra_check_modules: ["Launcher3LintChecker"],
         baseline_filename: "lint-baseline.xml",
     },
 }
diff --git a/checks/Android.bp b/checks/Android.bp
new file mode 100644
index 0000000..dfd701e
--- /dev/null
+++ b/checks/Android.bp
@@ -0,0 +1,46 @@
+// Copyright (C) 2025 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 {
+    default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library_host {
+    name: "Launcher3LintChecker",
+    srcs: ["src/**/*.kt"],
+    plugins: ["auto_service_plugin"],
+    libs: [
+        "auto_service_annotations",
+        "lint_api",
+    ],
+    kotlincflags: ["-Xjvm-default=all"],
+}
+
+java_test_host {
+    name: "Launcher3LintCheckerTest",
+    defaults: ["AndroidLintCheckerTestDefaults"],
+    srcs: ["tests/**/*.kt"],
+    data: [
+        ":androidx.annotation_annotation",
+        ":dagger2",
+        ":kotlinx-coroutines-core",
+    ],
+    device_common_data: [
+        ":framework",
+    ],
+    static_libs: [
+        "Launcher3LintChecker",
+    ],
+}
diff --git a/checks/src/com/android/internal/launcher3/lint/CustomDialogDetector.kt b/checks/src/com/android/internal/launcher3/lint/CustomDialogDetector.kt
new file mode 100644
index 0000000..37358bb
--- /dev/null
+++ b/checks/src/com/android/internal/launcher3/lint/CustomDialogDetector.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2025 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.
+ */
+
+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 org.jetbrains.uast.UClass
+
+/** Detector to identify custom usage of Android's Dialog within the Launcher3 codebase. */
+class CustomDialogDetector : Detector(), SourceCodeScanner {
+
+    override fun applicableSuperClasses(): List<String> {
+        return listOf(DIALOG_CLASS_NAME)
+    }
+
+    override fun visitClass(context: JavaContext, declaration: UClass) {
+        val superTypeClassNames = declaration.superTypes.mapNotNull { it.resolve()?.qualifiedName }
+        if (superTypeClassNames.contains(DIALOG_CLASS_NAME)) {
+            context.report(
+                ISSUE,
+                declaration,
+                context.getNameLocation(declaration),
+                "Class implements Dialog",
+            )
+        }
+    }
+
+    companion object {
+        private const val DIALOG_CLASS_NAME = "android.app.Dialog"
+
+        @JvmField
+        val ISSUE =
+            Issue.create(
+                id = "IllegalUseOfCustomDialog",
+                briefDescription = "dialogs should not be used in Launcher",
+                explanation =
+                    """
+                Don't use custom Dialogs within the launcher code base, instead consider utilizing
+                AbstractFloatingView to display content that should float above the launcher where
+                it can be correctly managed for dismissal.
+            """
+                        .trimIndent(),
+                category = Category.CORRECTNESS,
+                priority = 10,
+                severity = Severity.ERROR,
+                implementation =
+                    Implementation(CustomDialogDetector::class.java, Scope.JAVA_FILE_SCOPE),
+            )
+    }
+}
diff --git a/checks/src/com/android/internal/launcher3/lint/Launcher3IssueRegistry.kt b/checks/src/com/android/internal/launcher3/lint/Launcher3IssueRegistry.kt
new file mode 100644
index 0000000..c77c42b
--- /dev/null
+++ b/checks/src/com/android/internal/launcher3/lint/Launcher3IssueRegistry.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2025 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.android.internal.launcher3.lint
+
+import CustomDialogDetector
+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.android.tools.lint.detector.api.Issue
+import com.google.auto.service.AutoService
+
+@AutoService(IssueRegistry::class)
+@Suppress("UnstableApiUsage")
+class Launcher3IssueRegistry : IssueRegistry() {
+    override val issues: List<Issue>
+        get() = listOf(CustomDialogDetector.ISSUE)
+
+    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=78010",
+            contact = "abegovic@google.com",
+        )
+}
diff --git a/checks/tests/com/android/internal/launcher3/lint/CustomDialogDetectorTest.kt b/checks/tests/com/android/internal/launcher3/lint/CustomDialogDetectorTest.kt
new file mode 100644
index 0000000..2a37953
--- /dev/null
+++ b/checks/tests/com/android/internal/launcher3/lint/CustomDialogDetectorTest.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2025 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.android.internal.launcher3.lint
+
+import CustomDialogDetector
+import com.android.tools.lint.checks.infrastructure.TestFiles
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+
+/** Test for [CustomDialogDetector]. */
+class CustomDialogDetectorTest : Launcher3LintDetectorTest() {
+    override fun getDetector(): Detector = CustomDialogDetector()
+
+    override fun getIssues(): List<Issue> = listOf(CustomDialogDetector.ISSUE)
+
+    @Test
+    fun classDoesNotExtendDialog_noViolation() {
+        lint()
+            .files(
+                TestFiles.kotlin(
+                    """
+                    package test.pkg
+
+                    class SomeClass
+                """
+                        .trimIndent()
+                ),
+                *androidStubs,
+            )
+            .issues(CustomDialogDetector.ISSUE)
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun classDoesExtendDialog_violation() {
+        lint()
+            .files(
+                TestFiles.kotlin(
+                    """
+                    package test.pkg
+
+                    import android.app.Dialog
+
+                    class SomeClass(context: Context) : Dialog(context)
+                """
+                        .trimIndent()
+                ),
+                *androidStubs,
+            )
+            .issues(CustomDialogDetector.ISSUE)
+            .run()
+            .expect(
+                ("""
+                src/test/pkg/SomeClass.kt:5: Error: Class implements Dialog [IllegalUseOfCustomDialog]
+                class SomeClass(context: Context) : Dialog(context)
+                      ~~~~~~~~~
+                1 errors, 0 warnings
+                """)
+                    .trimIndent()
+            )
+    }
+}
diff --git a/checks/tests/com/android/internal/launcher3/lint/Launcher3LintDetectorTest.kt b/checks/tests/com/android/internal/launcher3/lint/Launcher3LintDetectorTest.kt
new file mode 100644
index 0000000..09085c7
--- /dev/null
+++ b/checks/tests/com/android/internal/launcher3/lint/Launcher3LintDetectorTest.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2025 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.android.internal.launcher3.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestFiles
+import com.android.tools.lint.checks.infrastructure.TestLintTask
+import java.io.File
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/**
+ * Abstract class that should be used by any test for launcher 3 lint detectors.
+ *
+ * When you write your test, ensure that you pass [androidStubs] as part of your [TestFiles]
+ * definition.
+ */
+@RunWith(JUnit4::class)
+abstract class Launcher3LintDetectorTest : LintDetectorTest() {
+
+    /**
+     * Customize the lint task to disable SDK usage completely. This ensures that running the tests
+     * in Android Studio has the same result as running the tests in atest
+     */
+    override fun lint(): TestLintTask =
+        super.lint().allowMissingSdk(true).sdkHome(File("/dev/null"))
+
+    companion object {
+        private val libraryNames =
+            arrayOf(
+                "androidx.annotation_annotation.jar",
+                "dagger2.jar",
+                "framework.jar",
+                "kotlinx-coroutines-core.jar",
+            )
+
+        /**
+         * This file contains stubs of framework APIs and System UI classes for testing purposes
+         * only. The stubs are not used in the lint detectors themselves.
+         */
+        val androidStubs =
+            libraryNames
+                .map { TestFiles.LibraryReferenceTestFile(File(it).canonicalFile) }
+                .toTypedArray()
+    }
+}