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