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