• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * 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.selectionbar
18 
19 import android.provider.MediaStore
20 import androidx.compose.animation.AnimatedVisibility
21 import androidx.compose.animation.slideInVertically
22 import androidx.compose.animation.slideOutVertically
23 import androidx.compose.foundation.layout.Arrangement
24 import androidx.compose.foundation.layout.PaddingValues
25 import androidx.compose.foundation.layout.Row
26 import androidx.compose.foundation.layout.Spacer
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.foundation.shape.CornerSize
31 import androidx.compose.foundation.shape.RoundedCornerShape
32 import androidx.compose.material.icons.Icons
33 import androidx.compose.material.icons.filled.Close
34 import androidx.compose.material3.ButtonDefaults
35 import androidx.compose.material3.FilledTonalButton
36 import androidx.compose.material3.Icon
37 import androidx.compose.material3.IconButton
38 import androidx.compose.material3.MaterialTheme
39 import androidx.compose.material3.Surface
40 import androidx.compose.material3.Text
41 import androidx.compose.runtime.Composable
42 import androidx.compose.runtime.getValue
43 import androidx.compose.runtime.rememberCoroutineScope
44 import androidx.compose.ui.Alignment
45 import androidx.compose.ui.Modifier
46 import androidx.compose.ui.res.stringResource
47 import androidx.compose.ui.semantics.contentDescription
48 import androidx.compose.ui.semantics.semantics
49 import androidx.compose.ui.unit.dp
50 import androidx.lifecycle.compose.collectAsStateWithLifecycle
51 import com.android.photopicker.R
52 import com.android.photopicker.core.animations.emphasizedAccelerate
53 import com.android.photopicker.core.animations.emphasizedDecelerate
54 import com.android.photopicker.core.components.ElevationTokens
55 import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration
56 import com.android.photopicker.core.events.Event
57 import com.android.photopicker.core.events.LocalEvents
58 import com.android.photopicker.core.events.Telemetry
59 import com.android.photopicker.core.features.FeatureToken
60 import com.android.photopicker.core.features.LocalFeatureManager
61 import com.android.photopicker.core.features.Location
62 import com.android.photopicker.core.features.LocationParams
63 import com.android.photopicker.core.selection.LocalSelection
64 import com.android.photopicker.core.theme.CustomAccentColorScheme
65 import com.android.photopicker.util.LocalLocalizationHelper
66 import kotlinx.coroutines.launch
67 
68 /* The size of spacers between elements on the bar */
69 private val MEASUREMENT_BUTTONS_SPACER_SIZE = 8.dp
70 private val MEASUREMENT_DESELECT_SPACER_SIZE = 4.dp
71 private val MEASUREMENT_DESELECT_DISABLED_SPACER_SIZE = 16.dp
72 
73 /* Corner radius of the selection bar */
74 private val MEASUREMENT_SELECTION_BAR_CORNER_SIZE = 100
75 
76 /* The amount of padding between elements and the edge of the selection bar */
77 private val MEASUREMENT_BAR_PADDING = PaddingValues(horizontal = 10.dp, vertical = 4.dp)
78 
79 /**
80  * The Photopicker selection bar that shows the actions related to the current selection of Media.
81  * This composable does not provide a secondary action button directly, but instead exposes
82  * [Location.SELECTION_BAR_SECONDARY_ACTION] for another feature to provide a secondary action that
83  * is relevant to the selection bar. If not are provided, the space will be left empty.
84  */
85 @Composable
SelectionBarnull86 fun SelectionBar(modifier: Modifier = Modifier, params: LocationParams) {
87     // Collect selection to ensure this is recomposed when the selection is updated.
88     val selection = LocalSelection.current
89     val currentSelection by LocalSelection.current.flow.collectAsStateWithLifecycle()
90 
91     // For ACTION_USER_SELECT_IMAGES_FOR_APP selection bar should always be visible to allow users
92     // the option to exit with zero selection i.e. revoking all grants.
93     val visible =
94         currentSelection.isNotEmpty() ||
95             MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP.equals(
96                 LocalPhotopickerConfiguration.current.action
97             )
98     val disableClearAllButton =
99         MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP.equals(
100             LocalPhotopickerConfiguration.current.action
101         ) && LocalPhotopickerConfiguration.current.flags.OWNED_PHOTOS_ENABLED
102     val configuration = LocalPhotopickerConfiguration.current
103     val events = LocalEvents.current
104     val scope = rememberCoroutineScope()
105     val localizedCurrentSelectionSize =
106         LocalLocalizationHelper.current.getLocalizedCount(currentSelection.size)
107     // The entire selection bar is hidden if the selection is empty, and
108     // animates between visible states.
109     AnimatedVisibility(
110         // Pass through the modifier that is received for positioning offsets.
111         modifier = modifier,
112         visible = visible,
113         enter =
114             slideInVertically(animationSpec = emphasizedDecelerate, initialOffsetY = { it * 2 }),
115         exit = slideOutVertically(animationSpec = emphasizedAccelerate, targetOffsetY = { it * 2 }),
116     ) {
117         Surface(
118             modifier = Modifier.fillMaxWidth(),
119             color = MaterialTheme.colorScheme.surfaceContainerHighest,
120             shape = RoundedCornerShape(CornerSize(MEASUREMENT_SELECTION_BAR_CORNER_SIZE)),
121             shadowElevation = ElevationTokens.Level2,
122         ) {
123             Row(
124                 horizontalArrangement = Arrangement.SpaceBetween,
125                 verticalAlignment = Alignment.CenterVertically,
126                 modifier = Modifier.padding(MEASUREMENT_BAR_PADDING),
127             ) {
128 
129                 // Deselect all button [Left side]
130                 Row(verticalAlignment = Alignment.CenterVertically) {
131                     if (disableClearAllButton) {
132                         Spacer(Modifier.size(MEASUREMENT_DESELECT_DISABLED_SPACER_SIZE))
133                     } else {
134                         IconButton(onClick = { scope.launch { selection.clear() } }) {
135                             Icon(
136                                 Icons.Filled.Close,
137                                 contentDescription =
138                                     stringResource(
139                                         R.string.photopicker_clear_selection_button_description
140                                     ),
141                                 tint = MaterialTheme.colorScheme.onSurface,
142                             )
143                         }
144                         Spacer(Modifier.size(MEASUREMENT_DESELECT_SPACER_SIZE))
145                     }
146 
147                     val selectionSizeDescription =
148                         stringResource(
149                             R.string.photopicker_selection_size_description,
150                             localizedCurrentSelectionSize,
151                         )
152                     Text(
153                         "$localizedCurrentSelectionSize",
154                         style = MaterialTheme.typography.headlineSmall,
155                         modifier =
156                             Modifier.semantics { contentDescription = selectionSizeDescription },
157                     )
158                 }
159 
160                 // Primary and Secondary actions [Right side]
161                 Row(verticalAlignment = Alignment.CenterVertically) {
162                     LocalFeatureManager.current.composeLocation(
163                         Location.SELECTION_BAR_SECONDARY_ACTION,
164                         maxSlots = 1, // Only accept one additional action.
165                         modifier = Modifier,
166                     )
167                     Spacer(Modifier.size(MEASUREMENT_BUTTONS_SPACER_SIZE))
168                     FilledTonalButton(
169                         onClick = {
170                             // Log clicking the picker Add media button
171                             scope.launch {
172                                 events.dispatch(
173                                     Event.LogPhotopickerUIEvent(
174                                         FeatureToken.SELECTION_BAR.token,
175                                         configuration.sessionId,
176                                         configuration.callingPackageUid ?: -1,
177                                         Telemetry.UiEvent.PICKER_CLICK_ADD_BUTTON,
178                                     )
179                                 )
180                             }
181                             // The selection bar should receive a click handler from its parent
182                             // to handle the primary button click.
183                             val clickAction = params as? LocationParams.WithClickAction
184                             clickAction?.onClick()
185                         },
186                         colors =
187                             ButtonDefaults.filledTonalButtonColors(
188                                 containerColor =
189                                     CustomAccentColorScheme.current.getAccentColorIfDefinedOrElse(
190                                         /* fallback */ MaterialTheme.colorScheme.primary
191                                     ),
192                                 contentColor =
193                                     CustomAccentColorScheme.current
194                                         .getTextColorForAccentComponentsIfDefinedOrElse(
195                                             /* fallback */ MaterialTheme.colorScheme.onPrimary
196                                         ),
197                             ),
198                     ) {
199                         Text(stringResource(R.string.photopicker_done_button_label))
200                     }
201                 }
202             }
203         }
204     }
205 }
206