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