[GH] [Fragment] Add lint warning for calling setOnCancelListener and setOnDismissListener in onCreateDialog()

## Proposed Changes

 When using a `DialogFragment`, the `setOnCancelListener` and `setOnDismissListener` callback functions within the `onCreateDialog` function __must not be used__ because the `DialogFragment` owns these callbacks. Instead the respective `onCancel` and `onDismiss` functions can be used to achieve the desired effect.

## Testing

Test: `./gradlew fragment:fragment-lint:test`

## Issues Fixed

Fixes: The bug on [https://issuetracker.google.com/issues/181780047](https://issuetracker.google.com/issues/181780047) being fixed

This is an imported pull request from https://github.com/androidx/androidx/pull/171.

Resolves #171
Github-Pr-Head-Sha: 92f6ef9d142d57909e0ffb10b473673f840d7d3f
GitOrigin-RevId: ab96d00aee90a600607fa7e0c55386e445dfa3fa
Change-Id: Iccbd039116b8d7fae70332ceb93d1ca40b8e371c
diff --git a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/FragmentIssueRegistry.kt b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/FragmentIssueRegistry.kt
index 5384ce0..c0dc727 100644
--- a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/FragmentIssueRegistry.kt
+++ b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/FragmentIssueRegistry.kt
@@ -31,6 +31,7 @@
         UnsafeFragmentLifecycleObserverDetector.BACK_PRESSED_ISSUE,
         UnsafeFragmentLifecycleObserverDetector.LIVEDATA_ISSUE,
         UseRequireInsteadOfGet.ISSUE,
-        UseGetLayoutInflater.ISSUE
+        UseGetLayoutInflater.ISSUE,
+        OnCreateDialogIncorrectCallbackDetector.ISSUE
     )
 }
diff --git a/fragment/fragment-lint/src/main/java/androidx/fragment/lint/OnCreateDialogIncorrectCallbackDetector.kt b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/OnCreateDialogIncorrectCallbackDetector.kt
new file mode 100644
index 0000000..b462e88
--- /dev/null
+++ b/fragment/fragment-lint/src/main/java/androidx/fragment/lint/OnCreateDialogIncorrectCallbackDetector.kt
@@ -0,0 +1,156 @@
+/*
+ * Copyright 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.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.fragment.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.isKotlin
+import com.intellij.psi.impl.source.PsiClassReferenceType
+import org.jetbrains.kotlin.psi.psiUtil.getSuperNames
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UClass
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.kotlin.KotlinUClass
+import org.jetbrains.uast.visitor.AbstractUastVisitor
+
+/**
+ * When using a `DialogFragment`, the `setOnCancelListener` and `setOnDismissListener` callback
+ * functions within the `onCreateDialog` function __must not be used__
+ * because the `DialogFragment` owns these callbacks. Instead the respective `onCancel` and
+ * `onDismiss` functions can be used to achieve the desired effect.
+ */
+class OnCreateDialogIncorrectCallbackDetector : Detector(), SourceCodeScanner {
+
+    companion object Issues {
+        val ISSUE = Issue.create(
+            id = "DialogFragmentCallbacksDetector",
+            briefDescription = "Use onCancel() and onDismiss() instead of calling " +
+                "setOnCancelListener() and setOnDismissListener() from onCreateDialog()",
+            explanation = """When using a `DialogFragment`, the `setOnCancelListener` and \
+                `setOnDismissListener` callback functions within the `onCreateDialog` function \
+                 __must not be used__ because the `DialogFragment` owns these callbacks. \
+                 Instead the respective `onCancel` and `onDismiss` functions can be used to \
+                 achieve the desired effect.""",
+            category = Category.CORRECTNESS,
+            severity = Severity.WARNING,
+            implementation = Implementation(
+                OnCreateDialogIncorrectCallbackDetector::class.java,
+                Scope.JAVA_FILE_SCOPE
+            ),
+            androidSpecific = true
+        )
+    }
+
+    override fun getApplicableUastTypes(): List<Class<out UElement>>? {
+        return listOf(UClass::class.java)
+    }
+
+    override fun createUastHandler(context: JavaContext): UElementHandler? {
+        return UastHandler(context)
+    }
+
+    private inner class UastHandler(val context: JavaContext) : UElementHandler() {
+        override fun visitClass(node: UClass) {
+            if (isKotlin(context.psiFile) && (node as KotlinUClass).ktClass!!.getSuperNames()[0] !=
+                DIALOG_FRAGMENT_CLASS
+            ) {
+                return
+            }
+
+            if (!isKotlin(context.psiFile) &&
+                (node.uastSuperTypes[0].type as PsiClassReferenceType)
+                    .className != DIALOG_FRAGMENT_CLASS
+            ) {
+                return
+            }
+
+            node.methods.forEach {
+                if (it.name == ENTRY_METHOD) {
+                    val visitor = UastMethodsVisitor(context, it.name)
+                    it.uastBody?.accept(visitor)
+                }
+            }
+        }
+    }
+
+    /**
+     * A UAST Visitor that explores all method calls within a
+     * [androidx.fragment.app.DialogFragment] callback to check for an incorrect method call.
+     *
+     * @param context The context of the lint request.
+     * @param containingMethodName The name of the originating Fragment lifecycle method.
+     */
+    private class UastMethodsVisitor(
+        private val context: JavaContext,
+        private val containingMethodName: String
+    ) : AbstractUastVisitor() {
+        private val visitedMethods = mutableSetOf<UCallExpression>()
+
+        override fun visitCallExpression(node: UCallExpression): Boolean {
+            if (visitedMethods.contains(node)) {
+                return super.visitCallExpression(node)
+            }
+
+            val methodName = node.methodIdentifier?.name ?: return super.visitCallExpression(node)
+
+            when (methodName) {
+                SET_ON_CANCEL_LISTENER -> {
+                    report(
+                        context = context,
+                        node = node,
+                        message = "Use onCancel() instead of calling setOnCancelListener() " +
+                            "from onCreateDialog()"
+                    )
+                    visitedMethods.add(node)
+                }
+                SET_ON_DISMISS_LISTENER -> {
+                    report(
+                        context = context,
+                        node = node,
+                        message = "Use onDismiss() instead of calling setOnDismissListener() " +
+                            "from onCreateDialog()"
+                    )
+                    visitedMethods.add(node)
+                }
+            }
+            return super.visitCallExpression(node)
+        }
+
+        private fun report(context: JavaContext, node: UCallExpression, message: String) {
+            context.report(
+                issue = ISSUE,
+                location = context.getLocation(node),
+                message = message,
+                quickfixData = null
+            )
+        }
+    }
+}
+
+private const val ENTRY_METHOD = "onCreateDialog"
+private const val DIALOG_FRAGMENT_CLASS = "DialogFragment"
+private const val SET_ON_CANCEL_LISTENER = "setOnCancelListener"
+private const val SET_ON_DISMISS_LISTENER = "setOnDismissListener"
diff --git a/fragment/fragment-lint/src/test/java/androidx/fragment/lint/OnCreateDialogIncorrectCallbackDetectorTest.kt b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/OnCreateDialogIncorrectCallbackDetectorTest.kt
new file mode 100644
index 0000000..c67cd41
--- /dev/null
+++ b/fragment/fragment-lint/src/test/java/androidx/fragment/lint/OnCreateDialogIncorrectCallbackDetectorTest.kt
@@ -0,0 +1,271 @@
+/*
+ * Copyright 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 androidx.fragment.lint
+
+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
+
+/* ktlint-disable max-line-length */
+@RunWith(JUnit4::class)
+class OnCreateDialogIncorrectCallbackDetectorTest : LintDetectorTest() {
+
+    override fun getDetector(): Detector = OnCreateDialogIncorrectCallbackDetector()
+
+    override fun getIssues(): MutableList<Issue> {
+        return mutableListOf(OnCreateDialogIncorrectCallbackDetector.ISSUE)
+    }
+
+    private val dialogFragmentCorrectImplementationStubJava = java(
+        """
+            package foo;
+            import android.app.Dialog;
+            import android.content.DialogInterface;
+            import android.os.Bundle;
+            import androidx.appcompat.app.AlertDialog;
+            import androidx.fragment.app.DialogFragment;
+            public class TestFragment extends DialogFragment {
+                @NonNull
+                @Override
+                public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+                    Dialog dialog = AlertDialog.Builder(requireActivity());
+                    return dialog.create();
+                }
+            }
+            """
+    ).indented()
+
+    private val dialogFragmentCorrectImplementationStubKotlin = kotlin(
+        """
+            package foo
+            import android.app.Dialog
+            import android.content.DialogInterface
+            import android.os.Bundle
+            import androidx.appcompat.app.AlertDialog
+            import androidx.fragment.app.DialogFragment
+            class TestDialog : DialogFragment() {
+                override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+                    val dialog = AlertDialog.Builder(requireActivity())
+                    return dialog.create()
+                }
+                override fun onCancel(dialog: DialogInterface) {
+                    super.onCancel(dialog)
+                }
+
+                override fun onDismiss(dialog: DialogInterface) {
+                    super.onDismiss(dialog)
+                }
+            }
+            """
+    ).indented()
+
+    private val dialogFragmentStubJavaWithCancelListener = java(
+        """
+            package foo;
+            import android.app.Dialog;
+            import android.content.DialogInterface;
+            import android.os.Bundle;
+            import androidx.appcompat.app.AlertDialog;
+            import androidx.fragment.app.DialogFragment;
+            public class TestFragment extends DialogFragment {
+                @Override
+                public Dialog onCreateDialog(Bundle savedInstanceState) {
+                    Dialog dialog = AlertDialog.Builder(requireActivity());
+                    dialog.setOnCancelListener({ });
+                    return dialog.create();
+                }
+            }
+            """
+    ).indented()
+
+    private val dialogFragmentStubJavaWithDismissListener = java(
+        """
+            package foo;
+            import android.app.Dialog;
+            import android.content.DialogInterface;
+            import android.os.Bundle;
+            import androidx.appcompat.app.AlertDialog;
+            import androidx.fragment.app.DialogFragment;
+            public class TestFragment extends DialogFragment {
+                @Override
+                public Dialog onCreateDialog(Bundle savedInstanceState) {
+                    Dialog dialog = AlertDialog.Builder(requireActivity());
+                    dialog.setOnDismissListener({ });
+                    return dialog.create();
+                }
+            }
+            """
+    ).indented()
+
+    private val dialogFragmentStubKotlinWithCancelListener = kotlin(
+        """
+            package foo
+            import android.app.Dialog
+            import android.content.DialogInterface
+            import android.os.Bundle
+            import androidx.appcompat.app.AlertDialog
+            import androidx.fragment.app.DialogFragment
+            class TestDialog : DialogFragment() {
+                override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+                    val dialog = AlertDialog.Builder(requireActivity())
+                    dialog.setOnCancelListener { }
+                    return dialog.create()
+                }
+
+                override fun onDismiss(dialog: DialogInterface) {
+                    super.onDismiss(dialog)
+                }
+            }
+            """
+    ).indented()
+
+    private val dialogFragmentStubKotlinWithDismissListener = kotlin(
+        """
+            package foo
+            import android.app.Dialog
+            import android.content.DialogInterface
+            import android.os.Bundle
+            import androidx.appcompat.app.AlertDialog
+            import androidx.fragment.app.DialogFragment
+            class TestDialog : DialogFragment() {
+                override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+                    val dialog = AlertDialog.Builder(requireActivity())
+                    dialog.setOnDismissListener { }
+                    return dialog.create()
+                }
+
+                override fun onCancel(dialog: DialogInterface) {
+                    super.onCancel(dialog)
+                }
+            }
+            """
+    ).indented()
+
+    private val dialogFragmentStubKotlinWithDismissAndCancelListeners = kotlin(
+        """
+            package foo
+            import android.app.Dialog
+            import android.content.DialogInterface
+            import android.os.Bundle
+            import androidx.appcompat.app.AlertDialog
+            import androidx.fragment.app.DialogFragment
+            class TestDialog : DialogFragment() {
+                override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+                    val dialog = AlertDialog.Builder(requireActivity())
+                    dialog.setOnDismissListener { }
+                    dialog.setOnCancelListener { }
+                    return dialog.create()
+                }
+            }
+            """
+    ).indented()
+
+    @Test
+    fun `java expect fail dialog fragment with cancel listener`() {
+        lint().files(dialogFragmentStubJavaWithCancelListener)
+            .run()
+            .expect(
+                """
+src/foo/TestFragment.java:11: Warning: Use onCancel() instead of calling setOnCancelListener() from onCreateDialog() [DialogFragmentCallbacksDetector]
+        dialog.setOnCancelListener({ });
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+0 errors, 1 warnings
+            """
+            )
+            .expectWarningCount(1)
+    }
+
+    @Test
+    fun `java expect fail dialog fragment with dismiss listener`() {
+        lint().files(dialogFragmentStubJavaWithDismissListener)
+            .run()
+            .expect(
+                """
+src/foo/TestFragment.java:11: Warning: Use onDismiss() instead of calling setOnDismissListener() from onCreateDialog() [DialogFragmentCallbacksDetector]
+        dialog.setOnDismissListener({ });
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+0 errors, 1 warnings
+            """
+            )
+            .expectWarningCount(1)
+    }
+
+    @Test
+    fun `java expect clean dialog fragment`() {
+        lint().files(dialogFragmentCorrectImplementationStubJava)
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun `kotlin expect fail dialog fragment with cancel listener`() {
+        lint().files(dialogFragmentStubKotlinWithCancelListener)
+            .run()
+            .expect(
+                """
+src/foo/TestDialog.kt:10: Warning: Use onCancel() instead of calling setOnCancelListener() from onCreateDialog() [DialogFragmentCallbacksDetector]
+        dialog.setOnCancelListener { }
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+0 errors, 1 warnings
+            """
+            )
+            .expectWarningCount(1)
+    }
+
+    @Test
+    fun `kotlin expect fail dialog fragment with dismiss listener`() {
+        lint().files(dialogFragmentStubKotlinWithDismissListener)
+            .run()
+            .expect(
+                """
+src/foo/TestDialog.kt:10: Warning: Use onDismiss() instead of calling setOnDismissListener() from onCreateDialog() [DialogFragmentCallbacksDetector]
+        dialog.setOnDismissListener { }
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+0 errors, 1 warnings
+            """
+            )
+            .expectWarningCount(1)
+    }
+
+    @Test
+    fun `kotlin expect fail dialog fragment with dismiss and cancel listeners`() {
+        lint().files(dialogFragmentStubKotlinWithDismissAndCancelListeners)
+            .run()
+            .expect(
+                """
+src/foo/TestDialog.kt:10: Warning: Use onDismiss() instead of calling setOnDismissListener() from onCreateDialog() [DialogFragmentCallbacksDetector]
+        dialog.setOnDismissListener { }
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+src/foo/TestDialog.kt:11: Warning: Use onCancel() instead of calling setOnCancelListener() from onCreateDialog() [DialogFragmentCallbacksDetector]
+        dialog.setOnCancelListener { }
+        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+0 errors, 2 warnings
+            """
+            )
+            .expectWarningCount(2)
+    }
+
+    @Test
+    fun `kotlin expect clean dialog fragment`() {
+        lint().files(dialogFragmentCorrectImplementationStubKotlin)
+            .run()
+            .expectClean()
+    }
+}