1 /*
<lambda>null2  * Copyright 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.ui.window
18 
19 import android.content.Context
20 import android.graphics.Outline
21 import android.os.Build
22 import android.view.ContextThemeWrapper
23 import android.view.Gravity
24 import android.view.KeyEvent
25 import android.view.MotionEvent
26 import android.view.View
27 import android.view.ViewGroup
28 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
29 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
30 import android.view.ViewOutlineProvider
31 import android.view.Window
32 import android.view.WindowManager
33 import androidx.activity.ComponentDialog
34 import androidx.activity.addCallback
35 import androidx.annotation.DoNotInline
36 import androidx.annotation.RequiresApi
37 import androidx.compose.runtime.Composable
38 import androidx.compose.runtime.CompositionContext
39 import androidx.compose.runtime.DisposableEffect
40 import androidx.compose.runtime.Immutable
41 import androidx.compose.runtime.LaunchedEffect
42 import androidx.compose.runtime.SideEffect
43 import androidx.compose.runtime.getValue
44 import androidx.compose.runtime.mutableStateOf
45 import androidx.compose.runtime.remember
46 import androidx.compose.runtime.rememberCompositionContext
47 import androidx.compose.runtime.rememberUpdatedState
48 import androidx.compose.runtime.saveable.rememberSaveable
49 import androidx.compose.runtime.setValue
50 import androidx.compose.ui.Modifier
51 import androidx.compose.ui.R
52 import androidx.compose.ui.layout.Layout
53 import androidx.compose.ui.platform.AbstractComposeView
54 import androidx.compose.ui.platform.LocalDensity
55 import androidx.compose.ui.platform.LocalLayoutDirection
56 import androidx.compose.ui.platform.LocalView
57 import androidx.compose.ui.platform.ViewRootForInspector
58 import androidx.compose.ui.semantics.dialog
59 import androidx.compose.ui.semantics.semantics
60 import androidx.compose.ui.unit.Density
61 import androidx.compose.ui.unit.LayoutDirection
62 import androidx.compose.ui.unit.dp
63 import androidx.compose.ui.util.fastCoerceAtLeast
64 import androidx.compose.ui.util.fastForEach
65 import androidx.compose.ui.util.fastMap
66 import androidx.core.graphics.Insets
67 import androidx.core.view.OnApplyWindowInsetsListener
68 import androidx.core.view.ViewCompat
69 import androidx.core.view.WindowCompat
70 import androidx.core.view.WindowInsetsAnimationCompat
71 import androidx.core.view.WindowInsetsCompat
72 import androidx.lifecycle.findViewTreeLifecycleOwner
73 import androidx.lifecycle.findViewTreeViewModelStoreOwner
74 import androidx.lifecycle.setViewTreeLifecycleOwner
75 import androidx.lifecycle.setViewTreeViewModelStoreOwner
76 import androidx.savedstate.findViewTreeSavedStateRegistryOwner
77 import androidx.savedstate.setViewTreeSavedStateRegistryOwner
78 import java.util.UUID
79 import kotlin.math.max
80 import kotlin.math.roundToInt
81 
82 /**
83  * Properties used to customize the behavior of a [Dialog].
84  *
85  * @property dismissOnBackPress whether the dialog can be dismissed by pressing the back or escape
86  *   buttons. If true, pressing the back button will call onDismissRequest.
87  * @property dismissOnClickOutside whether the dialog can be dismissed by clicking outside the
88  *   dialog's bounds. If true, clicking outside the dialog will call onDismissRequest.
89  * @property securePolicy Policy for setting [WindowManager.LayoutParams.FLAG_SECURE] on the
90  *   dialog's window.
91  * @property usePlatformDefaultWidth Whether the width of the dialog's content should be limited to
92  *   the platform default, which is smaller than the screen width. It is recommended to use
93  *   [decorFitsSystemWindows] set to `false` when [usePlatformDefaultWidth] is false to support
94  *   using the entire screen and avoiding UI glitches on some devices when the IME animates in.
95  * @property decorFitsSystemWindows Sets [WindowCompat.setDecorFitsSystemWindows] value. Set to
96  *   `false` to use WindowInsets. If `false`, the
97  *   [soft input mode][WindowManager.LayoutParams.softInputMode] will be changed to
98  *   [WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE] on [Build.VERSION_CODES.R] and below and
99  *   [WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING] on [Build.VERSION_CODES.S] and above.
100  *   [Window.isFloating] will be `false` when `decorFitsSystemWindows` is `false`.
101  */
102 @Immutable
103 actual class DialogProperties(
104     actual val dismissOnBackPress: Boolean = true,
105     actual val dismissOnClickOutside: Boolean = true,
106     val securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit,
107     actual val usePlatformDefaultWidth: Boolean = true,
108     val decorFitsSystemWindows: Boolean = true
109 ) {
110     actual constructor(
111         dismissOnBackPress: Boolean,
112         dismissOnClickOutside: Boolean,
113         usePlatformDefaultWidth: Boolean,
114     ) : this(
115         dismissOnBackPress = dismissOnBackPress,
116         dismissOnClickOutside = dismissOnClickOutside,
117         securePolicy = SecureFlagPolicy.Inherit,
118         usePlatformDefaultWidth = usePlatformDefaultWidth,
119         decorFitsSystemWindows = true
120     )
121 
122     @Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN)
123     constructor(
124         dismissOnBackPress: Boolean = true,
125         dismissOnClickOutside: Boolean = true,
126         securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit,
127     ) : this(
128         dismissOnBackPress = dismissOnBackPress,
129         dismissOnClickOutside = dismissOnClickOutside,
130         securePolicy = securePolicy,
131         usePlatformDefaultWidth = true,
132         decorFitsSystemWindows = true
133     )
134 
135     override fun equals(other: Any?): Boolean {
136         if (this === other) return true
137         if (other !is DialogProperties) return false
138 
139         if (dismissOnBackPress != other.dismissOnBackPress) return false
140         if (dismissOnClickOutside != other.dismissOnClickOutside) return false
141         if (securePolicy != other.securePolicy) return false
142         if (usePlatformDefaultWidth != other.usePlatformDefaultWidth) return false
143         if (decorFitsSystemWindows != other.decorFitsSystemWindows) return false
144 
145         return true
146     }
147 
148     override fun hashCode(): Int {
149         var result = dismissOnBackPress.hashCode()
150         result = 31 * result + dismissOnClickOutside.hashCode()
151         result = 31 * result + securePolicy.hashCode()
152         result = 31 * result + usePlatformDefaultWidth.hashCode()
153         result = 31 * result + decorFitsSystemWindows.hashCode()
154         return result
155     }
156 }
157 
158 /**
159  * Opens a dialog with the given content.
160  *
161  * A dialog is a small window that prompts the user to make a decision or enter additional
162  * information. A dialog does not fill the screen and is normally used for modal events that require
163  * users to take an action before they can proceed.
164  *
165  * The dialog is visible as long as it is part of the composition hierarchy. In order to let the
166  * user dismiss the Dialog, the implementation of [onDismissRequest] should contain a way to remove
167  * the dialog from the composition hierarchy.
168  *
169  * Example usage:
170  *
171  * @sample androidx.compose.ui.samples.DialogSample
172  * @param onDismissRequest Executes when the user tries to dismiss the dialog.
173  * @param properties [DialogProperties] for further customization of this dialog's behavior.
174  * @param content The content to be displayed inside the dialog.
175  */
176 @Composable
177 actual fun Dialog(
178     onDismissRequest: () -> Unit,
179     properties: DialogProperties,
180     content: @Composable () -> Unit
181 ) {
182     val view = LocalView.current
183     val density = LocalDensity.current
184     val layoutDirection = LocalLayoutDirection.current
185     val composition = rememberCompositionContext()
186     val currentContent by rememberUpdatedState(content)
<lambda>null187     val dialogId = rememberSaveable { UUID.randomUUID() }
188     val dialog =
<lambda>null189         remember(view, density) {
190             DialogWrapper(onDismissRequest, properties, view, layoutDirection, density, dialogId)
191                 .apply {
192                     setContent(composition) {
193                         DialogLayout(Modifier.semantics { dialog() }, currentContent)
194                     }
195                 }
196         }
197 
<lambda>null198     LaunchedEffect(Unit) { dialog.show() }
199 
<lambda>null200     DisposableEffect(dialog) {
201         onDispose {
202             dialog.dismiss()
203             dialog.disposeComposition()
204         }
205     }
206 
<lambda>null207     SideEffect {
208         dialog.updateParameters(
209             onDismissRequest = onDismissRequest,
210             properties = properties,
211             layoutDirection = layoutDirection
212         )
213     }
214 }
215 
216 /**
217  * Provides the underlying window of a dialog.
218  *
219  * Implemented by dialog's root layout.
220  */
221 interface DialogWindowProvider {
222     val window: Window
223 }
224 
225 @Suppress("ViewConstructor")
226 private class DialogLayout(context: Context, override val window: Window) :
227     AbstractComposeView(context), DialogWindowProvider, OnApplyWindowInsetsListener {
228 
229     private var content: @Composable () -> Unit by mutableStateOf({})
230 
231     private var usePlatformDefaultWidth = false
232     private var decorFitsSystemWindows = false
233     private var hasCalledSetLayout = false
234 
235     override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
236         private set
237 
<lambda>null238     init {
239         ViewCompat.setOnApplyWindowInsetsListener(this, this)
240         ViewCompat.setWindowInsetsAnimationCallback(
241             this,
242             object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
243                 override fun onStart(
244                     animation: WindowInsetsAnimationCompat,
245                     bounds: WindowInsetsAnimationCompat.BoundsCompat
246                 ): WindowInsetsAnimationCompat.BoundsCompat =
247                     insetValue(bounds) { l, t, r, b -> bounds.inset(Insets.of(l, t, r, b)) }
248 
249                 override fun onProgress(
250                     insets: WindowInsetsCompat,
251                     runningAnimations: MutableList<WindowInsetsAnimationCompat>
252                 ): WindowInsetsCompat =
253                     insetValue(insets) { l, t, r, b -> insets.inset(l, t, r, b) }
254             }
255         )
256     }
257 
updatePropertiesnull258     fun updateProperties(usePlatformDefaultWidth: Boolean, decorFitsSystemWindows: Boolean) {
259         val callSetLayout =
260             !hasCalledSetLayout ||
261                 usePlatformDefaultWidth != this.usePlatformDefaultWidth ||
262                 decorFitsSystemWindows != this.decorFitsSystemWindows
263         this.usePlatformDefaultWidth = usePlatformDefaultWidth
264         this.decorFitsSystemWindows = decorFitsSystemWindows
265 
266         if (callSetLayout) {
267             val attrs = window.attributes
268             val measurementWidth = if (usePlatformDefaultWidth) WRAP_CONTENT else MATCH_PARENT
269             if (measurementWidth != attrs.width || !hasCalledSetLayout) {
270                 // Always use WRAP_CONTENT for height. internalOnMeasure() will change
271                 // it to MATCH_PARENT if it needs more height. If we use MATCH_PARENT here,
272                 // and change to WRAP_CONTENT in internalOnMeasure(), the window size will
273                 // be wrong on the first frame.
274                 window.setLayout(measurementWidth, WRAP_CONTENT)
275                 hasCalledSetLayout = true
276             }
277         }
278     }
279 
internalOnMeasurenull280     override fun internalOnMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
281         val child = getChildAt(0)
282         if (child == null) {
283             super.internalOnMeasure(widthMeasureSpec, heightMeasureSpec)
284             return
285         }
286         val width = MeasureSpec.getSize(widthMeasureSpec)
287         val height = MeasureSpec.getSize(heightMeasureSpec)
288         val heightMode = MeasureSpec.getMode(heightMeasureSpec)
289         val targetHeight =
290             if (
291                 heightMode == MeasureSpec.AT_MOST &&
292                     !usePlatformDefaultWidth &&
293                     !decorFitsSystemWindows &&
294                     window.attributes.height == WRAP_CONTENT
295             ) {
296                 // Any size larger than the WRAP_CONTENT to test to see if this is full-screen
297                 // content.
298                 height + 1
299             } else {
300                 height
301             }
302 
303         val horizontalPadding = paddingLeft + paddingRight
304         val verticalPadding = paddingTop + paddingBottom
305         val remainingWidth = (width - horizontalPadding).fastCoerceAtLeast(0)
306         val remainingHeight = (targetHeight - verticalPadding).fastCoerceAtLeast(0)
307 
308         val widthMode = MeasureSpec.getMode(widthMeasureSpec)
309         val childWidthSpec =
310             if (widthMode == MeasureSpec.UNSPECIFIED) {
311                 widthMeasureSpec
312             } else {
313                 MeasureSpec.makeMeasureSpec(remainingWidth, MeasureSpec.AT_MOST)
314             }
315         val childHeightSpec =
316             if (heightMode == MeasureSpec.UNSPECIFIED) {
317                 heightMeasureSpec
318             } else {
319                 MeasureSpec.makeMeasureSpec(remainingHeight, MeasureSpec.AT_MOST)
320             }
321         child.measure(childWidthSpec, childHeightSpec)
322 
323         // respect passed dimensions
324         val measuredWidth =
325             when (widthMode) {
326                 MeasureSpec.EXACTLY -> width
327                 MeasureSpec.AT_MOST -> minOf(width, child.measuredWidth + horizontalPadding)
328                 else -> child.measuredWidth + horizontalPadding
329             }
330         val measuredHeight =
331             when (heightMode) {
332                 MeasureSpec.EXACTLY -> height
333                 MeasureSpec.AT_MOST -> minOf(height, child.measuredHeight + verticalPadding)
334                 else -> child.measuredHeight + verticalPadding
335             }
336         setMeasuredDimension(measuredWidth, measuredHeight)
337 
338         if (
339             !decorFitsSystemWindows &&
340                 child.measuredHeight + verticalPadding > height &&
341                 window.attributes.height == WRAP_CONTENT
342         ) {
343             // We're going to use the full screen, so don't put a background behind the system bars
344             window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
345             if (!usePlatformDefaultWidth) {
346                 // The size of the window is too small with WRAP_CONTENT for height. Change it
347                 // to use MATCH_PARENT to give as much room as possible
348                 window.setLayout(MATCH_PARENT, MATCH_PARENT)
349             }
350         }
351     }
352 
internalOnLayoutnull353     override fun internalOnLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
354         val child = getChildAt(0) ?: return
355 
356         // center content
357         val hPadding = paddingLeft + paddingRight
358         val vPadding = paddingTop + paddingBottom
359         val width = right - left
360         val height = bottom - top
361         val childWidth = child.measuredWidth
362         val childHeight = child.measuredHeight
363 
364         val extraWidth = width - childWidth - hPadding
365         val extraHeight = height - childHeight - vPadding
366 
367         val l = paddingLeft + (extraWidth / 2)
368         val t = paddingTop + (extraHeight / 2)
369         val r = l + childWidth
370         val b = t + childHeight
371         child.layout(l, t, r, b)
372     }
373 
setContentnull374     fun setContent(parent: CompositionContext, content: @Composable () -> Unit) {
375         setParentCompositionContext(parent)
376         this.content = content
377         shouldCreateCompositionOnAttachedToWindow = true
378         createComposition()
379     }
380 
onApplyWindowInsetsnull381     override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat =
382         insetValue(insets) { l, t, r, b -> insets.inset(l, t, r, b) }
383 
insetValuenull384     private inline fun <T> insetValue(
385         unchangedValue: T,
386         block: (left: Int, top: Int, right: Int, bottom: Int) -> T
387     ): T {
388         if (decorFitsSystemWindows) {
389             return unchangedValue
390         }
391         val child = getChildAt(0)
392         val left = maxOf(0, child.left)
393         val top = maxOf(0, child.top)
394         val right = maxOf(0, width - child.right)
395         val bottom = maxOf(0, height - child.bottom)
396         return if (left == 0 && top == 0 && right == 0 && bottom == 0) {
397             unchangedValue
398         } else {
399             block(left, top, right, bottom)
400         }
401     }
402 
isInsideContentnull403     fun isInsideContent(event: MotionEvent): Boolean {
404         if (!event.x.isFinite() || !event.y.isFinite()) return false
405         val child = getChildAt(0) ?: return false
406         val left = left + child.left
407         val right = left + child.width
408         val top = top + child.top
409         val bottom = top + child.height
410         return event.x.roundToInt() in left..right && event.y.roundToInt() in top..bottom
411     }
412 
413     @Composable
Contentnull414     override fun Content() {
415         content()
416     }
417 }
418 
419 private class DialogWrapper(
420     private var onDismissRequest: () -> Unit,
421     private var properties: DialogProperties,
422     private val composeView: View,
423     layoutDirection: LayoutDirection,
424     density: Density,
425     dialogId: UUID
426 ) :
427     ComponentDialog(
428         /**
429          * [Window.setClipToOutline] is only available from 22+, but the style attribute exists
430          * on 21. So use a wrapped context that sets this attribute for compatibility back to 21.
431          */
432         ContextThemeWrapper(
433             composeView.context,
434             if (properties.decorFitsSystemWindows) {
435                 R.style.DialogWindowTheme
436             } else {
437                 R.style.FloatingDialogWindowTheme
438             }
439         )
440     ),
441     ViewRootForInspector {
442 
443     private val dialogLayout: DialogLayout
444 
445     // On systems older than Android S, there is a bug in the surface insets matrix math used by
446     // elevation, so high values of maxSupportedElevation break accessibility services: b/232788477.
447     private val maxSupportedElevation = 8.dp
448 
449     private var isPressOutside = false
450 
451     override val subCompositionView: AbstractComposeView
452         get() = dialogLayout
453 
<lambda>null454     init {
455         val window = window ?: error("Dialog has no window")
456         window.requestFeature(Window.FEATURE_NO_TITLE)
457         window.setBackgroundDrawableResource(android.R.color.transparent)
458         WindowCompat.setDecorFitsSystemWindows(window, properties.decorFitsSystemWindows)
459         window.setGravity(Gravity.CENTER)
460         if (!properties.decorFitsSystemWindows) {
461             @Suppress("DEPRECATION")
462             window.addFlags(
463                 WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR or
464                     WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
465             )
466             val attrs = window.attributes
467             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
468                 Api30Impl.setFitInsetsSides(attrs, 0)
469                 Api30Impl.setFitInsetsTypes(attrs, 0)
470             }
471             window.attributes = attrs
472         }
473 
474         dialogLayout =
475             DialogLayout(context, window).apply {
476                 // Set unique id for AbstractComposeView. This allows state restoration for the
477                 // state
478                 // defined inside the Dialog via rememberSaveable()
479                 setTag(R.id.compose_view_saveable_id_tag, "Dialog:$dialogId")
480                 // Enable children to draw their shadow by not clipping them
481                 clipChildren = false
482                 // Allocate space for elevation
483                 with(density) { elevation = maxSupportedElevation.toPx() }
484                 // Simple outline to force window manager to allocate space for shadow.
485                 // Note that the outline affects clickable area for the dismiss listener. In case of
486                 // shapes like circle the area for dismiss might be to small (rectangular outline
487                 // consuming clicks outside of the circle).
488                 outlineProvider =
489                     object : ViewOutlineProvider() {
490                         override fun getOutline(view: View, result: Outline) {
491                             result.setRect(0, 0, view.width, view.height)
492                             // We set alpha to 0 to hide the view's shadow and let the composable to
493                             // draw its own shadow. This still enables us to get the extra space
494                             // needed in the surface.
495                             result.alpha = 0f
496                         }
497                     }
498             }
499 
500         /**
501          * Disables clipping for [this] and all its descendant [ViewGroup]s until we reach a
502          * [DialogLayout] (the [ViewGroup] containing the Compose hierarchy).
503          */
504         fun ViewGroup.disableClipping() {
505             clipChildren = false
506             if (this is DialogLayout) return
507             for (i in 0 until childCount) {
508                 (getChildAt(i) as? ViewGroup)?.disableClipping()
509             }
510         }
511 
512         // Turn of all clipping so shadows can be drawn outside the window
513         (window.decorView as? ViewGroup)?.disableClipping()
514 
515         setContentView(dialogLayout)
516         dialogLayout.setViewTreeLifecycleOwner(composeView.findViewTreeLifecycleOwner())
517         dialogLayout.setViewTreeViewModelStoreOwner(composeView.findViewTreeViewModelStoreOwner())
518         dialogLayout.setViewTreeSavedStateRegistryOwner(
519             composeView.findViewTreeSavedStateRegistryOwner()
520         )
521 
522         // Initial setup
523         updateParameters(onDismissRequest, properties, layoutDirection)
524 
525         // Due to how the onDismissRequest callback works
526         // (it enforces a just-in-time decision on whether to update the state to hide the dialog)
527         // we need to unconditionally add a callback here that is always enabled,
528         // meaning we'll never get a system UI controlled predictive back animation
529         // for these dialogs
530         onBackPressedDispatcher.addCallback(this) {
531             if (properties.dismissOnBackPress) {
532                 onDismissRequest()
533             }
534         }
535     }
536 
onKeyUpnull537     override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
538         if (
539             properties.dismissOnBackPress &&
540                 event.isTracking &&
541                 !event.isCanceled &&
542                 keyCode == KeyEvent.KEYCODE_ESCAPE
543         ) {
544             onDismissRequest()
545             return true
546         }
547         return super.onKeyUp(keyCode, event)
548     }
549 
setLayoutDirectionnull550     private fun setLayoutDirection(layoutDirection: LayoutDirection) {
551         dialogLayout.layoutDirection =
552             when (layoutDirection) {
553                 LayoutDirection.Ltr -> android.util.LayoutDirection.LTR
554                 LayoutDirection.Rtl -> android.util.LayoutDirection.RTL
555             }
556     }
557 
setContentnull558     fun setContent(parentComposition: CompositionContext, children: @Composable () -> Unit) {
559         dialogLayout.setContent(parentComposition, children)
560     }
561 
setSecurePolicynull562     private fun setSecurePolicy(securePolicy: SecureFlagPolicy) {
563         val secureFlagEnabled =
564             securePolicy.shouldApplySecureFlag(composeView.isFlagSecureEnabled())
565         window!!.setFlags(
566             if (secureFlagEnabled) {
567                 WindowManager.LayoutParams.FLAG_SECURE
568             } else {
569                 WindowManager.LayoutParams.FLAG_SECURE.inv()
570             },
571             WindowManager.LayoutParams.FLAG_SECURE
572         )
573     }
574 
updateParametersnull575     fun updateParameters(
576         onDismissRequest: () -> Unit,
577         properties: DialogProperties,
578         layoutDirection: LayoutDirection
579     ) {
580         this.onDismissRequest = onDismissRequest
581         this.properties = properties
582         setSecurePolicy(properties.securePolicy)
583         setLayoutDirection(layoutDirection)
584         val decorFitsSystemWindows = properties.decorFitsSystemWindows
585         dialogLayout.updateProperties(
586             usePlatformDefaultWidth = properties.usePlatformDefaultWidth,
587             decorFitsSystemWindows = decorFitsSystemWindows
588         )
589         setCanceledOnTouchOutside(properties.dismissOnClickOutside)
590         val window = window
591         if (window != null) {
592             val softInput =
593                 when {
594                     decorFitsSystemWindows ->
595                         WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED
596                     Build.VERSION.SDK_INT < Build.VERSION_CODES.S ->
597                         @Suppress("DEPRECATION") WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
598                     else -> WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING
599                 }
600             window.setSoftInputMode(softInput)
601         }
602     }
603 
disposeCompositionnull604     fun disposeComposition() {
605         dialogLayout.disposeComposition()
606     }
607 
onTouchEventnull608     override fun onTouchEvent(event: MotionEvent): Boolean {
609         var result = super.onTouchEvent(event)
610         if (properties.dismissOnClickOutside && !dialogLayout.isInsideContent(event)) {
611             when (event.actionMasked) {
612                 MotionEvent.ACTION_DOWN -> {
613                     isPressOutside = true
614                     result = true
615                 }
616                 MotionEvent.ACTION_UP ->
617                     if (isPressOutside) {
618                         onDismissRequest()
619                         result = true
620                         isPressOutside = false
621                     }
622                 MotionEvent.ACTION_CANCEL -> isPressOutside = false
623             }
624         } else {
625             when (event.actionMasked) {
626                 MotionEvent.ACTION_DOWN,
627                 MotionEvent.ACTION_UP,
628                 MotionEvent.ACTION_CANCEL -> isPressOutside = false
629             }
630         }
631 
632         return result
633     }
634 
cancelnull635     override fun cancel() {
636         // Prevents the dialog from dismissing itself
637         return
638     }
639 }
640 
641 @Composable
DialogLayoutnull642 private fun DialogLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
643     Layout(content = content, modifier = modifier) { measurables, constraints ->
644         var maxWidth = 0
645         var maxHeight = 0
646         val placeables =
647             measurables.fastMap {
648                 it.measure(constraints).apply {
649                     maxWidth = max(maxWidth, width)
650                     maxHeight = max(maxHeight, height)
651                 }
652             }
653         if (measurables.isEmpty()) {
654             maxWidth = constraints.minWidth
655             maxHeight = constraints.minHeight
656         }
657         layout(maxWidth, maxHeight) { placeables.fastForEach { it.placeRelative(0, 0) } }
658     }
659 }
660 
661 @RequiresApi(30)
662 private object Api30Impl {
663     @DoNotInline
setFitInsetsSidesnull664     fun setFitInsetsSides(attrs: WindowManager.LayoutParams, sides: Int) {
665         attrs.setFitInsetsSides(sides)
666     }
667 
668     @DoNotInline
setFitInsetsTypesnull669     fun setFitInsetsTypes(attrs: WindowManager.LayoutParams, types: Int) {
670         attrs.setFitInsetsTypes(types)
671     }
672 }
673