1 /*
<lambda>null2  * Copyright 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.photopicker.features.cloudmedia
18 
19 import android.util.Log
20 import androidx.compose.foundation.layout.Column
21 import androidx.compose.foundation.layout.Row
22 import androidx.compose.foundation.layout.Spacer
23 import androidx.compose.foundation.layout.height
24 import androidx.compose.foundation.layout.padding
25 import androidx.compose.foundation.layout.width
26 import androidx.compose.foundation.layout.wrapContentHeight
27 import androidx.compose.foundation.layout.wrapContentWidth
28 import androidx.compose.material3.AlertDialogDefaults
29 import androidx.compose.material3.BasicAlertDialog
30 import androidx.compose.material3.CircularProgressIndicator
31 import androidx.compose.material3.ExperimentalMaterial3Api
32 import androidx.compose.material3.MaterialTheme
33 import androidx.compose.material3.Surface
34 import androidx.compose.material3.Text
35 import androidx.compose.material3.TextButton
36 import androidx.compose.runtime.Composable
37 import androidx.compose.runtime.LaunchedEffect
38 import androidx.compose.runtime.getValue
39 import androidx.compose.runtime.setValue
40 import androidx.compose.ui.Alignment
41 import androidx.compose.ui.Modifier
42 import androidx.compose.ui.res.stringResource
43 import androidx.compose.ui.unit.dp
44 import androidx.lifecycle.compose.collectAsStateWithLifecycle
45 import com.android.photopicker.R
46 import com.android.photopicker.core.features.LocationParams
47 import com.android.photopicker.core.obtainViewModel
48 
49 /* Size of the spacer between dialog elements. */
50 private val MEASUREMENT_DIALOG_SPACER_SIZE = 24.dp
51 
52 /* Size of the padding around the edge of the dialog. */
53 private val MEASUREMENT_DIALOG_PADDING = 24.dp
54 
55 @Composable
56 @OptIn(ExperimentalMaterial3Api::class)
57 /**
58  * Attaches a [MediaPreloader] so that it can handle emissions in the
59  * [LocationParams.WithMediaPreloader.preloadMedia] and display preloading dialogs to the user.
60  *
61  * This composable has three states:
62  * - Empty (No preload activity, no error state)
63  * - Loading (A preload operation is currently running)
64  * - Error (A preloading operation has failed)
65  *
66  * For the non empty states, the appropriate dialog is shown to the user. For an empty state, this
67  * composable exists to attach the [MediaPreloaderViewModel] so that it can monitor the event bus.
68  */
69 fun MediaPreloader(
70     // The incoming modifier is ignored, since no elements are actually added to
71     // [Location.MEDIA_PRELOADER], only floating dialogs that sit above the app.
72     @Suppress("UNUSED_PARAMETER") modifier: Modifier,
73     params: LocationParams,
74     viewModel: MediaPreloaderViewModel = obtainViewModel(),
75 ) {
76 
77     // Data flow from the view model for which Dialog to display.
78     val dialogData by viewModel.dialogData.collectAsStateWithLifecycle()
79 
80     // These must be set by the parent composable for the preloader to have any effect.
81     val preloaderParameters = params as? LocationParams.WithMediaPreloader
82 
83     preloaderParameters?.let {
84         LaunchedEffect(params) {
85             // Listen for emissions of media to preload, and begin the preload when requested.
86             it.preloadMedia.collect { media -> viewModel.startPreload(media, it.obtainDeferred()) }
87         }
88     }
89         // If no preloaderParameters were passed to this location, there is no way to trigger
90         // the preloader.
91         ?: Log.w(
92             CloudMediaFeature.TAG,
93             "MediaPreloader did not receive parameters from parent location," +
94                 "  the preloader will not be active."
95         )
96 
97     // Show a dialog or empty state based on which [PreloaderDialogData] is present.
98     when (val data = dialogData) {
99         null -> Unit // Empty state, no dialog
100         is PreloaderDialogData.PreloaderLoadingDialogData ->
101             MediaPreloaderLoadingDialog(
102                 dialogData = data,
103                 onDismissRequest = {
104                     viewModel.cancelPreload()
105                     viewModel.hideAllDialogs()
106                 },
107             )
108         is PreloaderDialogData.PreloaderLoadingErrorDialog ->
109             MediaPreloaderErrorDialog(
110                 onDismissRequest = {
111                     viewModel.cancelPreload()
112                     viewModel.hideAllDialogs()
113                 },
114             )
115     }
116 }
117 
118 /**
119  * This is the Loading state dialog of the Preloader.
120  *
121  * This dialog shows a Loading message and progress indicator to the user which updates as the
122  * [MediaPreloaderViewModel] emits updated [PreloaderDialogData].
123  *
124  * The user can cancel the preload operation to return to the previous screen. (This will also
125  * prevent the Photopicker from being closed when the Media is ready.)
126  */
127 @OptIn(ExperimentalMaterial3Api::class)
128 @Composable
MediaPreloaderLoadingDialognull129 private fun MediaPreloaderLoadingDialog(
130     onDismissRequest: () -> Unit,
131     dialogData: PreloaderDialogData.PreloaderLoadingDialogData,
132 ) {
133     BasicAlertDialog(
134         onDismissRequest = {},
135     ) {
136         Surface(
137             modifier = Modifier.wrapContentWidth().wrapContentHeight(),
138             shape = MaterialTheme.shapes.large,
139             tonalElevation = AlertDialogDefaults.TonalElevation
140         ) {
141             Column(modifier = Modifier.padding(MEASUREMENT_DIALOG_PADDING)) {
142                 Text(
143                     stringResource(R.string.photopicker_preloading_dialog_title),
144                     style = MaterialTheme.typography.titleLarge
145                 )
146                 Spacer(modifier = Modifier.height(MEASUREMENT_DIALOG_SPACER_SIZE))
147                 Row(verticalAlignment = Alignment.CenterVertically) {
148                     CircularProgressIndicator()
149                     Spacer(modifier = Modifier.width(MEASUREMENT_DIALOG_SPACER_SIZE))
150                     Text(
151                         stringResource(
152                             R.string.photopicker_preloading_progress_message,
153                             dialogData.completed,
154                             dialogData.total
155                         ),
156                         style = MaterialTheme.typography.bodyMedium
157                     )
158                 }
159                 Spacer(modifier = Modifier.height(MEASUREMENT_DIALOG_SPACER_SIZE))
160                 Row(Modifier.align(Alignment.End)) {
161                     TextButton(
162                         onClick = onDismissRequest,
163                     ) {
164                         Text(stringResource(android.R.string.cancel))
165                     }
166                 }
167             }
168         }
169     }
170 }
171 
172 @OptIn(ExperimentalMaterial3Api::class)
173 @Composable
174 /**
175  * This is the Error state dialog of the Preloader.
176  *
177  * This dialog shows a generic Error message to the user which updates as the
178  * [MediaPreloaderViewModel] emits updated [PreloaderDialogData].
179  *
180  * The user can dismiss the dialog to return to the previous screen.
181  */
MediaPreloaderErrorDialognull182 private fun MediaPreloaderErrorDialog(
183     onDismissRequest: () -> Unit,
184 ) {
185 
186     BasicAlertDialog(
187         onDismissRequest = onDismissRequest,
188     ) {
189         Surface(
190             modifier = Modifier.wrapContentWidth().wrapContentHeight(),
191             shape = MaterialTheme.shapes.large,
192             tonalElevation = AlertDialogDefaults.TonalElevation
193         ) {
194             Column(modifier = Modifier.padding(MEASUREMENT_DIALOG_PADDING)) {
195                 Text(
196                     stringResource(R.string.photopicker_preloading_dialog_error_title),
197                     style = MaterialTheme.typography.titleLarge
198                 )
199                 Spacer(modifier = Modifier.height(MEASUREMENT_DIALOG_SPACER_SIZE))
200                 Text(
201                     stringResource(R.string.photopicker_preloading_dialog_error_message),
202                     style = MaterialTheme.typography.bodyMedium
203                 )
204                 Spacer(modifier = Modifier.height(MEASUREMENT_DIALOG_SPACER_SIZE))
205                 Row(Modifier.align(Alignment.End)) {
206                     TextButton(
207                         onClick = onDismissRequest,
208                     ) {
209                         Text(stringResource(android.R.string.ok))
210                     }
211                 }
212             }
213         }
214     }
215 }
216