1 /*
2  * Copyright (C) 2024 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.volume.panel.component.popup.ui.composable
18 
19 import android.view.Gravity
20 import androidx.annotation.GravityInt
21 import androidx.compose.foundation.layout.Arrangement
22 import androidx.compose.foundation.layout.Box
23 import androidx.compose.foundation.layout.Column
24 import androidx.compose.foundation.layout.fillMaxWidth
25 import androidx.compose.foundation.layout.padding
26 import androidx.compose.foundation.layout.size
27 import androidx.compose.foundation.layout.wrapContentHeight
28 import androidx.compose.material3.Icon
29 import androidx.compose.material3.IconButton
30 import androidx.compose.material3.IconButtonDefaults
31 import androidx.compose.material3.MaterialTheme
32 import androidx.compose.runtime.Composable
33 import androidx.compose.ui.Alignment
34 import androidx.compose.ui.Modifier
35 import androidx.compose.ui.layout.LayoutCoordinates
36 import androidx.compose.ui.layout.boundsInRoot
37 import androidx.compose.ui.res.painterResource
38 import androidx.compose.ui.res.stringResource
39 import androidx.compose.ui.semantics.paneTitle
40 import androidx.compose.ui.semantics.semantics
41 import androidx.compose.ui.unit.dp
42 import com.android.systemui.animation.DialogTransitionAnimator
43 import com.android.systemui.animation.Expandable
44 import com.android.systemui.res.R
45 import com.android.systemui.statusbar.phone.SystemUIDialog
46 import com.android.systemui.statusbar.phone.SystemUIDialogFactory
47 import com.android.systemui.statusbar.phone.create
48 import javax.inject.Inject
49 
50 /** Volume panel bottom popup menu. */
51 class VolumePanelPopup
52 @Inject
53 constructor(
54     private val dialogFactory: SystemUIDialogFactory,
55     private val dialogTransitionAnimator: DialogTransitionAnimator,
56 ) {
57 
58     /**
59      * Shows a popup with the [expandable] animation.
60      *
61      * @param title is shown on the top of the popup
62      * @param content is the popup body
63      */
shownull64     fun show(
65         expandable: Expandable?,
66         @GravityInt gravity: Int,
67         title: @Composable (SystemUIDialog) -> Unit,
68         content: @Composable (SystemUIDialog) -> Unit,
69     ) {
70         val dialog =
71             dialogFactory.create(
72                 theme = R.style.Theme_VolumePanel_Popup,
73                 dialogGravity = gravity,
74             ) {
75                 PopupComposable(it, title, content)
76             }
77         val controller = expandable?.dialogTransitionController()
78         if (controller == null) {
79             dialog.show()
80         } else {
81             dialogTransitionAnimator.show(dialog, controller)
82         }
83     }
84 
85     @Composable
PopupComposablenull86     private fun PopupComposable(
87         dialog: SystemUIDialog,
88         title: @Composable (SystemUIDialog) -> Unit,
89         content: @Composable (SystemUIDialog) -> Unit,
90     ) {
91         val paneTitle = stringResource(R.string.accessibility_volume_settings)
92         Box(Modifier.fillMaxWidth().semantics(properties = { this.paneTitle = paneTitle })) {
93             Column(
94                 modifier = Modifier.fillMaxWidth().padding(vertical = 20.dp),
95                 verticalArrangement = Arrangement.spacedBy(20.dp),
96             ) {
97                 Box(
98                     modifier =
99                         Modifier.padding(horizontal = 80.dp).fillMaxWidth().wrapContentHeight(),
100                     contentAlignment = Alignment.Center
101                 ) {
102                     title(dialog)
103                 }
104 
105                 Box(
106                     modifier =
107                         Modifier.padding(horizontal = 16.dp).fillMaxWidth().wrapContentHeight(),
108                     contentAlignment = Alignment.Center
109                 ) {
110                     content(dialog)
111                 }
112             }
113 
114             IconButton(
115                 modifier = Modifier.align(Alignment.TopEnd).size(64.dp).padding(20.dp),
116                 onClick = { dialog.dismiss() },
117                 colors =
118                     IconButtonDefaults.iconButtonColors(
119                         contentColor = MaterialTheme.colorScheme.outline
120                     )
121             ) {
122                 Icon(
123                     painterResource(R.drawable.ic_close),
124                     contentDescription = stringResource(R.string.accessibility_desc_close),
125                 )
126             }
127         }
128     }
129 
130     companion object {
131 
132         /**
133          * Returns absolute ([Gravity.LEFT], [Gravity.RIGHT] or [Gravity.CENTER_HORIZONTAL])
134          * [GravityInt] for the popup based on the [coordinates] global position relative to the
135          * [screenWidthPx].
136          */
137         @GravityInt
calculateGravitynull138         fun calculateGravity(coordinates: LayoutCoordinates, screenWidthPx: Float): Int {
139             val bottomCenter: Float = coordinates.boundsInRoot().bottomCenter.x
140             val rootBottomCenter: Float = screenWidthPx / 2
141             return when {
142                 bottomCenter < rootBottomCenter -> Gravity.LEFT
143                 bottomCenter > rootBottomCenter -> Gravity.RIGHT
144                 else -> Gravity.CENTER_HORIZONTAL
145             }
146         }
147     }
148 }
149