• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2023 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 com.android.systemui.statusbar.phone
18 
19 import android.app.Dialog
20 import android.content.Context
21 import android.content.res.Configuration
22 import android.os.Bundle
23 import androidx.annotation.GravityInt
24 import androidx.compose.foundation.clickable
25 import androidx.compose.foundation.gestures.AnchoredDraggableDefaults
26 import androidx.compose.foundation.gestures.AnchoredDraggableState
27 import androidx.compose.foundation.gestures.DraggableAnchors
28 import androidx.compose.foundation.gestures.Orientation
29 import androidx.compose.foundation.gestures.anchoredDraggable
30 import androidx.compose.foundation.gestures.detectTapGestures
31 import androidx.compose.foundation.interaction.MutableInteractionSource
32 import androidx.compose.foundation.interaction.collectIsDraggedAsState
33 import androidx.compose.foundation.layout.Box
34 import androidx.compose.foundation.layout.Column
35 import androidx.compose.foundation.layout.WindowInsets
36 import androidx.compose.foundation.layout.offset
37 import androidx.compose.foundation.layout.padding
38 import androidx.compose.foundation.layout.safeDrawing
39 import androidx.compose.foundation.layout.size
40 import androidx.compose.foundation.layout.widthIn
41 import androidx.compose.foundation.layout.wrapContentWidth
42 import androidx.compose.foundation.shape.RoundedCornerShape
43 import androidx.compose.material3.LocalContentColor
44 import androidx.compose.material3.MaterialTheme
45 import androidx.compose.material3.Surface
46 import androidx.compose.runtime.Composable
47 import androidx.compose.runtime.CompositionLocalProvider
48 import androidx.compose.runtime.LaunchedEffect
49 import androidx.compose.runtime.getValue
50 import androidx.compose.runtime.remember
51 import androidx.compose.ui.Alignment
52 import androidx.compose.ui.Modifier
53 import androidx.compose.ui.input.pointer.pointerInput
54 import androidx.compose.ui.layout.onSizeChanged
55 import androidx.compose.ui.platform.ComposeView
56 import androidx.compose.ui.platform.LocalConfiguration
57 import androidx.compose.ui.platform.LocalDensity
58 import androidx.compose.ui.platform.LocalLayoutDirection
59 import androidx.compose.ui.res.dimensionResource
60 import androidx.compose.ui.res.stringResource
61 import androidx.compose.ui.semantics.contentDescription
62 import androidx.compose.ui.semantics.hideFromAccessibility
63 import androidx.compose.ui.semantics.semantics
64 import androidx.compose.ui.unit.Dp
65 import androidx.compose.ui.unit.IntOffset
66 import androidx.compose.ui.unit.dp
67 import androidx.compose.ui.unit.isSpecified
68 import com.android.compose.theme.PlatformTheme
69 import com.android.systemui.keyboard.shortcut.ui.composable.hasCompactWindowSize
70 import com.android.systemui.res.R
71 import kotlin.math.roundToInt
72 
73 /**
74  * Create a [SystemUIDialog] with the given [content].
75  *
76  * Note that the returned dialog will already have a background so the content should not draw an
77  * additional background.
78  *
79  * Example:
80  * ```
81  * val dialog = systemUiDialogFactory.create {
82  *   AlertDialogContent(
83  *     title = { Text("My title") },
84  *     content = { Text("My content") },
85  *   )
86  * }
87  *
88  * dialogTransitionAnimator.showFromView(dialog, viewThatWasClicked)
89  * ```
90  *
91  * @param context the [Context] in which the dialog will be constructed.
92  * @param dismissOnDeviceLock whether the dialog should be automatically dismissed when the device
93  *   is locked (true by default).
94  * @param dialogGravity is one of the [android.view.Gravity] and determines dialog position on the
95  *   screen.
96  */
97 fun SystemUIDialogFactory.create(
98     context: Context = this.applicationContext,
99     theme: Int = SystemUIDialog.DEFAULT_THEME,
100     dismissOnDeviceLock: Boolean = SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK,
101     @GravityInt dialogGravity: Int? = null,
102     dialogDelegate: DialogDelegate<SystemUIDialog> =
103         object : DialogDelegate<SystemUIDialog> {
104             override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
105                 super.onCreate(dialog, savedInstanceState)
106                 dialogGravity?.let { dialog.window?.setGravity(it) }
107             }
108         },
109     content: @Composable (SystemUIDialog) -> Unit,
110 ): ComponentSystemUIDialog {
111     return create(
112         context = context,
113         theme = theme,
114         dismissOnDeviceLock = dismissOnDeviceLock,
115         delegate = dialogDelegate,
116         content = content,
117     )
118 }
119 
120 /** Same as [create] but creates a bottom sheet dialog. */
SystemUIDialogFactorynull121 fun SystemUIDialogFactory.createBottomSheet(
122     context: Context = this.applicationContext,
123     theme: Int = R.style.Theme_SystemUI_BottomSheet,
124     dismissOnDeviceLock: Boolean = SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK,
125     content: @Composable (SystemUIDialog) -> Unit,
126     isDraggable: Boolean = true,
127     // TODO(b/337205027): remove maxWidth parameter when aligned to M3 spec
128     maxWidth: Dp = Dp.Unspecified,
129 ): ComponentSystemUIDialog {
130     return create(
131         context = context,
132         theme = theme,
133         dismissOnDeviceLock = dismissOnDeviceLock,
134         delegate = EdgeToEdgeDialogDelegate(),
135         content = { dialog ->
136             val dragState =
137                 if (isDraggable)
138                     remember { AnchoredDraggableState(initialValue = DragAnchors.Start) }
139                 else null
140             val interactionSource =
141                 if (isDraggable) remember { MutableInteractionSource() } else null
142             if (dragState != null) {
143                 val isDragged by interactionSource!!.collectIsDraggedAsState()
144                 LaunchedEffect(dragState.currentValue, isDragged) {
145                     if (!isDragged && dragState.currentValue == DragAnchors.End) dialog.dismiss()
146                 }
147             }
148             Box(
149                 modifier =
150                     Modifier.bottomSheetClickable { dialog.dismiss() }
151                         .then(
152                             if (isDraggable)
153                                 Modifier.anchoredDraggable(
154                                         state = dragState!!,
155                                         interactionSource = interactionSource,
156                                         orientation = Orientation.Vertical,
157                                         flingBehavior =
158                                             AnchoredDraggableDefaults.flingBehavior(
159                                                 state = dragState
160                                             ),
161                                     )
162                                     .offset {
163                                         IntOffset(x = 0, y = dragState.requireOffset().roundToInt())
164                                     }
165                                     .onSizeChanged { layoutSize ->
166                                         val dragEndPoint = layoutSize.height - dialog.height
167                                         dragState.updateAnchors(
168                                             DraggableAnchors {
169                                                 DragAnchors.entries.forEach { anchor ->
170                                                     anchor at dragEndPoint * anchor.fraction
171                                                 }
172                                             }
173                                         )
174                                     }
175                                     .padding(top = draggableTopPadding())
176                             else Modifier // No-Op
177                         ),
178                 contentAlignment = Alignment.BottomCenter,
179             ) {
180                 val radius = dimensionResource(R.dimen.bottom_sheet_corner_radius)
181                 Surface(
182                     modifier =
183                         Modifier.bottomSheetPaddings()
184                             // consume input so it doesn't get to the parent Composable
185                             .bottomSheetClickable {}
186                             .widthIn(
187                                 max =
188                                     if (maxWidth.isSpecified) maxWidth
189                                     else DraggableBottomSheet.MaxWidth
190                             ),
191                     shape = RoundedCornerShape(topStart = radius, topEnd = radius),
192                     color = MaterialTheme.colorScheme.surfaceContainer,
193                 ) {
194                     Box(
195                         Modifier.padding(
196                             bottom =
197                                 with(LocalDensity.current) {
198                                     WindowInsets.safeDrawing.getBottom(this).toDp()
199                                 }
200                         )
201                     ) {
202                         if (isDraggable) {
203                             Column(
204                                 Modifier.wrapContentWidth(Alignment.CenterHorizontally),
205                                 horizontalAlignment = Alignment.CenterHorizontally,
206                             ) {
207                                 DragHandle(dialog)
208                                 content(dialog)
209                             }
210                         } else {
211                             content(dialog)
212                         }
213                     }
214                 }
215             }
216         },
217     )
218 }
219 
220 private enum class DragAnchors(val fraction: Float) {
221     Start(0f),
222     End(1f),
223 }
224 
SystemUIDialogFactorynull225 private fun SystemUIDialogFactory.create(
226     context: Context,
227     theme: Int,
228     dismissOnDeviceLock: Boolean,
229     delegate: DialogDelegate<SystemUIDialog>,
230     content: @Composable (SystemUIDialog) -> Unit,
231 ): ComponentSystemUIDialog {
232     val dialog = create(context, theme, dismissOnDeviceLock, delegate)
233 
234     // Create the dialog so that it is properly constructed before we set the Compose content.
235     // Otherwise, the ComposeView won't render properly.
236     dialog.create()
237 
238     // Set the content. Note that the background of the dialog is drawn on the DecorView of the
239     // dialog directly, which makes it automatically work nicely with DialogTransitionAnimator.
240     dialog.setContentView(
241         ComposeView(context).apply {
242             setContent {
243                 PlatformTheme {
244                     val defaultContentColor = MaterialTheme.colorScheme.onSurfaceVariant
245                     CompositionLocalProvider(LocalContentColor provides defaultContentColor) {
246                         content(dialog)
247                     }
248                 }
249             }
250         }
251     )
252 
253     return dialog
254 }
255 
256 /** Adds paddings for the bottom sheet surface. */
257 @Composable
Modifiernull258 private fun Modifier.bottomSheetPaddings(): Modifier {
259     val isPortrait = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT
260     return with(LocalDensity.current) {
261         val insets = WindowInsets.safeDrawing
262         // TODO(b/337205027) change paddings
263         val horizontalPadding: Dp = if (isPortrait) 0.dp else 48.dp
264         padding(
265             start = insets.getLeft(this, LocalLayoutDirection.current).toDp() + horizontalPadding,
266             top = insets.getTop(this).toDp(),
267             end = insets.getRight(this, LocalLayoutDirection.current).toDp() + horizontalPadding,
268         )
269     }
270 }
271 
272 /**
273  * For some reason adding clickable modifier onto the VolumePanel affects the traversal order:
274  * b/331155283.
275  *
276  * TODO(b/334870995) revert this to Modifier.clickable
277  */
278 @Composable
bottomSheetClickablenull279 private fun Modifier.bottomSheetClickable(onClick: () -> Unit) =
280     pointerInput(onClick) { detectTapGestures { onClick() } }
281 
282 @Composable
DragHandlenull283 private fun DragHandle(dialog: Dialog) {
284     // TODO(b/373340318): Rename drag handle string resource.
285     val dragHandleContentDescription =
286         stringResource(id = R.string.shortcut_helper_content_description_drag_handle)
287     Surface(
288         modifier =
289             Modifier.padding(top = 16.dp, bottom = 6.dp)
290                 .semantics {
291                     contentDescription = dragHandleContentDescription
292                     hideFromAccessibility()
293                 }
294                 .clickable { dialog.dismiss() },
295         color = MaterialTheme.colorScheme.onSurfaceVariant,
296         shape = MaterialTheme.shapes.extraLarge,
297     ) {
298         Box(Modifier.size(width = 32.dp, height = 4.dp))
299     }
300 }
301 
302 @Composable
draggableTopPaddingnull303 private fun draggableTopPadding(): Dp {
304     return if (hasCompactWindowSize()) DraggableBottomSheet.DefaultTopPadding
305     else DraggableBottomSheet.LargeScreenTopPadding
306 }
307 
308 private object DraggableBottomSheet {
309     val DefaultTopPadding = 64.dp
310     val LargeScreenTopPadding = 56.dp
311     val MaxWidth = 640.dp
312 }
313