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
)