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