1 /* 2 * Copyright (C) 2015 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 package com.android.messaging.util; 17 18 import android.graphics.Color; 19 import android.net.Uri; 20 import android.net.Uri.Builder; 21 import androidx.annotation.NonNull; 22 import androidx.annotation.Nullable; 23 import android.text.TextUtils; 24 25 import com.android.messaging.datamodel.data.ParticipantData; 26 27 import java.util.ArrayList; 28 import java.util.List; 29 30 /** 31 * A helper utility for creating {@link android.net.Uri}s to describe what avatar to fetch or 32 * generate and will help verify and extract information from avatar {@link android.net.Uri}s. 33 * 34 * There are three types of avatar {@link android.net.Uri}. 35 * 36 * 1) Group Avatars - These are avatars which are used to represent a group conversation. Group 37 * avatars uris are basically multiple avatar uri which can be any of the below types but not 38 * another group avatar. The group avatars can hold anywhere from two to four avatars uri and can 39 * be in any of the following format 40 * messaging://avatar/g?p=<avatarUri>&p=<avatarUri2> 41 * messaging://avatar/g?p=<avatarUri>&p=<avatarUri2>&p=<avatarUri3> 42 * messaging://avatar/g?p=<avatarUri>&p=<avatarUri2>&p=<avatarUri3>&p=<avatarUri4> 43 * 44 * 2) Local Resource - A local resource avatar is use when there is a profile photo for the 45 * participant. This can be any local resource. 46 * 47 * 3) Letter Tile - A letter tile is used when a participant has a name but no profile photo. A 48 * letter tile will contain the first code point of the participant's name and a background color 49 * based on the hash of the participant's full name. Letter tiles will be in the following format. 50 * messaging://avatar/l?n=<fullName> 51 * 52 * 4) Default Avatars - These are avatars are used when the participant has no profile photo or 53 * name. In these cases we use the default person icon with a color background. The color 54 * background is based on a hash of the normalized phone number. 55 * 56 * 5) Default Background Avatars - This is a special case for Default Avatars where we use the 57 * default background color for the default avatar. 58 * 59 * 6) SIM Selector Avatars - These are avatars used in the SIM selector. This may either be a 60 * regular local resource avatar (2) or an avatar with a SIM identifier (i.e. SIM background with 61 * a letter or a slot number). 62 */ 63 public class AvatarUriUtil { 64 private static final int MAX_GROUP_PARTICIPANTS = 4; 65 66 public static final String TYPE_GROUP_URI = "g"; 67 public static final String TYPE_LOCAL_RESOURCE_URI = "r"; 68 public static final String TYPE_LETTER_TILE_URI = "l"; 69 public static final String TYPE_DEFAULT_URI = "d"; 70 public static final String TYPE_DEFAULT_BACKGROUND_URI = "b"; 71 public static final String TYPE_SIM_SELECTOR_URI = "s"; 72 73 private static final String SCHEME = "messaging"; 74 private static final String AUTHORITY = "avatar"; 75 private static final String PARAM_NAME = "n"; 76 private static final String PARAM_PRIMARY_URI = "m"; 77 private static final String PARAM_FALLBACK_URI = "f"; 78 private static final String PARAM_PARTICIPANT = "p"; 79 private static final String PARAM_IDENTIFIER = "i"; 80 private static final String PARAM_SIM_COLOR = "c"; 81 private static final String PARAM_SIM_SELECTED = "s"; 82 private static final String PARAM_SIM_INCOMING = "g"; 83 84 public static final Uri DEFAULT_BACKGROUND_AVATAR = new Uri.Builder().scheme(SCHEME) 85 .authority(AUTHORITY).appendPath(TYPE_DEFAULT_BACKGROUND_URI).build(); 86 87 private static final Uri BLANK_SIM_INDICATOR_INCOMING_URI = createSimIconUri("", 88 false /* selected */, Color.TRANSPARENT, true /* incoming */); 89 private static final Uri BLANK_SIM_INDICATOR_OUTGOING_URI = createSimIconUri("", 90 false /* selected */, Color.TRANSPARENT, false /* incoming */); 91 92 /** 93 * Creates an avatar uri based on a list of ParticipantData. The list of participants may not 94 * be null or empty. Depending on the size of the list either a group avatar uri will be create 95 * or an individual's avatar will be created. This will never return a null uri. 96 */ createAvatarUri(@onNull final List<ParticipantData> participants)97 public static Uri createAvatarUri(@NonNull final List<ParticipantData> participants) { 98 Assert.notNull(participants); 99 Assert.isTrue(!participants.isEmpty()); 100 101 if (participants.size() == 1) { 102 return createAvatarUri(participants.get(0)); 103 } 104 105 final int numParticipants = Math.min(participants.size(), MAX_GROUP_PARTICIPANTS); 106 final ArrayList<Uri> avatarUris = new ArrayList<Uri>(numParticipants); 107 for (int i = 0; i < numParticipants; i++) { 108 avatarUris.add(createAvatarUri(participants.get(i))); 109 } 110 return AvatarUriUtil.joinAvatarUriToGroup(avatarUris); 111 } 112 113 /** 114 * Joins together a list of valid avatar uri into a group uri.The list of participants may not 115 * be null or empty. If a lit of one is given then the first element will be return back 116 * instead of a group avatar uri. All uris in the list must be a valid avatar uri. This will 117 * never return a null uri. 118 */ joinAvatarUriToGroup(@onNull final List<Uri> avatarUris)119 public static Uri joinAvatarUriToGroup(@NonNull final List<Uri> avatarUris) { 120 Assert.notNull(avatarUris); 121 Assert.isTrue(!avatarUris.isEmpty()); 122 123 if (avatarUris.size() == 1) { 124 final Uri firstAvatar = avatarUris.get(0); 125 Assert.isTrue(AvatarUriUtil.isAvatarUri(firstAvatar)); 126 return firstAvatar; 127 } 128 129 final Builder builder = new Builder(); 130 builder.scheme(SCHEME); 131 builder.authority(AUTHORITY); 132 builder.appendPath(TYPE_GROUP_URI); 133 final int numParticipants = Math.min(avatarUris.size(), MAX_GROUP_PARTICIPANTS); 134 for (int i = 0; i < numParticipants; i++) { 135 final Uri uri = avatarUris.get(i); 136 Assert.notNull(uri); 137 Assert.isTrue(UriUtil.isLocalResourceUri(uri) || AvatarUriUtil.isAvatarUri(uri)); 138 builder.appendQueryParameter(PARAM_PARTICIPANT, uri.toString()); 139 } 140 return builder.build(); 141 } 142 143 /** 144 * Creates an avatar uri based on ParticipantData which may not be null and expected to have 145 * profilePhotoUri, fullName and normalizedDestination populated. This will never return a null 146 * uri. 147 */ createAvatarUri(@onNull final ParticipantData participant)148 public static Uri createAvatarUri(@NonNull final ParticipantData participant) { 149 Assert.notNull(participant); 150 final String photoUriString = participant.getProfilePhotoUri(); 151 final Uri profilePhotoUri = (photoUriString == null) ? null : Uri.parse(photoUriString); 152 final String name = participant.getFullName(); 153 final String destination = participant.getNormalizedDestination(); 154 final String contactLookupKey = participant.getLookupKey(); 155 return createAvatarUri(profilePhotoUri, name, destination, contactLookupKey); 156 } 157 158 /** 159 * Creates an avatar uri based on a the input data. 160 */ createAvatarUri(final Uri profilePhotoUri, final CharSequence name, final String defaultIdentifier, final String contactLookupKey)161 public static Uri createAvatarUri(final Uri profilePhotoUri, final CharSequence name, 162 final String defaultIdentifier, final String contactLookupKey) { 163 Uri generatedUri; 164 if (!TextUtils.isEmpty(name) && isValidFirstCharacter(name)) { 165 generatedUri = AvatarUriUtil.fromName(name, contactLookupKey); 166 } else { 167 final String identifier = TextUtils.isEmpty(contactLookupKey) 168 ? defaultIdentifier : contactLookupKey; 169 generatedUri = AvatarUriUtil.fromIdentifier(identifier); 170 } 171 172 if (profilePhotoUri != null) { 173 if (UriUtil.isLocalResourceUri(profilePhotoUri)) { 174 return fromLocalResourceWithFallback(profilePhotoUri, generatedUri); 175 } else { 176 return profilePhotoUri; 177 } 178 } else { 179 return generatedUri; 180 } 181 } 182 isValidFirstCharacter(final CharSequence name)183 public static boolean isValidFirstCharacter(final CharSequence name) { 184 final char c = name.charAt(0); 185 return c != '+'; 186 } 187 188 /** 189 * Creates an avatar URI used for the SIM selector. 190 * @param participantData the self participant data for an <i>active</i> SIM 191 * @param slotIdentifier when null, this will simply use a regular avatar; otherwise, the 192 * first letter of slotIdentifier will be used for the icon. 193 * @param selected is this the currently selected SIM? 194 * @param incoming is this for an incoming message or outgoing message? 195 */ createAvatarUri(@onNull final ParticipantData participantData, @Nullable final String slotIdentifier, final boolean selected, final boolean incoming)196 public static Uri createAvatarUri(@NonNull final ParticipantData participantData, 197 @Nullable final String slotIdentifier, final boolean selected, final boolean incoming) { 198 Assert.notNull(participantData); 199 Assert.isTrue(participantData.isActiveSubscription()); 200 Assert.isTrue(!TextUtils.isEmpty(slotIdentifier) || 201 !TextUtils.isEmpty(participantData.getProfilePhotoUri())); 202 if (TextUtils.isEmpty(slotIdentifier)) { 203 return createAvatarUri(participantData); 204 } 205 206 return createSimIconUri(slotIdentifier, selected, participantData.getSubscriptionColor(), 207 incoming); 208 } 209 createSimIconUri(final String slotIdentifier, final boolean selected, final int subColor, final boolean incoming)210 private static Uri createSimIconUri(final String slotIdentifier, final boolean selected, 211 final int subColor, final boolean incoming) { 212 final Builder builder = new Builder(); 213 builder.scheme(SCHEME); 214 builder.authority(AUTHORITY); 215 builder.appendPath(TYPE_SIM_SELECTOR_URI); 216 builder.appendQueryParameter(PARAM_IDENTIFIER, slotIdentifier); 217 builder.appendQueryParameter(PARAM_SIM_COLOR, String.valueOf(subColor)); 218 builder.appendQueryParameter(PARAM_SIM_SELECTED, String.valueOf(selected)); 219 builder.appendQueryParameter(PARAM_SIM_INCOMING, String.valueOf(incoming)); 220 return builder.build(); 221 } 222 getBlankSimIndicatorUri(final boolean incoming)223 public static Uri getBlankSimIndicatorUri(final boolean incoming) { 224 return incoming ? BLANK_SIM_INDICATOR_INCOMING_URI : BLANK_SIM_INDICATOR_OUTGOING_URI; 225 } 226 227 /** 228 * Creates an avatar uri from the given local resource Uri, followed by a fallback Uri in case 229 * the local resource one could not be loaded. 230 */ fromLocalResourceWithFallback(@onNull final Uri profilePhotoUri, @NonNull Uri fallbackUri)231 private static Uri fromLocalResourceWithFallback(@NonNull final Uri profilePhotoUri, 232 @NonNull Uri fallbackUri) { 233 Assert.notNull(profilePhotoUri); 234 Assert.notNull(fallbackUri); 235 final Builder builder = new Builder(); 236 builder.scheme(SCHEME); 237 builder.authority(AUTHORITY); 238 builder.appendPath(TYPE_LOCAL_RESOURCE_URI); 239 builder.appendQueryParameter(PARAM_PRIMARY_URI, profilePhotoUri.toString()); 240 builder.appendQueryParameter(PARAM_FALLBACK_URI, fallbackUri.toString()); 241 return builder.build(); 242 } 243 fromName(@onNull final CharSequence name, final String contactLookupKey)244 private static Uri fromName(@NonNull final CharSequence name, final String contactLookupKey) { 245 Assert.notNull(name); 246 final Builder builder = new Builder(); 247 builder.scheme(SCHEME); 248 builder.authority(AUTHORITY); 249 builder.appendPath(TYPE_LETTER_TILE_URI); 250 final String nameString = String.valueOf(name); 251 builder.appendQueryParameter(PARAM_NAME, nameString); 252 final String identifier = 253 TextUtils.isEmpty(contactLookupKey) ? nameString : contactLookupKey; 254 builder.appendQueryParameter(PARAM_IDENTIFIER, identifier); 255 return builder.build(); 256 } 257 fromIdentifier(@onNull final String identifier)258 private static Uri fromIdentifier(@NonNull final String identifier) { 259 final Builder builder = new Builder(); 260 builder.scheme(SCHEME); 261 builder.authority(AUTHORITY); 262 builder.appendPath(TYPE_DEFAULT_URI); 263 builder.appendQueryParameter(PARAM_IDENTIFIER, identifier); 264 return builder.build(); 265 } 266 isAvatarUri(@onNull final Uri uri)267 public static boolean isAvatarUri(@NonNull final Uri uri) { 268 Assert.notNull(uri); 269 return uri != null && TextUtils.equals(SCHEME, uri.getScheme()) && 270 TextUtils.equals(AUTHORITY, uri.getAuthority()); 271 } 272 getAvatarType(@onNull final Uri uri)273 public static String getAvatarType(@NonNull final Uri uri) { 274 Assert.notNull(uri); 275 final List<String> path = uri.getPathSegments(); 276 return path.isEmpty() ? null : path.get(0); 277 } 278 getIdentifier(@onNull final Uri uri)279 public static String getIdentifier(@NonNull final Uri uri) { 280 Assert.notNull(uri); 281 return uri.getQueryParameter(PARAM_IDENTIFIER); 282 } 283 getName(@onNull final Uri uri)284 public static String getName(@NonNull final Uri uri) { 285 Assert.notNull(uri); 286 return uri.getQueryParameter(PARAM_NAME); 287 } 288 getGroupParticipantUris(@onNull final Uri uri)289 public static List<String> getGroupParticipantUris(@NonNull final Uri uri) { 290 Assert.notNull(uri); 291 return uri.getQueryParameters(PARAM_PARTICIPANT); 292 } 293 getSimColor(@onNull final Uri uri)294 public static int getSimColor(@NonNull final Uri uri) { 295 Assert.notNull(uri); 296 return Integer.valueOf(uri.getQueryParameter(PARAM_SIM_COLOR)); 297 } 298 getSimSelected(@onNull final Uri uri)299 public static boolean getSimSelected(@NonNull final Uri uri) { 300 Assert.notNull(uri); 301 return Boolean.valueOf(uri.getQueryParameter(PARAM_SIM_SELECTED)); 302 } 303 getSimIncoming(@onNull final Uri uri)304 public static boolean getSimIncoming(@NonNull final Uri uri) { 305 Assert.notNull(uri); 306 return Boolean.valueOf(uri.getQueryParameter(PARAM_SIM_INCOMING)); 307 } 308 getPrimaryUri(@onNull final Uri uri)309 public static Uri getPrimaryUri(@NonNull final Uri uri) { 310 Assert.notNull(uri); 311 final String primaryUriString = uri.getQueryParameter(PARAM_PRIMARY_URI); 312 return primaryUriString == null ? null : Uri.parse(primaryUriString); 313 } 314 getFallbackUri(@onNull final Uri uri)315 public static Uri getFallbackUri(@NonNull final Uri uri) { 316 Assert.notNull(uri); 317 final String fallbackUriString = uri.getQueryParameter(PARAM_FALLBACK_URI); 318 return fallbackUriString == null ? null : Uri.parse(fallbackUriString); 319 } 320 } 321