AvatarService.java

  1package eu.siacs.conversations.services;
  2
  3import android.content.Context;
  4import android.content.res.Resources;
  5import android.graphics.Bitmap;
  6import android.graphics.Canvas;
  7import android.graphics.Paint;
  8import android.graphics.PorterDuff;
  9import android.graphics.PorterDuffXfermode;
 10import android.graphics.Rect;
 11import android.graphics.Typeface;
 12import android.graphics.drawable.BitmapDrawable;
 13import android.graphics.drawable.Drawable;
 14import android.net.Uri;
 15import android.text.TextUtils;
 16import android.util.DisplayMetrics;
 17import androidx.annotation.ColorInt;
 18import androidx.annotation.Nullable;
 19import androidx.core.content.res.ResourcesCompat;
 20import com.google.common.base.Strings;
 21import eu.siacs.conversations.R;
 22import eu.siacs.conversations.entities.Account;
 23import eu.siacs.conversations.entities.Bookmark;
 24import eu.siacs.conversations.entities.Contact;
 25import eu.siacs.conversations.entities.Conversation;
 26import eu.siacs.conversations.entities.Conversational;
 27import eu.siacs.conversations.entities.ListItem;
 28import eu.siacs.conversations.entities.Message;
 29import eu.siacs.conversations.entities.MucOptions;
 30import eu.siacs.conversations.entities.RawBlockable;
 31import eu.siacs.conversations.entities.Room;
 32import eu.siacs.conversations.utils.UIHelper;
 33import eu.siacs.conversations.xmpp.Jid;
 34import java.util.HashMap;
 35import java.util.HashSet;
 36import java.util.List;
 37import java.util.Locale;
 38import java.util.Set;
 39
 40public class AvatarService {
 41
 42    private static final int FG_COLOR = 0xFFFAFAFA;
 43    private static final int TRANSPARENT = 0x00000000;
 44    private static final int PLACEHOLDER_COLOR = 0xFF202020;
 45
 46    public static final int SYSTEM_UI_AVATAR_SIZE = 48;
 47
 48    private static final String PREFIX_CONTACT = "contact";
 49    private static final String PREFIX_CONVERSATION = "conversation";
 50    private static final String PREFIX_ACCOUNT = "account";
 51    private static final String PREFIX_GENERIC = "generic";
 52
 53    private static final String CHANNEL_SYMBOL = "#";
 54
 55    private final Set<Integer> sizes = new HashSet<>();
 56    // TODO refactor to multimap
 57    private final HashMap<String, Set<String>> conversationDependentKeys = new HashMap<>();
 58
 59    protected XmppConnectionService mXmppConnectionService = null;
 60
 61    AvatarService(XmppConnectionService service) {
 62        this.mXmppConnectionService = service;
 63    }
 64
 65    public static int getSystemUiAvatarSize(final Context context) {
 66        return (int) (SYSTEM_UI_AVATAR_SIZE * context.getResources().getDisplayMetrics().density);
 67    }
 68
 69    public Bitmap get(final Avatarable avatarable, final int size, final boolean cachedOnly) {
 70        if (avatarable instanceof Account) {
 71            return get((Account) avatarable, size, cachedOnly);
 72        } else if (avatarable instanceof Conversation) {
 73            return get((Conversation) avatarable, size, cachedOnly);
 74        } else if (avatarable instanceof Message) {
 75            return get((Message) avatarable, size, cachedOnly);
 76        } else if (avatarable instanceof ListItem) {
 77            return get((ListItem) avatarable, size, cachedOnly);
 78        } else if (avatarable instanceof MucOptions.User) {
 79            return get((MucOptions.User) avatarable, size, cachedOnly);
 80        } else if (avatarable instanceof Room) {
 81            return get((Room) avatarable, size, cachedOnly);
 82        }
 83        throw new AssertionError(
 84                "AvatarService does not know how to generate avatar from "
 85                        + avatarable.getClass().getName());
 86    }
 87
 88    private Bitmap get(final Room result, final int size, boolean cacheOnly) {
 89        final Jid room = result.getRoom();
 90        Conversation conversation = room != null ? mXmppConnectionService.findFirstMuc(room) : null;
 91        if (conversation != null) {
 92            return get(conversation, size, cacheOnly);
 93        }
 94        return get(
 95                CHANNEL_SYMBOL,
 96                room != null ? room.asBareJid().toString() : result.getName(),
 97                size,
 98                cacheOnly);
 99    }
100
101    private Bitmap get(final Contact contact, final int size, boolean cachedOnly) {
102        if (contact.isSelf()) {
103            return get(contact.getAccount(), size, cachedOnly);
104        }
105        final String KEY = key(contact, size);
106        Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY);
107        if (avatar != null || cachedOnly) {
108            return avatar;
109        }
110        if (contact.getAvatar() != null && QuickConversationsService.isQuicksy()) {
111            avatar = mXmppConnectionService.getFileBackend().getAvatar(contact.getAvatar(), size);
112        }
113        if (avatar == null && contact.getProfilePhoto() != null) {
114            avatar =
115                    mXmppConnectionService
116                            .getFileBackend()
117                            .cropCenterSquare(Uri.parse(contact.getProfilePhoto()), size);
118        }
119        if (avatar == null && contact.getAvatar() != null) {
120            avatar = mXmppConnectionService.getFileBackend().getAvatar(contact.getAvatar(), size);
121        }
122        if (avatar == null) {
123            avatar =
124                    get(
125                            contact.getDisplayName(),
126                            contact.getJid().asBareJid().toString(),
127                            size,
128                            false);
129        }
130        this.mXmppConnectionService.getBitmapCache().put(KEY, avatar);
131        return avatar;
132    }
133
134    public Bitmap getRoundedShortcut(final MucOptions mucOptions) {
135        final DisplayMetrics metrics = mXmppConnectionService.getResources().getDisplayMetrics();
136        final int size = Math.round(metrics.density * 48);
137        final Bitmap bitmap = get(mucOptions, size, false);
138        final Bitmap output =
139                Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
140        final Canvas canvas = new Canvas(output);
141        final Paint paint = new Paint();
142        drawAvatar(bitmap, canvas, paint);
143        return output;
144    }
145
146    public Bitmap getRoundedShortcut(final Contact contact) {
147        return getRoundedShortcut(contact, false);
148    }
149
150    public Bitmap getRoundedShortcutWithIcon(final Contact contact) {
151        return getRoundedShortcut(contact, true);
152    }
153
154    private Bitmap getRoundedShortcut(final Contact contact, boolean withIcon) {
155        DisplayMetrics metrics = mXmppConnectionService.getResources().getDisplayMetrics();
156        int size = Math.round(metrics.density * 48);
157        Bitmap bitmap = get(contact, size);
158        Bitmap output =
159                Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
160        Canvas canvas = new Canvas(output);
161        final Paint paint = new Paint();
162
163        drawAvatar(bitmap, canvas, paint);
164        if (withIcon) {
165            drawIcon(canvas, paint);
166        }
167        return output;
168    }
169
170    private static void drawAvatar(Bitmap bitmap, Canvas canvas, Paint paint) {
171        final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
172        paint.setAntiAlias(true);
173        canvas.drawARGB(0, 0, 0, 0);
174        canvas.drawCircle(
175                bitmap.getWidth() / 2, bitmap.getHeight() / 2, bitmap.getWidth() / 2, paint);
176        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
177        canvas.drawBitmap(bitmap, rect, rect, paint);
178    }
179
180    private void drawIcon(Canvas canvas, Paint paint) {
181        final Resources resources = mXmppConnectionService.getResources();
182        final Bitmap icon = getRoundLauncherIcon(resources);
183        if (icon == null) {
184            return;
185        }
186        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));
187
188        int iconSize = Math.round(canvas.getHeight() / 2.6f);
189
190        int left = canvas.getWidth() - iconSize;
191        int top = canvas.getHeight() - iconSize;
192        final Rect rect = new Rect(left, top, left + iconSize, top + iconSize);
193        canvas.drawBitmap(icon, null, rect, paint);
194    }
195
196    private static Bitmap getRoundLauncherIcon(Resources resources) {
197
198        final Drawable drawable =
199                ResourcesCompat.getDrawable(resources, R.mipmap.new_launcher_round, null);
200        if (drawable == null) {
201            return null;
202        }
203
204        if (drawable instanceof BitmapDrawable) {
205            return ((BitmapDrawable) drawable).getBitmap();
206        }
207
208        Bitmap bitmap =
209                Bitmap.createBitmap(
210                        drawable.getIntrinsicWidth(),
211                        drawable.getIntrinsicHeight(),
212                        Bitmap.Config.ARGB_8888);
213        Canvas canvas = new Canvas(bitmap);
214        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
215        drawable.draw(canvas);
216
217        return bitmap;
218    }
219
220    public Bitmap get(final MucOptions.User user, final int size, boolean cachedOnly) {
221        Contact c = user.getContact();
222        if (c != null
223                && (c.getProfilePhoto() != null
224                        || c.getAvatar() != null
225                        || user.getAvatar() == null)) {
226            return get(c, size, cachedOnly);
227        } else {
228            return getImpl(user, size, cachedOnly);
229        }
230    }
231
232    private Bitmap getImpl(final MucOptions.User user, final int size, boolean cachedOnly) {
233        final String KEY = key(user, size);
234        Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY);
235        if (avatar != null || cachedOnly) {
236            return avatar;
237        }
238        if (user.getAvatar() != null) {
239            avatar = mXmppConnectionService.getFileBackend().getAvatar(user.getAvatar(), size);
240        }
241        if (avatar == null) {
242            Contact contact = user.getContact();
243            if (contact != null) {
244                avatar = get(contact, size, false);
245            } else {
246                String seed =
247                        user.getRealJid() != null ? user.getRealJid().asBareJid().toString() : null;
248                avatar = get(user.getName(), seed, size, false);
249            }
250        }
251        this.mXmppConnectionService.getBitmapCache().put(KEY, avatar);
252        return avatar;
253    }
254
255    public void clear(final Contact contact) {
256        synchronized (this.sizes) {
257            for (final Integer size : sizes) {
258                this.mXmppConnectionService.getBitmapCache().remove(key(contact, size));
259            }
260        }
261        for (final Conversation conversation :
262                mXmppConnectionService.findAllConferencesWith(contact)) {
263            final var mucOptions = conversation.getMucOptions();
264            final var user = mucOptions.findUserByRealJid(contact.getJid().asBareJid());
265            if (user != null) {
266                clear(user);
267            }
268            if (Strings.isNullOrEmpty(mucOptions.getAvatar())
269                    && mucOptions.isPrivateAndNonAnonymous()) {
270                clear(mucOptions);
271            }
272        }
273    }
274
275    private String key(final Contact contact, final int size) {
276        synchronized (this.sizes) {
277            this.sizes.add(size);
278        }
279        return PREFIX_CONTACT
280                + '\0'
281                + contact.getAccount().getJid().asBareJid()
282                + '\0'
283                + emptyOnNull(contact.getJid())
284                + '\0'
285                + size;
286    }
287
288    private String key(MucOptions.User user, int size) {
289        synchronized (this.sizes) {
290            this.sizes.add(size);
291        }
292        return PREFIX_CONTACT
293                + '\0'
294                + user.getAccount().getJid().asBareJid()
295                + '\0'
296                + emptyOnNull(user.getFullJid())
297                + '\0'
298                + emptyOnNull(user.getRealJid())
299                + '\0'
300                + size;
301    }
302
303    public Bitmap get(ListItem item, int size) {
304        return get(item, size, false);
305    }
306
307    public Bitmap get(ListItem item, int size, boolean cachedOnly) {
308        if (item instanceof RawBlockable) {
309            return get(item.getDisplayName(), item.getJid().toString(), size, cachedOnly);
310        } else if (item instanceof Contact) {
311            return get((Contact) item, size, cachedOnly);
312        } else if (item instanceof Bookmark bookmark) {
313            if (bookmark.getConversation() != null) {
314                return get(bookmark.getConversation(), size, cachedOnly);
315            } else {
316                Jid jid = bookmark.getJid();
317                Account account = bookmark.getAccount();
318                Contact contact = jid == null ? null : account.getRoster().getContact(jid);
319                if (contact != null && contact.getAvatar() != null) {
320                    return get(contact, size, cachedOnly);
321                }
322                String seed = jid != null ? jid.asBareJid().toString() : null;
323                return get(bookmark.getDisplayName(), seed, size, cachedOnly);
324            }
325        } else {
326            String seed = item.getJid() != null ? item.getJid().asBareJid().toString() : null;
327            return get(item.getDisplayName(), seed, size, cachedOnly);
328        }
329    }
330
331    public Bitmap get(Conversation conversation, int size) {
332        return get(conversation, size, false);
333    }
334
335    public Bitmap get(final Conversation conversation, final int size, final boolean cachedOnly) {
336        if (conversation.getMode() == Conversation.MODE_SINGLE) {
337            return get(conversation.getContact(), size, cachedOnly);
338        } else {
339            return get(conversation.getMucOptions(), size, cachedOnly);
340        }
341    }
342
343    private Bitmap get(final MucOptions mucOptions, final int size, final boolean cachedOnly) {
344        final String KEY = key(mucOptions, size);
345        Bitmap bitmap = this.mXmppConnectionService.getBitmapCache().get(KEY);
346        if (bitmap != null || cachedOnly) {
347            return bitmap;
348        }
349
350        bitmap = mXmppConnectionService.getFileBackend().getAvatar(mucOptions.getAvatar(), size);
351
352        if (bitmap == null) {
353            Conversation c = mucOptions.getConversation();
354            if (mucOptions.isPrivateAndNonAnonymous()) {
355                final List<MucOptions.User> users = mucOptions.getUsersRelevantForNameAndAvatar();
356                if (users.isEmpty()) {
357                    bitmap =
358                            getImpl(
359                                    c.getName().toString(),
360                                    c.getJid().asBareJid().toString(),
361                                    size);
362                } else {
363                    bitmap = getImpl(users, size);
364                }
365            } else {
366                bitmap = getImpl(CHANNEL_SYMBOL, c.getJid().asBareJid().toString(), size);
367            }
368        }
369
370        this.mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
371
372        return bitmap;
373    }
374
375    private Bitmap get(List<MucOptions.User> users, int size, boolean cachedOnly) {
376        final String KEY = key(users, size);
377        Bitmap bitmap = this.mXmppConnectionService.getBitmapCache().get(KEY);
378        if (bitmap != null || cachedOnly) {
379            return bitmap;
380        }
381        bitmap = getImpl(users, size);
382        this.mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
383        return bitmap;
384    }
385
386    private Bitmap getImpl(List<MucOptions.User> users, int size) {
387        int count = users.size();
388        Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
389        Canvas canvas = new Canvas(bitmap);
390        bitmap.eraseColor(TRANSPARENT);
391        if (count == 0) {
392            throw new AssertionError("Unable to draw tiles for 0 users");
393        } else if (count == 1) {
394            drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size);
395            drawTile(canvas, users.get(0).getAccount(), size / 2 + 1, 0, size, size);
396        } else if (count == 2) {
397            drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size);
398            drawTile(canvas, users.get(1), size / 2 + 1, 0, size, size);
399        } else if (count == 3) {
400            drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size);
401            drawTile(canvas, users.get(1), size / 2 + 1, 0, size, size / 2 - 1);
402            drawTile(canvas, users.get(2), size / 2 + 1, size / 2 + 1, size, size);
403        } else if (count == 4) {
404            drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size / 2 - 1);
405            drawTile(canvas, users.get(1), 0, size / 2 + 1, size / 2 - 1, size);
406            drawTile(canvas, users.get(2), size / 2 + 1, 0, size, size / 2 - 1);
407            drawTile(canvas, users.get(3), size / 2 + 1, size / 2 + 1, size, size);
408        } else {
409            drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size / 2 - 1);
410            drawTile(canvas, users.get(1), 0, size / 2 + 1, size / 2 - 1, size);
411            drawTile(canvas, users.get(2), size / 2 + 1, 0, size, size / 2 - 1);
412            drawTile(canvas, "\u2026", PLACEHOLDER_COLOR, size / 2 + 1, size / 2 + 1, size, size);
413        }
414        return bitmap;
415    }
416
417    public void clear(final MucOptions options) {
418        if (options == null) {
419            return;
420        }
421        synchronized (this.sizes) {
422            for (Integer size : sizes) {
423                this.mXmppConnectionService.getBitmapCache().remove(key(options, size));
424            }
425        }
426    }
427
428    private String key(final MucOptions options, int size) {
429        synchronized (this.sizes) {
430            this.sizes.add(size);
431        }
432        return PREFIX_CONVERSATION + "_" + options.getConversation().getUuid() + "_" + size;
433    }
434
435    private String key(final List<MucOptions.User> users, final int size) {
436        final Conversation conversation = users.get(0).getConversation();
437        StringBuilder builder = new StringBuilder("TILE_");
438        builder.append(conversation.getUuid());
439
440        for (final MucOptions.User user : users) {
441            builder.append("\0");
442            builder.append(emptyOnNull(user.getRealJid()));
443            builder.append("\0");
444            builder.append(emptyOnNull(user.getFullJid()));
445        }
446        builder.append('\0');
447        builder.append(size);
448        final String key = builder.toString();
449        synchronized (this.conversationDependentKeys) {
450            Set<String> keys;
451            if (this.conversationDependentKeys.containsKey(conversation.getUuid())) {
452                keys = this.conversationDependentKeys.get(conversation.getUuid());
453            } else {
454                keys = new HashSet<>();
455                this.conversationDependentKeys.put(conversation.getUuid(), keys);
456            }
457            keys.add(key);
458        }
459        return key;
460    }
461
462    public Bitmap get(Account account, int size) {
463        return get(account, size, false);
464    }
465
466    public Bitmap get(Account account, int size, boolean cachedOnly) {
467        final String KEY = key(account, size);
468        Bitmap avatar = mXmppConnectionService.getBitmapCache().get(KEY);
469        if (avatar != null || cachedOnly) {
470            return avatar;
471        }
472        avatar = mXmppConnectionService.getFileBackend().getAvatar(account.getAvatar(), size);
473        if (avatar == null) {
474            final String displayName = account.getDisplayName();
475            final String jid = account.getJid().asBareJid().toString();
476            if (QuickConversationsService.isQuicksy() && !TextUtils.isEmpty(displayName)) {
477                avatar = get(displayName, jid, size, false);
478            } else {
479                avatar = get(jid, null, size, false);
480            }
481        }
482        mXmppConnectionService.getBitmapCache().put(KEY, avatar);
483        return avatar;
484    }
485
486    public Bitmap get(Message message, int size, boolean cachedOnly) {
487        final Conversational conversation = message.getConversation();
488        if (message.getType() == Message.TYPE_STATUS
489                && message.getCounterparts() != null
490                && message.getCounterparts().size() > 1) {
491            return get(message.getCounterparts(), size, cachedOnly);
492        } else if (message.getStatus() == Message.STATUS_RECEIVED) {
493            Contact c = message.getContact();
494            if (c != null && (c.getProfilePhoto() != null || c.getAvatar() != null)) {
495                return get(c, size, cachedOnly);
496            } else if (conversation instanceof Conversation
497                    && message.getConversation().getMode() == Conversation.MODE_MULTI) {
498                final Jid trueCounterpart = message.getTrueCounterpart();
499                final MucOptions mucOptions = ((Conversation) conversation).getMucOptions();
500                MucOptions.User user;
501                if (trueCounterpart != null) {
502                    user =
503                            mucOptions.findOrCreateUserByRealJid(
504                                    trueCounterpart, message.getCounterpart());
505                } else {
506                    user = mucOptions.findUserByFullJid(message.getCounterpart());
507                }
508                if (user != null) {
509                    return getImpl(user, size, cachedOnly);
510                }
511            } else if (c != null) {
512                return get(c, size, cachedOnly);
513            }
514            Jid tcp = message.getTrueCounterpart();
515            String seed = tcp != null ? tcp.asBareJid().toString() : null;
516            return get(UIHelper.getMessageDisplayName(message), seed, size, cachedOnly);
517        } else {
518            return get(conversation.getAccount(), size, cachedOnly);
519        }
520    }
521
522    public void clear(Account account) {
523        synchronized (this.sizes) {
524            for (Integer size : sizes) {
525                this.mXmppConnectionService.getBitmapCache().remove(key(account, size));
526            }
527        }
528    }
529
530    public void clear(final MucOptions.User user) {
531        synchronized (this.sizes) {
532            for (Integer size : sizes) {
533                this.mXmppConnectionService.getBitmapCache().remove(key(user, size));
534            }
535        }
536        synchronized (this.conversationDependentKeys) {
537            final Set<String> keys =
538                    this.conversationDependentKeys.get(user.getConversation().getUuid());
539            if (keys == null) {
540                return;
541            }
542            final var cache = this.mXmppConnectionService.getBitmapCache();
543            for (final String key : keys) {
544                cache.remove(key);
545            }
546            keys.clear();
547        }
548    }
549
550    private String key(Account account, int size) {
551        synchronized (this.sizes) {
552            this.sizes.add(size);
553        }
554        return PREFIX_ACCOUNT + "_" + account.getUuid() + "_" + size;
555    }
556
557    /*public Bitmap get(String name, int size) {
558    	return get(name,null, size,false);
559    }*/
560
561    public Bitmap get(final String name, String seed, final int size, boolean cachedOnly) {
562        final String KEY = key(seed == null ? name : name + "\0" + seed, size);
563        Bitmap bitmap = mXmppConnectionService.getBitmapCache().get(KEY);
564        if (bitmap != null || cachedOnly) {
565            return bitmap;
566        }
567        bitmap = getImpl(name, seed, size);
568        mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
569        return bitmap;
570    }
571
572    public static Bitmap get(final Jid jid, final int size) {
573        return getImpl(jid.asBareJid().toString(), null, size);
574    }
575
576    private static Bitmap getImpl(final String name, final String seed, final int size) {
577        Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
578        Canvas canvas = new Canvas(bitmap);
579        final String trimmedName = name == null ? "" : name.trim();
580        drawTile(canvas, trimmedName, seed, 0, 0, size, size);
581        return bitmap;
582    }
583
584    private String key(String name, int size) {
585        synchronized (this.sizes) {
586            this.sizes.add(size);
587        }
588        return PREFIX_GENERIC + "_" + name + "_" + size;
589    }
590
591    private static boolean drawTile(
592            Canvas canvas, String letter, int tileColor, int left, int top, int right, int bottom) {
593        letter = letter.toUpperCase(Locale.getDefault());
594        Paint tilePaint = new Paint(), textPaint = new Paint();
595        tilePaint.setColor(tileColor);
596        textPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
597        textPaint.setColor(FG_COLOR);
598        textPaint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));
599        textPaint.setTextSize((float) ((right - left) * 0.8));
600        Rect rect = new Rect();
601
602        canvas.drawRect(new Rect(left, top, right, bottom), tilePaint);
603        textPaint.getTextBounds(letter, 0, 1, rect);
604        float width = textPaint.measureText(letter);
605        canvas.drawText(
606                letter,
607                (right + left) / 2 - width / 2,
608                (top + bottom) / 2 + rect.height() / 2,
609                textPaint);
610        return true;
611    }
612
613    private boolean drawTile(
614            Canvas canvas, MucOptions.User user, int left, int top, int right, int bottom) {
615        Contact contact = user.getContact();
616        if (contact != null) {
617            Uri uri = null;
618            if (contact.getAvatar() != null && QuickConversationsService.isQuicksy()) {
619                uri = mXmppConnectionService.getFileBackend().getAvatarUri(contact.getAvatar());
620            } else if (contact.getProfilePhoto() != null) {
621                uri = Uri.parse(contact.getProfilePhoto());
622            } else if (contact.getAvatar() != null) {
623                uri = mXmppConnectionService.getFileBackend().getAvatarUri(contact.getAvatar());
624            }
625            if (drawTile(canvas, uri, left, top, right, bottom)) {
626                return true;
627            }
628        } else if (user.getAvatar() != null) {
629            Uri uri = mXmppConnectionService.getFileBackend().getAvatarUri(user.getAvatar());
630            if (drawTile(canvas, uri, left, top, right, bottom)) {
631                return true;
632            }
633        }
634        if (contact != null) {
635            String seed = contact.getJid().asBareJid().toString();
636            drawTile(canvas, contact.getDisplayName(), seed, left, top, right, bottom);
637        } else {
638            String seed =
639                    user.getRealJid() == null ? null : user.getRealJid().asBareJid().toString();
640            drawTile(canvas, user.getName(), seed, left, top, right, bottom);
641        }
642        return true;
643    }
644
645    private boolean drawTile(
646            Canvas canvas, Account account, int left, int top, int right, int bottom) {
647        String avatar = account.getAvatar();
648        if (avatar != null) {
649            Uri uri = mXmppConnectionService.getFileBackend().getAvatarUri(avatar);
650            if (uri != null) {
651                if (drawTile(canvas, uri, left, top, right, bottom)) {
652                    return true;
653                }
654            }
655        }
656        String name = account.getJid().asBareJid().toString();
657        return drawTile(canvas, name, name, left, top, right, bottom);
658    }
659
660    private static boolean drawTile(
661            Canvas canvas, String name, String seed, int left, int top, int right, int bottom) {
662        if (name != null) {
663            final String letter = name.equals(CHANNEL_SYMBOL) ? name : getFirstLetter(name);
664            final int color = UIHelper.getColorForName(seed == null ? name : seed);
665            drawTile(canvas, letter, color, left, top, right, bottom);
666            return true;
667        }
668        return false;
669    }
670
671    private static String getFirstLetter(String name) {
672        for (Character c : name.toCharArray()) {
673            if (Character.isLetterOrDigit(c)) {
674                return c.toString();
675            }
676        }
677        return "X";
678    }
679
680    private boolean drawTile(Canvas canvas, Uri uri, int left, int top, int right, int bottom) {
681        if (uri != null) {
682            Bitmap bitmap =
683                    mXmppConnectionService
684                            .getFileBackend()
685                            .cropCenter(uri, bottom - top, right - left);
686            if (bitmap != null) {
687                drawTile(canvas, bitmap, left, top, right, bottom);
688                return true;
689            }
690        }
691        return false;
692    }
693
694    private boolean drawTile(
695            Canvas canvas, Bitmap bm, int dstleft, int dsttop, int dstright, int dstbottom) {
696        Rect dst = new Rect(dstleft, dsttop, dstright, dstbottom);
697        canvas.drawBitmap(bm, null, dst, null);
698        return true;
699    }
700
701    private static String emptyOnNull(@Nullable Jid value) {
702        return value == null ? "" : value.toString();
703    }
704
705    public interface Avatarable {
706        @ColorInt
707        int getAvatarBackgroundColor();
708
709        String getAvatarName();
710    }
711}