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