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