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(final Conversation conversation, final int size, final 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    private Bitmap get(final MucOptions mucOptions, final int size, final boolean cachedOnly) {
354        final String KEY = key(mucOptions, size);
355        Bitmap bitmap = this.mXmppConnectionService.getBitmapCache().get(KEY);
356        if (bitmap != null || cachedOnly) {
357            return bitmap;
358        }
359
360        bitmap = mXmppConnectionService.getFileBackend().getAvatar(mucOptions.getAvatar(), size);
361
362        if (bitmap == null) {
363            Conversation c = mucOptions.getConversation();
364            if (mucOptions.isPrivateAndNonAnonymous()) {
365                final List<MucOptions.User> users = mucOptions.getUsersRelevantForNameAndAvatar();
366                if (users.isEmpty()) {
367                    bitmap =
368                            getImpl(
369                                    c.getName().toString(),
370                                    c.getJid().asBareJid().toString(),
371                                    size);
372                } else {
373                    bitmap = getImpl(users, size);
374                }
375            } else {
376                bitmap = getImpl(CHANNEL_SYMBOL, c.getJid().asBareJid().toString(), size);
377            }
378        }
379
380        this.mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
381
382        return bitmap;
383    }
384
385    private Bitmap get(List<MucOptions.User> users, int size, boolean cachedOnly) {
386        final String KEY = key(users, size);
387        Bitmap bitmap = this.mXmppConnectionService.getBitmapCache().get(KEY);
388        if (bitmap != null || cachedOnly) {
389            return bitmap;
390        }
391        bitmap = getImpl(users, size);
392        this.mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
393        return bitmap;
394    }
395
396    private Bitmap getImpl(List<MucOptions.User> users, int size) {
397        int count = users.size();
398        Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
399        Canvas canvas = new Canvas(bitmap);
400        bitmap.eraseColor(TRANSPARENT);
401        if (count == 0) {
402            throw new AssertionError("Unable to draw tiles for 0 users");
403        } else if (count == 1) {
404            drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size);
405            drawTile(canvas, users.get(0).getAccount(), size / 2 + 1, 0, size, size);
406        } else if (count == 2) {
407            drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size);
408            drawTile(canvas, users.get(1), size / 2 + 1, 0, size, size);
409        } else if (count == 3) {
410            drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size);
411            drawTile(canvas, users.get(1), size / 2 + 1, 0, size, size / 2 - 1);
412            drawTile(canvas, users.get(2), size / 2 + 1, size / 2 + 1, size, size);
413        } else if (count == 4) {
414            drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size / 2 - 1);
415            drawTile(canvas, users.get(1), 0, size / 2 + 1, size / 2 - 1, size);
416            drawTile(canvas, users.get(2), size / 2 + 1, 0, size, size / 2 - 1);
417            drawTile(canvas, users.get(3), size / 2 + 1, size / 2 + 1, size, size);
418        } else {
419            drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size / 2 - 1);
420            drawTile(canvas, users.get(1), 0, size / 2 + 1, size / 2 - 1, size);
421            drawTile(canvas, users.get(2), size / 2 + 1, 0, size, size / 2 - 1);
422            drawTile(canvas, "\u2026", PLACEHOLDER_COLOR, size / 2 + 1, size / 2 + 1, size, size);
423        }
424        return bitmap;
425    }
426
427    public void clear(final MucOptions options) {
428        if (options == null) {
429            return;
430        }
431        synchronized (this.sizes) {
432            for (Integer size : sizes) {
433                this.mXmppConnectionService.getBitmapCache().remove(key(options, size));
434            }
435        }
436    }
437
438    private String key(final MucOptions options, int size) {
439        synchronized (this.sizes) {
440            this.sizes.add(size);
441        }
442        return PREFIX_CONVERSATION + "_" + options.getConversation().getUuid() + "_" + size;
443    }
444
445    private String key(final List<MucOptions.User> users, final int size) {
446        final Conversation conversation = users.get(0).getConversation();
447        StringBuilder builder = new StringBuilder("TILE_");
448        builder.append(conversation.getUuid());
449
450        for (final MucOptions.User user : users) {
451            builder.append("\0");
452            builder.append(emptyOnNull(user.getRealJid()));
453            builder.append("\0");
454            builder.append(emptyOnNull(user.getFullJid()));
455        }
456        builder.append('\0');
457        builder.append(size);
458        final String key = builder.toString();
459        synchronized (this.conversationDependentKeys) {
460            Set<String> keys;
461            if (this.conversationDependentKeys.containsKey(conversation.getUuid())) {
462                keys = this.conversationDependentKeys.get(conversation.getUuid());
463            } else {
464                keys = new HashSet<>();
465                this.conversationDependentKeys.put(conversation.getUuid(), keys);
466            }
467            keys.add(key);
468        }
469        return key;
470    }
471
472    public Bitmap get(Account account, int size) {
473        return get(account, size, false);
474    }
475
476    public Bitmap get(Account account, int size, boolean cachedOnly) {
477        final String KEY = key(account, size);
478        Bitmap avatar = mXmppConnectionService.getBitmapCache().get(KEY);
479        if (avatar != null || cachedOnly) {
480            return avatar;
481        }
482        avatar = mXmppConnectionService.getFileBackend().getAvatar(account.getAvatar(), size);
483        if (avatar == null) {
484            final String displayName = account.getDisplayName();
485            final String jid = account.getJid().asBareJid().toString();
486            if (QuickConversationsService.isQuicksy() && !TextUtils.isEmpty(displayName)) {
487                avatar = get(displayName, jid, size, false);
488            } else {
489                avatar = get(jid, null, size, false);
490            }
491        }
492        mXmppConnectionService.getBitmapCache().put(KEY, avatar);
493        return avatar;
494    }
495
496    public Bitmap get(Message message, int size, boolean cachedOnly) {
497        final Conversational conversation = message.getConversation();
498        if (message.getType() == Message.TYPE_STATUS
499                && message.getCounterparts() != null
500                && message.getCounterparts().size() > 1) {
501            return get(message.getCounterparts(), size, cachedOnly);
502        } else if (message.getStatus() == Message.STATUS_RECEIVED) {
503            Contact c = message.getContact();
504            if (c != null && (c.getProfilePhoto() != null || c.getAvatarFilename() != null)) {
505                return get(c, size, cachedOnly);
506            } else if (conversation instanceof Conversation
507                    && message.getConversation().getMode() == Conversation.MODE_MULTI) {
508                final Jid trueCounterpart = message.getTrueCounterpart();
509                final MucOptions mucOptions = ((Conversation) conversation).getMucOptions();
510                MucOptions.User user;
511                if (trueCounterpart != null) {
512                    user =
513                            mucOptions.findOrCreateUserByRealJid(
514                                    trueCounterpart, message.getCounterpart());
515                } else {
516                    user = mucOptions.findUserByFullJid(message.getCounterpart());
517                }
518                if (user != null) {
519                    return getImpl(user, size, cachedOnly);
520                }
521            } else if (c != null) {
522                return get(c, size, cachedOnly);
523            }
524            Jid tcp = message.getTrueCounterpart();
525            String seed = tcp != null ? tcp.asBareJid().toString() : null;
526            return get(UIHelper.getMessageDisplayName(message), seed, size, cachedOnly);
527        } else {
528            return get(conversation.getAccount(), size, cachedOnly);
529        }
530    }
531
532    public void clear(Account account) {
533        synchronized (this.sizes) {
534            for (Integer size : sizes) {
535                this.mXmppConnectionService.getBitmapCache().remove(key(account, size));
536            }
537        }
538    }
539
540    public void clear(final MucOptions.User user) {
541        synchronized (this.sizes) {
542            for (Integer size : sizes) {
543                this.mXmppConnectionService.getBitmapCache().remove(key(user, size));
544            }
545        }
546        synchronized (this.conversationDependentKeys) {
547            final Set<String> keys =
548                    this.conversationDependentKeys.get(user.getConversation().getUuid());
549            if (keys == null) {
550                return;
551            }
552            final var cache = this.mXmppConnectionService.getBitmapCache();
553            for (final String key : keys) {
554                cache.remove(key);
555            }
556            keys.clear();
557        }
558    }
559
560    private String key(Account account, int size) {
561        synchronized (this.sizes) {
562            this.sizes.add(size);
563        }
564        return PREFIX_ACCOUNT + "_" + account.getUuid() + "_" + size;
565    }
566
567    /*public Bitmap get(String name, int size) {
568    	return get(name,null, size,false);
569    }*/
570
571    public Bitmap get(final String name, String seed, final int size, boolean cachedOnly) {
572        final String KEY = key(seed == null ? name : name + "\0" + seed, size);
573        Bitmap bitmap = mXmppConnectionService.getBitmapCache().get(KEY);
574        if (bitmap != null || cachedOnly) {
575            return bitmap;
576        }
577        bitmap = getImpl(name, seed, size);
578        mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
579        return bitmap;
580    }
581
582    public static Bitmap get(final Jid jid, final int size) {
583        return getImpl(jid.asBareJid().toString(), null, size);
584    }
585
586    private static Bitmap getImpl(final String name, final String seed, final int size) {
587        Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
588        Canvas canvas = new Canvas(bitmap);
589        final String trimmedName = name == null ? "" : name.trim();
590        drawTile(canvas, trimmedName, seed, 0, 0, size, size);
591        return bitmap;
592    }
593
594    private String key(String name, int size) {
595        synchronized (this.sizes) {
596            this.sizes.add(size);
597        }
598        return PREFIX_GENERIC + "_" + name + "_" + size;
599    }
600
601    private static boolean drawTile(
602            Canvas canvas, String letter, int tileColor, int left, int top, int right, int bottom) {
603        letter = letter.toUpperCase(Locale.getDefault());
604        Paint tilePaint = new Paint(), textPaint = new Paint();
605        tilePaint.setColor(tileColor);
606        textPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
607        textPaint.setColor(FG_COLOR);
608        textPaint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));
609        textPaint.setTextSize((float) ((right - left) * 0.8));
610        Rect rect = new Rect();
611
612        canvas.drawRect(new Rect(left, top, right, bottom), tilePaint);
613        textPaint.getTextBounds(letter, 0, 1, rect);
614        float width = textPaint.measureText(letter);
615        canvas.drawText(
616                letter,
617                (right + left) / 2 - width / 2,
618                (top + bottom) / 2 + rect.height() / 2,
619                textPaint);
620        return true;
621    }
622
623    private boolean drawTile(
624            Canvas canvas, MucOptions.User user, int left, int top, int right, int bottom) {
625        Contact contact = user.getContact();
626        if (contact != null) {
627            Uri uri = null;
628            if (contact.getAvatarFilename() != null && QuickConversationsService.isQuicksy()) {
629                uri =
630                        mXmppConnectionService
631                                .getFileBackend()
632                                .getAvatarUri(contact.getAvatarFilename());
633            } else if (contact.getProfilePhoto() != null) {
634                uri = Uri.parse(contact.getProfilePhoto());
635            } else if (contact.getAvatarFilename() != null) {
636                uri =
637                        mXmppConnectionService
638                                .getFileBackend()
639                                .getAvatarUri(contact.getAvatarFilename());
640            }
641            if (drawTile(canvas, uri, left, top, right, bottom)) {
642                return true;
643            }
644        } else if (user.getAvatar() != null) {
645            Uri uri = mXmppConnectionService.getFileBackend().getAvatarUri(user.getAvatar());
646            if (drawTile(canvas, uri, left, top, right, bottom)) {
647                return true;
648            }
649        }
650        if (contact != null) {
651            String seed = contact.getJid().asBareJid().toString();
652            drawTile(canvas, contact.getDisplayName(), seed, left, top, right, bottom);
653        } else {
654            String seed =
655                    user.getRealJid() == null ? null : user.getRealJid().asBareJid().toString();
656            drawTile(canvas, user.getName(), seed, left, top, right, bottom);
657        }
658        return true;
659    }
660
661    private boolean drawTile(
662            Canvas canvas, Account account, int left, int top, int right, int bottom) {
663        String avatar = account.getAvatar();
664        if (avatar != null) {
665            Uri uri = mXmppConnectionService.getFileBackend().getAvatarUri(avatar);
666            if (uri != null) {
667                if (drawTile(canvas, uri, left, top, right, bottom)) {
668                    return true;
669                }
670            }
671        }
672        String name = account.getJid().asBareJid().toString();
673        return drawTile(canvas, name, name, left, top, right, bottom);
674    }
675
676    private static boolean drawTile(
677            Canvas canvas, String name, String seed, int left, int top, int right, int bottom) {
678        if (name != null) {
679            final String letter = name.equals(CHANNEL_SYMBOL) ? name : getFirstLetter(name);
680            final int color = UIHelper.getColorForName(seed == null ? name : seed);
681            drawTile(canvas, letter, color, left, top, right, bottom);
682            return true;
683        }
684        return false;
685    }
686
687    private static String getFirstLetter(String name) {
688        for (Character c : name.toCharArray()) {
689            if (Character.isLetterOrDigit(c)) {
690                return c.toString();
691            }
692        }
693        return "X";
694    }
695
696    private boolean drawTile(Canvas canvas, Uri uri, int left, int top, int right, int bottom) {
697        if (uri != null) {
698            Bitmap bitmap =
699                    mXmppConnectionService
700                            .getFileBackend()
701                            .cropCenter(uri, bottom - top, right - left);
702            if (bitmap != null) {
703                drawTile(canvas, bitmap, left, top, right, bottom);
704                return true;
705            }
706        }
707        return false;
708    }
709
710    private boolean drawTile(
711            Canvas canvas, Bitmap bm, int dstleft, int dsttop, int dstright, int dstbottom) {
712        Rect dst = new Rect(dstleft, dsttop, dstright, dstbottom);
713        canvas.drawBitmap(bm, null, dst, null);
714        return true;
715    }
716
717    @Override
718    public void onAdvancedStreamFeaturesAvailable(Account account) {
719        XmppConnection.Features features = account.getXmppConnection().getFeatures();
720        if (features.pep() && !features.pepPersistent()) {
721            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": has pep but is not persistent");
722            if (account.getAvatar() != null) {
723                mXmppConnectionService.republishAvatarIfNeeded(account);
724            }
725        }
726    }
727
728    private static String emptyOnNull(@Nullable Jid value) {
729        return value == null ? "" : value.toString();
730    }
731
732    public interface Avatarable {
733        @ColorInt
734        int getAvatarBackgroundColor();
735
736        String getAvatarName();
737    }
738}