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 */
16package com.android.messaging.util;
17
18import android.graphics.Color;
19import android.net.Uri;
20import android.net.Uri.Builder;
21import android.support.annotation.NonNull;
22import android.support.annotation.Nullable;
23import android.text.TextUtils;
24
25import com.android.messaging.datamodel.data.ParticipantData;
26
27import java.util.ArrayList;
28import 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 */
63public 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     */
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     */
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     */
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     */
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
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     */
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
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
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     */
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
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
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
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
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
279    public static String getIdentifier(@NonNull final Uri uri) {
280        Assert.notNull(uri);
281        return uri.getQueryParameter(PARAM_IDENTIFIER);
282    }
283
284    public static String getName(@NonNull final Uri uri) {
285        Assert.notNull(uri);
286        return uri.getQueryParameter(PARAM_NAME);
287    }
288
289    public static List<String> getGroupParticipantUris(@NonNull final Uri uri) {
290        Assert.notNull(uri);
291        return uri.getQueryParameters(PARAM_PARTICIPANT);
292    }
293
294    public static int getSimColor(@NonNull final Uri uri) {
295        Assert.notNull(uri);
296        return Integer.valueOf(uri.getQueryParameter(PARAM_SIM_COLOR));
297    }
298
299    public static boolean getSimSelected(@NonNull final Uri uri) {
300        Assert.notNull(uri);
301        return Boolean.valueOf(uri.getQueryParameter(PARAM_SIM_SELECTED));
302    }
303
304    public static boolean getSimIncoming(@NonNull final Uri uri) {
305        Assert.notNull(uri);
306        return Boolean.valueOf(uri.getQueryParameter(PARAM_SIM_INCOMING));
307    }
308
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
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