Add lint rule for calling onBackPressed in callback

You should not call onBackPressed on the Activity or
OnBackPressedDisptacher inside of the handledOnBackPressed of an
OnBackPressedCallback. It is an anti-pattern and should be prevented.

This change adds a lint rule to ensure to call it out at compile time.

RelNote: "There is a now a lint rule to detect calls to onBackPressed
inside of OnBackPressedCallback.handledOnBackPressed."
Test: added test
Bug: 287505200

Change-Id: I3b5d8da34b9d8b43d765a05b8cc387aafc186df0
diff --git a/activity/activity-lint/src/main/java/androidx/activity/lint/ActivityIssueRegistry.kt b/activity/activity-lint/src/main/java/androidx/activity/lint/ActivityIssueRegistry.kt
index 168ec91..e2022c7 100644
--- a/activity/activity-lint/src/main/java/androidx/activity/lint/ActivityIssueRegistry.kt
+++ b/activity/activity-lint/src/main/java/androidx/activity/lint/ActivityIssueRegistry.kt
@@ -29,7 +29,8 @@
     override val api = 14
     override val minApi = CURRENT_API
     override val issues get() = listOf(
-        ActivityResultFragmentVersionDetector.ISSUE
+        ActivityResultFragmentVersionDetector.ISSUE,
+        OnBackPressedDetector.ISSUE
     )
     override val vendor = Vendor(
         feedbackUrl = "https://issuetracker.google.com/issues/new?component=527362",
diff --git a/activity/activity-lint/src/main/java/androidx/activity/lint/OnBackPressedDetector.kt b/activity/activity-lint/src/main/java/androidx/activity/lint/OnBackPressedDetector.kt
new file mode 100644
index 0000000..7e3b21e
--- /dev/null
+++ b/activity/activity-lint/src/main/java/androidx/activity/lint/OnBackPressedDetector.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 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 androidx.activity.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.intellij.psi.PsiMethod
+import java.util.EnumSet
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UMethod
+import org.jetbrains.uast.getParentOfType
+
+class OnBackPressedDetector : Detector(), Detector.UastScanner {
+    companion object {
+        val ISSUE = Issue.create(
+            id = "InvalidUseOfOnBackPressed",
+            briefDescription = "Do not call onBackPressed() within OnBackPressedDisptacher",
+            explanation = """You should not used OnBackPressedCallback for non-UI cases. If you
+                |add a callback, you have to handle back completely in the callback.
+            """,
+            category = Category.CORRECTNESS,
+            severity = Severity.WARNING,
+            implementation = Implementation(
+                OnBackPressedDetector::class.java,
+                EnumSet.of(Scope.JAVA_FILE),
+                Scope.JAVA_FILE_SCOPE
+            )
+        ).addMoreInfo(
+            "https://developer.android.com/guide/navigation/custom-back/" +
+                "predictive-back-gesture#ui-logic"
+        )
+    }
+
+    override fun getApplicableMethodNames(): List<String> = listOf(
+        OnBackPressed
+    )
+
+    override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+        method.containingClass ?: return
+        if (isCalledInHandledOnBackPressed(node.getParentOfType())) {
+            context.report(
+                ISSUE,
+                context.getLocation(node),
+                "Should not call onBackPressed inside of OnBackPressedCallback.handledOnBackPressed"
+            )
+        }
+    }
+
+    private fun isCalledInHandledOnBackPressed(uMethod: UMethod?): Boolean {
+        if (uMethod == null) return false
+        return HandledOnBackPressed == uMethod.name
+    }
+}
+
+private val OnBackPressed = "onBackPressed"
+
+private val HandledOnBackPressed = "handledOnBackPressed"
diff --git a/activity/activity-lint/src/test/java/androidx/activity/lint/OnBackPressedDispatcherTest.kt b/activity/activity-lint/src/test/java/androidx/activity/lint/OnBackPressedDispatcherTest.kt
new file mode 100644
index 0000000..9c903c2
--- /dev/null
+++ b/activity/activity-lint/src/test/java/androidx/activity/lint/OnBackPressedDispatcherTest.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.activity.lint
+
+import androidx.activity.lint.stubs.STUBS
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class OnBackPressedDispatcherTest : LintDetectorTest() {
+    override fun getDetector(): Detector = OnBackPressedDetector()
+
+    override fun getIssues(): MutableList<Issue> =
+        mutableListOf(OnBackPressedDetector.ISSUE)
+
+    @Test
+    fun expectPassOnBackPressed() {
+        lint().files(
+            kotlin(
+                """
+                package com.example
+
+                import androidx.activity.ComponentActivity
+                import androidx.activity.OnBackPressedDispatcher
+
+                fun test() {
+                    val activity = ComponentActivity()
+                    activity.onBackPressed()
+                    val dispatcher = OnBackPressedDispatcher()
+                    dispatcher.onBackPressed()
+                }
+            """
+            ),
+            *STUBS
+        )
+            .run().expectClean()
+    }
+
+    @Test
+    fun expectFailOnBackPressed() {
+        lint().files(
+            kotlin(
+                """
+                package com.example
+
+                import androidx.activity.ComponentActivity
+                import androidx.activity.OnBackPressedCallback
+                import androidx.activity.OnBackPressedDispatcher
+
+                fun test() {
+                    object: OnBackPressedCallback {
+                        override fun handledOnBackPressed() {
+                            val activity = ComponentActivity()
+                            activity.onBackPressed()
+                            val dispatcher = OnBackPressedDispatcher()
+                            dispatcher.onBackPressed()
+                        }
+                    }
+                }
+            """
+            ),
+            *STUBS
+        )
+            .run()
+            .expect(
+                """
+                src/com/example/test.kt:12: Warning: Should not call onBackPressed inside of OnBackPressedCallback.handledOnBackPressed [InvalidUseOfOnBackPressed]
+                                            activity.onBackPressed()
+                                            ~~~~~~~~~~~~~~~~~~~~~~~~
+                src/com/example/test.kt:14: Warning: Should not call onBackPressed inside of OnBackPressedCallback.handledOnBackPressed [InvalidUseOfOnBackPressed]
+                                            dispatcher.onBackPressed()
+                                            ~~~~~~~~~~~~~~~~~~~~~~~~~~
+                0 errors, 2 warnings
+                """
+            )
+    }
+}
diff --git a/activity/activity-lint/src/test/java/androidx/activity/lint/stubs/Stubs.kt b/activity/activity-lint/src/test/java/androidx/activity/lint/stubs/Stubs.kt
index 716b288..caf6b3c 100644
--- a/activity/activity-lint/src/test/java/androidx/activity/lint/stubs/Stubs.kt
+++ b/activity/activity-lint/src/test/java/androidx/activity/lint/stubs/Stubs.kt
@@ -17,6 +17,7 @@
 package androidx.activity.lint.stubs
 
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest.java
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest.kotlin
 
 private val ACTIVITY_RESULT_CALLER = java(
     """
@@ -36,8 +37,41 @@
 """
 )
 
+private val COMPONENT_ACTIVITY = kotlin(
+    """
+    package androidx.activity
+
+    class ComponentActivity {
+        open fun onBackPressed() { }
+    }
+"""
+)
+
+private val ON_BACK_PRESSED_CALLBACK = kotlin(
+    """
+    package androidx.activity
+
+    class OnBackPressedCallback {
+        open fun handleOnBackPressed() { }
+    }
+"""
+)
+
+private val ON_BACK_PRESSED_DISPATCHER = kotlin(
+    """
+    package androidx.activity
+
+    class OnBackPressedDispatcher {
+        open fun onBackPressed() { }
+    }
+"""
+)
+
 // stubs for testing calls to registerForActivityResult
 internal val STUBS = arrayOf(
     ACTIVITY_RESULT_CALLER,
-    ACTIVITY_RESULT_CONTRACT
+    ACTIVITY_RESULT_CONTRACT,
+    COMPONENT_ACTIVITY,
+    ON_BACK_PRESSED_CALLBACK,
+    ON_BACK_PRESSED_DISPATCHER
 )