Properly support Dialog.dismissOnClickOutside

Fixes: 364508685

The dialog should support dismissing when clicking outside.
This fixes the support when decorFitsSystemWindows = false.

Test: manual testing, new tests
Change-Id: If49301c4fbe826dceaa5680a523c05fc1e95c816
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt
index ce12b6d..9885522 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt
@@ -106,6 +106,20 @@
     }
 
     @Test
+    fun dialogTest_isNotDismissed_whenClicked_noClickableContent() {
+        setupDialogTest { DefaultDialogContent() }
+
+        val interaction = rule.onNodeWithTag(testTag)
+        interaction.assertIsDisplayed()
+
+        // Click inside the dialog
+        interaction.performClick()
+
+        // Check that the Clickable was pressed and the Dialog is still visible.
+        interaction.assertIsDisplayed()
+    }
+
+    @Test
     fun dialogTest_isDismissed_whenSpecified() {
         setupDialogTest()
         val textInteraction = rule.onNodeWithTag(testTag)
@@ -116,6 +130,16 @@
     }
 
     @Test
+    fun dialogTest_isDismissed_whenSpecified_decorFitsFalse() {
+        setupDialogTest(dialogProperties = DialogProperties(decorFitsSystemWindows = false))
+        val textInteraction = rule.onNodeWithTag(testTag)
+        textInteraction.assertIsDisplayed()
+
+        clickOutsideDialog()
+        textInteraction.assertDoesNotExist()
+    }
+
+    @Test
     fun dialogTest_isNotDismissed_whenNotSpecified() {
         setupDialogTest(closeDialogOnDismiss = false)
         val textInteraction = rule.onNodeWithTag(testTag)
@@ -138,6 +162,20 @@
     }
 
     @Test
+    fun dialogTest_isNotDismissed_whenDismissOnClickOutsideIsFalse_decorFitsFalse() {
+        setupDialogTest(
+            dialogProperties =
+                DialogProperties(dismissOnClickOutside = false, decorFitsSystemWindows = false)
+        )
+        val textInteraction = rule.onNodeWithTag(testTag)
+        textInteraction.assertIsDisplayed()
+
+        clickOutsideDialog()
+        // The Dialog should still be visible
+        textInteraction.assertIsDisplayed()
+    }
+
+    @Test
     fun dialogTest_isDismissed_whenSpecified_backButtonPressed() {
         setupDialogTest()
         val textInteraction = rule.onNodeWithTag(testTag)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
index 8e4f291..cdf731a 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
@@ -25,6 +25,7 @@
 import android.view.MotionEvent
 import android.view.View
 import android.view.View.OnLayoutChangeListener
+import android.view.View.OnTouchListener
 import android.view.ViewGroup
 import android.view.ViewOutlineProvider
 import android.view.Window
@@ -73,6 +74,7 @@
 import androidx.savedstate.findViewTreeSavedStateRegistryOwner
 import androidx.savedstate.setViewTreeSavedStateRegistryOwner
 import java.util.UUID
+import kotlin.math.roundToInt
 
 /**
  * Properties used to customize the behavior of a [Dialog].
@@ -274,7 +276,8 @@
     ),
     ViewRootForInspector,
     OnApplyWindowInsetsListener,
-    OnLayoutChangeListener {
+    OnLayoutChangeListener,
+    OnTouchListener {
 
     private val dialogLayout: DialogLayout
 
@@ -341,7 +344,6 @@
                 )
                 .also { it.gravity = Gravity.CENTER }
         )
-        frameLayout.setOnClickListener { onDismissRequest() }
         ViewCompat.setOnApplyWindowInsetsListener(frameLayout, this)
         ViewCompat.setWindowInsetsAnimationCallback(
             frameLayout,
@@ -454,6 +456,9 @@
         dialogLayout.usePlatformDefaultWidth = properties.usePlatformDefaultWidth
         val decorFitsSystemWindows = adjustedDecorFitsSystemWindows(properties, context)
         dialogLayout.decorFitsSystemWindows = decorFitsSystemWindows
+        setCanceledOnTouchOutside(properties.dismissOnClickOutside)
+        val frameLayout = dialogLayout.parent as View
+        frameLayout.setOnTouchListener(if (properties.dismissOnClickOutside) this else null)
         val window = window
         if (window != null) {
             val softInput =
@@ -533,6 +538,24 @@
     ) {
         v.requestApplyInsets()
     }
+
+    override fun onTouch(v: View, event: MotionEvent): Boolean {
+        // This handler only set when properties.dismissOnClickOutside is true
+        if (event.actionMasked == MotionEvent.ACTION_UP) {
+            val x = event.x.roundToInt()
+            val y = event.y.roundToInt()
+            val insideContent =
+                x in dialogLayout.left..dialogLayout.right &&
+                    y in dialogLayout.top..dialogLayout.bottom
+            if (!insideContent) {
+                onDismissRequest()
+                return true
+            }
+        }
+        // We must always accept the ACTION_DOWN or else we don't receive the rest of the
+        // event stream.
+        return event.actionMasked == MotionEvent.ACTION_DOWN
+    }
 }
 
 @Composable