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