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.profileselector
18
19 import androidx.compose.foundation.layout.Box
20 import androidx.compose.foundation.layout.Spacer
21 import androidx.compose.foundation.layout.fillMaxWidth
22 import androidx.compose.foundation.layout.widthIn
23 import androidx.compose.material.icons.Icons
24 import androidx.compose.material.icons.filled.AccountCircle
25 import androidx.compose.material.icons.filled.Person
26 import androidx.compose.material.icons.filled.Work
27 import androidx.compose.material3.DropdownMenu
28 import androidx.compose.material3.DropdownMenuItem
29 import androidx.compose.material3.Icon
30 import androidx.compose.material3.MaterialTheme
31 import androidx.compose.material3.MenuDefaults
32 import androidx.compose.material3.OutlinedIconButton
33 import androidx.compose.material3.Surface
34 import androidx.compose.material3.Text
35 import androidx.compose.runtime.Composable
36 import androidx.compose.runtime.getValue
37 import androidx.compose.runtime.mutableStateOf
38 import androidx.compose.runtime.remember
39 import androidx.compose.runtime.setValue
40 import androidx.compose.ui.Alignment
41 import androidx.compose.ui.Modifier
42 import androidx.compose.ui.graphics.vector.ImageVector
43 import androidx.compose.ui.platform.LocalContext
44 import androidx.compose.ui.res.stringResource
45 import androidx.compose.ui.unit.dp
46 import androidx.lifecycle.compose.collectAsStateWithLifecycle
47 import com.android.photopicker.R
48 import com.android.photopicker.core.obtainViewModel
49 import com.android.photopicker.core.user.UserProfile
50
51 /** Entry point for the profile selector. */
52 @Composable
ProfileSelectornull53 fun ProfileSelector(
54 modifier: Modifier = Modifier,
55 viewModel: ProfileSelectorViewModel = obtainViewModel(),
56 ) {
57
58 // Collect selection to ensure this is recomposed when the selection is updated.
59 val allProfiles by viewModel.allProfiles.collectAsStateWithLifecycle()
60
61 // MutableState which defines which profile to use to display the [ProfileUnavailableDialog].
62 // When this value is null, the dialog is hidden.
63 var disabledDialogProfile: UserProfile? by remember { mutableStateOf(null) }
64 disabledDialogProfile?.let {
65 ProfileUnavailableDialog(
66 onDismissRequest = { disabledDialogProfile = null },
67 profile = it,
68 )
69 }
70
71 // Ensure there is more than one available profile before creating all of the UI.
72 if (allProfiles.size > 1) {
73 val context = LocalContext.current
74 val currentProfile by viewModel.selectedProfile.collectAsStateWithLifecycle()
75 var expanded by remember { mutableStateOf(false) }
76 Box(modifier = modifier) {
77 OutlinedIconButton(
78 modifier = Modifier.align(Alignment.CenterStart),
79 onClick = { expanded = !expanded }
80 ) {
81 currentProfile.icon?.let {
82 Icon(
83 it,
84 contentDescription =
85 stringResource(R.string.photopicker_profile_switch_button_description)
86 )
87 }
88 // If the profile doesn't have an icon drawable set, then
89 // generate one.
90 ?: Icon(
91 getIconForProfile(currentProfile),
92 contentDescription =
93 stringResource(R.string.photopicker_profile_switch_button_description)
94 )
95 }
96
97 // DropdownMenu attaches to the element above it in the hierarchy, so this should stay
98 // directly below the button that opens it.
99 DropdownMenu(
100 expanded = expanded,
101 onDismissRequest = { expanded = !expanded },
102 ) {
103 for (profile in allProfiles) {
104
105 // The background color behind the text
106 Surface(
107 modifier = Modifier.widthIn(min = 200.dp),
108 color =
109 if (currentProfile == profile)
110 MaterialTheme.colorScheme.primaryContainer
111 else MaterialTheme.colorScheme.surface
112 ) {
113 DropdownMenuItem(
114 modifier = Modifier.fillMaxWidth(),
115 // enabled = profile.enabled,
116 onClick = {
117 // Only request a switch if the profile is actually different.
118 if (currentProfile != profile) {
119
120 if (profile.enabled) {
121 viewModel.requestSwitchUser(
122 context = context,
123 requested = profile
124 )
125 // Close the profile switcher popup
126 expanded = false
127 } else {
128
129 // Show the disabled profile dialog
130 disabledDialogProfile = profile
131 expanded = false
132 }
133 }
134 },
135 text = { Text(profile.label ?: getLabelForProfile(profile)) },
136 leadingIcon = {
137 profile.icon?.let {
138 Icon(
139 it,
140 contentDescription = null,
141 tint =
142 when (profile.enabled) {
143 true -> MenuDefaults.itemColors().leadingIconColor
144 false ->
145 MenuDefaults.itemColors()
146 .disabledLeadingIconColor
147 }
148 )
149 }
150 // If the profile doesn't have an icon drawable set, then
151 // generate one.
152 ?: Icon(
153 getIconForProfile(profile),
154 contentDescription = null,
155 tint =
156 when (profile.enabled) {
157 true -> MenuDefaults.itemColors().leadingIconColor
158 false ->
159 MenuDefaults.itemColors()
160 .disabledLeadingIconColor
161 }
162 )
163 },
164 )
165 }
166 }
167 }
168 }
169 } else {
170 // Return a spacer which consumes the modifier so the space is still occupied, but is empty.
171 Spacer(modifier)
172 }
173 }
174
175 /**
176 * Generates a display label for the provided profile.
177 *
178 * @param profile the profile!
179 * @return a display safe & localized profile name
180 */
181 @Composable
getLabelForProfilenull182 internal fun getLabelForProfile(profile: UserProfile): String {
183 return when (profile.profileType) {
184 UserProfile.ProfileType.PRIMARY ->
185 stringResource(R.string.photopicker_profile_primary_label)
186 UserProfile.ProfileType.MANAGED ->
187 stringResource(R.string.photopicker_profile_managed_label)
188 UserProfile.ProfileType.UNKNOWN ->
189 stringResource(R.string.photopicker_profile_unknown_label)
190 }
191 }
192
193 /**
194 * Generates a display icon for the provided profile.
195 *
196 * @param profile the profile!
197 * @return an icon [ImageVector] that represents the profile
198 */
199 @Composable
getIconForProfilenull200 internal fun getIconForProfile(profile: UserProfile): ImageVector {
201 return when (profile.profileType) {
202 UserProfile.ProfileType.PRIMARY -> Icons.Filled.AccountCircle
203 UserProfile.ProfileType.MANAGED -> Icons.Filled.Work
204 UserProfile.ProfileType.UNKNOWN -> Icons.Filled.Person
205 }
206 }
207