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.permissioncontroller.permission.ui.wear.elements
18 
19 import android.app.Activity
20 import android.content.Context
21 import android.content.ContextWrapper
22 import android.graphics.drawable.Drawable
23 import androidx.compose.foundation.Image
24 import androidx.compose.foundation.layout.Box
25 import androidx.compose.foundation.layout.PaddingValues
26 import androidx.compose.foundation.layout.fillMaxSize
27 import androidx.compose.foundation.layout.fillMaxWidth
28 import androidx.compose.foundation.layout.padding
29 import androidx.compose.foundation.layout.size
30 import androidx.compose.runtime.Composable
31 import androidx.compose.runtime.LaunchedEffect
32 import androidx.compose.runtime.getValue
33 import androidx.compose.runtime.mutableStateOf
34 import androidx.compose.runtime.remember
35 import androidx.compose.runtime.setValue
36 import androidx.compose.ui.Alignment
37 import androidx.compose.ui.Modifier
38 import androidx.compose.ui.focus.FocusRequester
39 import androidx.compose.ui.layout.ContentScale
40 import androidx.compose.ui.platform.LocalConfiguration
41 import androidx.compose.ui.platform.LocalContext
42 import androidx.compose.ui.platform.testTag
43 import androidx.compose.ui.res.painterResource
44 import androidx.compose.ui.text.style.TextAlign
45 import androidx.compose.ui.unit.Dp
46 import androidx.compose.ui.unit.dp
47 import androidx.fragment.app.FragmentActivity
48 import androidx.lifecycle.Lifecycle
49 import androidx.lifecycle.compose.LocalLifecycleOwner
50 import androidx.lifecycle.repeatOnLifecycle
51 import androidx.wear.compose.foundation.SwipeToDismissValue
52 import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
53 import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
54 import androidx.wear.compose.foundation.lazy.ScalingLazyListState
55 import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState
56 import androidx.wear.compose.material.CircularProgressIndicator
57 import androidx.wear.compose.material.MaterialTheme
58 import androidx.wear.compose.material.PositionIndicator
59 import androidx.wear.compose.material.Scaffold
60 import androidx.wear.compose.material.SwipeToDismissBox
61 import androidx.wear.compose.material.Text
62 import androidx.wear.compose.material.TimeText
63 import androidx.wear.compose.material.Vignette
64 import androidx.wear.compose.material.VignettePosition
65 import androidx.wear.compose.material.scrollAway
66 import com.android.permissioncontroller.permission.ui.wear.elements.rotaryinput.rotaryWithScroll
67 import com.android.permissioncontroller.permission.ui.wear.theme.WearPermissionTheme
68 
69 /**
70  * Screen that contains a list of items defined using the [content] parameter, adds the time text
71  * (if [showTimeText] is true), the tile (if [title] is not null), the vignette and the position
72  * indicator. It also manages the scaling animation and allows the user to scroll the content using
73  * the crown.
74  */
75 @Composable
76 fun ScrollableScreen(
77     showTimeText: Boolean = true,
78     title: String? = null,
79     subtitle: CharSequence? = null,
80     image: Any? = null,
81     isLoading: Boolean = false,
82     titleTestTag: String? = null,
83     subtitleTestTag: String? = null,
84     content: ScalingLazyListScope.() -> Unit,
85 ) {
86     var dismissed by remember { mutableStateOf(false) }
87     val activity = LocalContext.current.findActivity()
88     val state = rememberSwipeToDismissBoxState()
89 
90     LaunchedEffect(state.currentValue) {
91         if (state.currentValue == SwipeToDismissValue.Dismissed) {
92             dismiss(activity)
93             dismissed = true
94             state.snapTo(SwipeToDismissValue.Default)
95         }
96     }
97 
98     // To support Swipe-dismiss effect,
99     // add the view to SwipeToDismissBox if the screen is not on the top fragment.
100     if (getBackStackEntryCount(activity) > 0) {
101         SwipeToDismissBox(state = state) { isBackground ->
102             Scaffold(
103                 showTimeText,
104                 title,
105                 subtitle,
106                 image,
107                 isLoading = isLoading || isBackground || dismissed,
108                 content,
109                 titleTestTag,
110                 subtitleTestTag
111             )
112         }
113     } else {
114         Scaffold(
115             showTimeText,
116             title,
117             subtitle,
118             image,
119             isLoading,
120             content,
121             titleTestTag,
122             subtitleTestTag
123         )
124     }
125 }
126 
127 @Composable
Scaffoldnull128 internal fun Scaffold(
129     showTimeText: Boolean,
130     title: String?,
131     subtitle: CharSequence?,
132     image: Any?,
133     isLoading: Boolean,
134     content: ScalingLazyListScope.() -> Unit,
135     titleTestTag: String? = null,
136     subtitleTestTag: String? = null,
137 ) {
138     val screenWidth = LocalConfiguration.current.screenWidthDp
139     val screenHeight = LocalConfiguration.current.screenHeightDp
140     val scrollContentHorizontalPadding = (screenWidth * 0.052).dp
141     val titleHorizontalPadding = (screenWidth * 0.0884).dp
142     val subtitleHorizontalPadding = (screenWidth * 0.0416).dp
143     val scrollContentTopPadding = (screenHeight * 0.1456).dp
144     val scrollContentBottomPadding = (screenHeight * 0.3636).dp
145     val titleBottomPadding =
146         if (subtitle == null) {
147             8.dp
148         } else {
149             4.dp
150         }
151     val subtitleBottomPadding = 8.dp
152     val timeTextTopPadding =
153         if (showTimeText) {
154             1.dp
155         } else {
156             0.dp
157         }
158     val titlePaddingValues =
159         PaddingValues(
160             start = titleHorizontalPadding,
161             top = 4.dp,
162             bottom = titleBottomPadding,
163             end = titleHorizontalPadding
164         )
165     val subTitlePaddingValues =
166         PaddingValues(
167             start = subtitleHorizontalPadding,
168             top = 4.dp,
169             bottom = subtitleBottomPadding,
170             end = subtitleHorizontalPadding
171         )
172     val initialCenterIndex = 0
173     val centerHeightDp = Dp(LocalConfiguration.current.screenHeightDp / 2.0f)
174     // We are adding TimeText's padding to create a smooth scrolling
175     val initialCenterItemScrollOffset = scrollContentTopPadding + timeTextTopPadding
176     val scrollAwayOffset = centerHeightDp - initialCenterItemScrollOffset
177     val focusRequester = remember { FocusRequester() }
178     val listState = remember { ScalingLazyListState(initialCenterItemIndex = initialCenterIndex) }
179     LaunchedEffect(title) {
180         listState.animateScrollToItem(index = 0) // Scroll to the top when triggerValue changes
181     }
182     WearPermissionTheme {
183         Scaffold(
184             // TODO: Use a rotary modifier from Wear Compose once Wear Compose 1.4 is landed.
185             // (b/325560444)
186             modifier =
187                 Modifier.rotaryWithScroll(
188                     scrollableState = listState,
189                     focusRequester = focusRequester
190                 ),
191             timeText = {
192                 if (showTimeText && !isLoading) {
193                     TimeText(
194                         modifier =
195                             Modifier.scrollAway(listState, initialCenterIndex, scrollAwayOffset)
196                                 .padding(top = timeTextTopPadding),
197                     )
198                 }
199             },
200             vignette = { Vignette(vignettePosition = VignettePosition.TopAndBottom) },
201             positionIndicator = { PositionIndicator(scalingLazyListState = listState) }
202         ) {
203             Box(modifier = Modifier.fillMaxSize()) {
204                 if (isLoading) {
205                     CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
206                 } else {
207                     ScalingLazyColumn(
208                         modifier = Modifier.fillMaxWidth(),
209                         state = listState,
210                         // Set autoCentering to null to avoid adding extra padding based on the
211                         // content.
212                         autoCentering = null,
213                         contentPadding =
214                             PaddingValues(
215                                 start = scrollContentHorizontalPadding,
216                                 end = scrollContentHorizontalPadding,
217                                 top = scrollContentTopPadding,
218                                 bottom = scrollContentBottomPadding
219                             )
220                     ) {
221                         image?.let {
222                             val imageModifier = Modifier.size(24.dp)
223                             when (image) {
224                                 is Int ->
225                                     item {
226                                         Image(
227                                             painter = painterResource(id = image),
228                                             contentDescription = null,
229                                             contentScale = ContentScale.Crop,
230                                             modifier = imageModifier
231                                         )
232                                     }
233                                 is Drawable ->
234                                     item {
235                                         Image(
236                                             painter = rememberDrawablePainter(image),
237                                             contentDescription = null,
238                                             contentScale = ContentScale.Crop,
239                                             modifier = imageModifier
240                                         )
241                                     }
242                                 else -> {}
243                             }
244                         }
245                         if (title != null) {
246                             item {
247                                 var modifier: Modifier = Modifier
248                                 if (titleTestTag != null) {
249                                     modifier = modifier.testTag(titleTestTag)
250                                 }
251                                 ListHeader(modifier = Modifier.padding(titlePaddingValues)) {
252                                     Text(
253                                         text = title,
254                                         textAlign = TextAlign.Center,
255                                         modifier = modifier
256                                     )
257                                 }
258                             }
259                         }
260                         if (subtitle != null) {
261                             item {
262                                 var modifier: Modifier =
263                                     Modifier.align(Alignment.Center).padding(subTitlePaddingValues)
264                                 if (subtitleTestTag != null) {
265                                     modifier = modifier.testTag(subtitleTestTag)
266                                 }
267                                 AnnotatedText(
268                                     text = subtitle,
269                                     style =
270                                         MaterialTheme.typography.body2.copy(
271                                             color = MaterialTheme.colors.onSurfaceVariant
272                                         ),
273                                     modifier = modifier,
274                                 )
275                             }
276                         }
277 
278                         content()
279                     }
280                     RequestFocusOnResume(focusRequester = focusRequester)
281                 }
282             }
283         }
284     }
285 }
286 
287 @Composable
RequestFocusOnResumenull288 private fun RequestFocusOnResume(focusRequester: FocusRequester) {
289     val lifecycleOwner = LocalLifecycleOwner.current
290     LaunchedEffect(Unit) {
291         lifecycleOwner.repeatOnLifecycle(state = Lifecycle.State.RESUMED) {
292             focusRequester.requestFocus()
293         }
294     }
295 }
296 
dismissnull297 internal fun dismiss(activity: Activity) {
298     if (activity is FragmentActivity) {
299         if (!activity.supportFragmentManager.popBackStackImmediate()) {
300             activity.finish()
301         }
302     } else {
303         activity.finish()
304     }
305 }
306 
getBackStackEntryCountnull307 internal fun getBackStackEntryCount(activity: Activity): Int {
308     return if (activity is FragmentActivity) {
309         activity.supportFragmentManager.primaryNavigationFragment
310             ?.childFragmentManager
311             ?.backStackEntryCount
312             ?: 0
313     } else {
314         0
315     }
316 }
317 
findActivitynull318 internal fun Context.findActivity(): Activity {
319     var context = this
320     while (context is ContextWrapper) {
321         if (context is Activity) return context
322         context = context.baseContext
323     }
324     throw IllegalStateException("The screen should be called in the context of an Activity")
325 }
326