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