AvatarService.java

  1package eu.siacs.conversations.services;
  2
  3import android.graphics.Bitmap;
  4import android.graphics.Canvas;
  5import android.graphics.Paint;
  6import android.graphics.PorterDuff;
  7import android.graphics.PorterDuffXfermode;
  8import android.graphics.Rect;
  9import android.graphics.Typeface;
 10import android.net.Uri;
 11import android.util.DisplayMetrics;
 12import android.util.Log;
 13import android.util.LruCache;
 14
 15import java.util.ArrayList;
 16import java.util.HashMap;
 17import java.util.HashSet;
 18import java.util.List;
 19import java.util.Locale;
 20import java.util.Set;
 21
 22import eu.siacs.conversations.Config;
 23import eu.siacs.conversations.entities.Account;
 24import eu.siacs.conversations.entities.Bookmark;
 25import eu.siacs.conversations.entities.Contact;
 26import eu.siacs.conversations.entities.Conversation;
 27import eu.siacs.conversations.entities.ListItem;
 28import eu.siacs.conversations.entities.Message;
 29import eu.siacs.conversations.entities.MucOptions;
 30import eu.siacs.conversations.utils.UIHelper;
 31import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
 32import eu.siacs.conversations.xmpp.XmppConnection;
 33import rocks.xmpp.addr.Jid;
 34
 35public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
 36
 37	private static final int FG_COLOR = 0xFFFAFAFA;
 38	private static final int TRANSPARENT = 0x00000000;
 39	private static final int PLACEHOLDER_COLOR = 0xFF202020;
 40
 41	private static final String PREFIX_CONTACT = "contact";
 42	private static final String PREFIX_CONVERSATION = "conversation";
 43	private static final String PREFIX_ACCOUNT = "account";
 44	private static final String PREFIX_GENERIC = "generic";
 45
 46	final private ArrayList<Integer> sizes = new ArrayList<>();
 47	final private HashMap<String,Set<String>> conversationDependentKeys = new HashMap<>();
 48
 49	protected XmppConnectionService mXmppConnectionService = null;
 50
 51	public AvatarService(XmppConnectionService service) {
 52		this.mXmppConnectionService = service;
 53	}
 54
 55	private Bitmap get(final Contact contact, final int size, boolean cachedOnly) {
 56		if (contact.isSelf()) {
 57			return get(contact.getAccount(),size,cachedOnly);
 58		}
 59		final String KEY = key(contact, size);
 60		Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY);
 61		if (avatar != null || cachedOnly) {
 62			return avatar;
 63		}
 64		if (contact.getProfilePhoto() != null) {
 65			avatar = mXmppConnectionService.getFileBackend().cropCenterSquare(Uri.parse(contact.getProfilePhoto()), size);
 66		}
 67		if (avatar == null && contact.getAvatar() != null) {
 68			avatar = mXmppConnectionService.getFileBackend().getAvatar(contact.getAvatar(), size);
 69		}
 70		if (avatar == null) {
 71            avatar = get(contact.getDisplayName(), contact.getJid().asBareJid().toString(), size, cachedOnly);
 72		}
 73		this.mXmppConnectionService.getBitmapCache().put(KEY, avatar);
 74		return avatar;
 75	}
 76
 77	public Bitmap getRoundedShortcut(final Contact contact) {
 78		DisplayMetrics metrics = mXmppConnectionService.getResources().getDisplayMetrics();
 79		int size = Math.round(metrics.density * 48);
 80		Bitmap bitmap = get(contact,size);
 81		Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
 82		Canvas canvas = new Canvas(output);
 83
 84		final Paint paint = new Paint();
 85		final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
 86
 87		paint.setAntiAlias(true);
 88		canvas.drawARGB(0, 0, 0, 0);
 89		canvas.drawCircle(bitmap.getWidth() / 2, bitmap.getHeight() / 2, bitmap.getWidth() / 2, paint);
 90		paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
 91		canvas.drawBitmap(bitmap, rect, rect, paint);
 92		return output;
 93	}
 94
 95	public Bitmap get(final MucOptions.User user, final int size, boolean cachedOnly) {
 96		Contact c = user.getContact();
 97		if (c != null && (c.getProfilePhoto() != null || c.getAvatar() != null || user.getAvatar() == null)) {
 98			return get(c, size, cachedOnly);
 99		} else {
100			return getImpl(user, size, cachedOnly);
101		}
102	}
103
104	private Bitmap getImpl(final MucOptions.User user, final int size, boolean cachedOnly) {
105		final String KEY = key(user, size);
106		Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY);
107		if (avatar != null || cachedOnly) {
108			return avatar;
109		}
110		if (user.getAvatar() != null) {
111			avatar = mXmppConnectionService.getFileBackend().getAvatar(user.getAvatar(), size);
112		}
113		if (avatar == null) {
114			Contact contact = user.getContact();
115			if (contact != null) {
116				avatar = get(contact, size, cachedOnly);
117			} else {
118				String seed = user.getRealJid() != null ? user.getRealJid().asBareJid().toString() : null;
119				avatar = get(user.getName(), seed, size, cachedOnly);
120			}
121		}
122		this.mXmppConnectionService.getBitmapCache().put(KEY, avatar);
123		return avatar;
124	}
125
126	public void clear(Contact contact) {
127		synchronized (this.sizes) {
128			for (Integer size : sizes) {
129				this.mXmppConnectionService.getBitmapCache().remove(
130						key(contact, size));
131			}
132		}
133		for(Conversation conversation : mXmppConnectionService.findAllConferencesWith(contact)) {
134			clear(conversation);
135		}
136	}
137
138	private String key(Contact contact, int size) {
139		synchronized (this.sizes) {
140			if (!this.sizes.contains(size)) {
141				this.sizes.add(size);
142			}
143		}
144		return PREFIX_CONTACT + "_" + contact.getAccount().getJid().asBareJid() + "_"
145				+ contact.getJid() + "_" + String.valueOf(size);
146	}
147
148	private String key(MucOptions.User user, int size) {
149		synchronized (this.sizes) {
150			if (!this.sizes.contains(size)) {
151				this.sizes.add(size);
152			}
153		}
154		return PREFIX_CONTACT + "_" + user.getAccount().getJid().asBareJid() + "_"
155				+ user.getFullJid() + "_" + String.valueOf(size);
156	}
157
158	public Bitmap get(ListItem item, int size) {
159		return get(item,size,false);
160	}
161
162	public Bitmap get(ListItem item, int size, boolean cachedOnly) {
163		if (item instanceof Contact) {
164			return get((Contact) item, size,cachedOnly);
165		} else if (item instanceof Bookmark) {
166			Bookmark bookmark = (Bookmark) item;
167			if (bookmark.getConversation() != null) {
168				return get(bookmark.getConversation(), size, cachedOnly);
169			} else {
170				String seed = bookmark.getJid() != null ? bookmark.getJid().asBareJid().toString() : null;
171				return get(bookmark.getDisplayName(), seed, size, cachedOnly);
172			}
173		} else {
174			String seed = item.getJid() != null ? item.getJid().asBareJid().toString() : null;
175			return get(item.getDisplayName(), seed, size, cachedOnly);
176		}
177	}
178
179	public Bitmap get(Conversation conversation, int size) {
180		return get(conversation,size,false);
181	}
182
183	public Bitmap get(Conversation conversation, int size, boolean cachedOnly) {
184		if (conversation.getMode() == Conversation.MODE_SINGLE) {
185			return get(conversation.getContact(), size, cachedOnly);
186		} else {
187			return get(conversation.getMucOptions(), size, cachedOnly);
188		}
189	}
190
191	public void clear(Conversation conversation) {
192		if (conversation.getMode() == Conversation.MODE_SINGLE) {
193			clear(conversation.getContact());
194		} else {
195			clear(conversation.getMucOptions());
196			synchronized (this.conversationDependentKeys) {
197				Set<String> keys = this.conversationDependentKeys.get(conversation.getUuid());
198				if (keys == null) {
199					return;
200				}
201				LruCache<String, Bitmap> cache = this.mXmppConnectionService.getBitmapCache();
202				for(String key : keys) {
203					cache.remove(key);
204				}
205				keys.clear();
206			}
207		}
208	}
209
210	private Bitmap get(MucOptions mucOptions, int size,  boolean cachedOnly) {
211		final String KEY = key(mucOptions, size);
212		Bitmap bitmap = this.mXmppConnectionService.getBitmapCache().get(KEY);
213		if (bitmap != null || cachedOnly) {
214			return bitmap;
215		}
216		final List<MucOptions.User> users = mucOptions.getUsersRelevantForNameAndAvatar();
217		if (users.size() == 0) {
218			Conversation c = mucOptions.getConversation();
219			bitmap = getImpl(c.getName().toString(),c.getJid().asBareJid().toString(),size);
220		} else {
221			bitmap = getImpl(users,size);
222		}
223		this.mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
224		return bitmap;
225	}
226
227	private Bitmap get(List<MucOptions.User> users, int size, boolean cachedOnly) {
228		final String KEY = key(users, size);
229		Bitmap bitmap = this.mXmppConnectionService.getBitmapCache().get(KEY);
230		if (bitmap != null || cachedOnly) {
231			return bitmap;
232		}
233		bitmap = getImpl(users, size);
234		this.mXmppConnectionService.getBitmapCache().put(KEY,bitmap);
235		return bitmap;
236	}
237
238	private Bitmap getImpl(List<MucOptions.User> users, int size) {
239		int count = users.size();
240		Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
241		Canvas canvas = new Canvas(bitmap);
242		bitmap.eraseColor(TRANSPARENT);
243		if (count == 0) {
244			throw new AssertionError("Unable to draw tiles for 0 users");
245		} else if (count == 1) {
246			drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size);
247			drawTile(canvas, users.get(0).getAccount(), size / 2 + 1, 0, size, size);
248		} else if (count == 2) {
249			drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size);
250			drawTile(canvas, users.get(1), size / 2 + 1, 0, size, size);
251		} else if (count == 3) {
252			drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size);
253			drawTile(canvas, users.get(1), size / 2 + 1, 0, size, size / 2 - 1);
254			drawTile(canvas, users.get(2), size / 2 + 1, size / 2 + 1, size,
255					size);
256		} else if (count == 4) {
257			drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size / 2 - 1);
258			drawTile(canvas, users.get(1), 0, size / 2 + 1, size / 2 - 1, size);
259			drawTile(canvas, users.get(2), size / 2 + 1, 0, size, size / 2 - 1);
260			drawTile(canvas, users.get(3), size / 2 + 1, size / 2 + 1, size,
261					size);
262		} else {
263			drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size / 2 - 1);
264			drawTile(canvas, users.get(1), 0, size / 2 + 1, size / 2 - 1, size);
265			drawTile(canvas, users.get(2), size / 2 + 1, 0, size, size / 2 - 1);
266			drawTile(canvas, "\u2026", PLACEHOLDER_COLOR, size / 2 + 1, size / 2 + 1,
267					size, size);
268		}
269		return bitmap;
270	}
271
272	public void clear(MucOptions options) {
273		synchronized (this.sizes) {
274			for (Integer size : sizes) {
275				this.mXmppConnectionService.getBitmapCache().remove(key(options, size));
276			}
277		}
278	}
279
280	private String key(MucOptions options, int size) {
281		synchronized (this.sizes) {
282			if (!this.sizes.contains(size)) {
283				this.sizes.add(size);
284			}
285		}
286		return PREFIX_CONVERSATION + "_" + options.getConversation().getUuid()
287				+ "_" + String.valueOf(size);
288	}
289
290	private String key(List<MucOptions.User> users, int size) {
291		final Conversation conversation = users.get(0).getConversation();
292		StringBuilder builder = new StringBuilder("TILE_");
293		builder.append(conversation.getUuid());
294
295		for(MucOptions.User user : users) {
296			builder.append("\0");
297			builder.append(user.getRealJid() == null ? "" : user.getRealJid().asBareJid().toString());
298			builder.append("\0");
299			builder.append(user.getFullJid() == null ? "" : user.getFullJid().toString());
300		}
301		final String key = builder.toString();
302		synchronized (this.conversationDependentKeys) {
303			Set<String> keys;
304			if (this.conversationDependentKeys.containsKey(conversation.getUuid())) {
305				keys = this.conversationDependentKeys.get(conversation.getUuid());
306			} else {
307				keys = new HashSet<>();
308				this.conversationDependentKeys.put(conversation.getUuid(),keys);
309			}
310			keys.add(key);
311		}
312		return key;
313	}
314
315	public Bitmap get(Account account, int size) {
316		return get(account, size, false);
317	}
318
319	public Bitmap get(Account account, int size, boolean cachedOnly) {
320		final String KEY = key(account, size);
321		Bitmap avatar = mXmppConnectionService.getBitmapCache().get(KEY);
322		if (avatar != null || cachedOnly) {
323			return avatar;
324		}
325		avatar = mXmppConnectionService.getFileBackend().getAvatar(account.getAvatar(), size);
326		if (avatar == null) {
327			avatar = get(account.getJid().asBareJid().toString(), null, size,false);
328		}
329		mXmppConnectionService.getBitmapCache().put(KEY, avatar);
330		return avatar;
331	}
332
333	public Bitmap get(Message message, int size, boolean cachedOnly) {
334		final Conversation conversation = message.getConversation();
335		if (message.getType() == Message.TYPE_STATUS && message.getCounterparts() != null && message.getCounterparts().size() > 1) {
336			return get(message.getCounterparts(),size,cachedOnly);
337		} else if (message.getStatus() == Message.STATUS_RECEIVED) {
338			Contact c = message.getContact();
339			if (c != null && (c.getProfilePhoto() != null || c.getAvatar() != null)) {
340				return get(c, size, cachedOnly);
341			} else if (message.getConversation().getMode() == Conversation.MODE_MULTI){
342				final Jid trueCounterpart = message.getTrueCounterpart();
343				MucOptions.User user;
344				if (trueCounterpart != null) {
345					user = conversation.getMucOptions().findUserByRealJid(trueCounterpart);
346				} else {
347					user = conversation.getMucOptions().findUserByFullJid(message.getCounterpart());
348				}
349				if (user != null) {
350					return getImpl(user,size,cachedOnly);
351				}
352			} else if (c != null) {
353				return get(c, size, cachedOnly);
354			}
355			Jid tcp = message.getTrueCounterpart();
356			String seed = tcp != null ? tcp.asBareJid().toString() :null;
357			return get(UIHelper.getMessageDisplayName(message), seed, size, cachedOnly);
358		} else  {
359			return get(conversation.getAccount(), size, cachedOnly);
360		}
361	}
362
363	public void clear(Account account) {
364		synchronized (this.sizes) {
365			for (Integer size : sizes) {
366				this.mXmppConnectionService.getBitmapCache().remove(key(account, size));
367			}
368		}
369	}
370
371	public void clear(MucOptions.User user) {
372		synchronized (this.sizes) {
373			for (Integer size : sizes) {
374				this.mXmppConnectionService.getBitmapCache().remove(key(user, size));
375			}
376		}
377	}
378
379	private String key(Account account, int size) {
380		synchronized (this.sizes) {
381			if (!this.sizes.contains(size)) {
382				this.sizes.add(size);
383			}
384		}
385		return PREFIX_ACCOUNT + "_" + account.getUuid() + "_"
386				+ String.valueOf(size);
387	}
388
389	/*public Bitmap get(String name, int size) {
390		return get(name,null, size,false);
391	}*/
392
393	public Bitmap get(final String name, String seed, final int size, boolean cachedOnly) {
394		final String KEY = key(seed == null ? name : seed, size);
395		Bitmap bitmap = mXmppConnectionService.getBitmapCache().get(KEY);
396		if (bitmap != null || cachedOnly) {
397			return bitmap;
398		}
399		bitmap = getImpl(name, seed, size);
400		mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
401		return bitmap;
402	}
403
404	private Bitmap getImpl(final String name, final String seed, final int size) {
405		Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
406		Canvas canvas = new Canvas(bitmap);
407		final String trimmedName = name == null ? "" : name.trim();
408		drawTile(canvas, trimmedName, seed, 0, 0, size, size);
409		return bitmap;
410	}
411
412	private String key(String name, int size) {
413		synchronized (this.sizes) {
414			if (!this.sizes.contains(size)) {
415				this.sizes.add(size);
416			}
417		}
418		return PREFIX_GENERIC + "_" + name + "_" + String.valueOf(size);
419	}
420
421	private boolean drawTile(Canvas canvas, String letter, int tileColor, int left, int top, int right, int bottom) {
422		letter = letter.toUpperCase(Locale.getDefault());
423		Paint tilePaint = new Paint(), textPaint = new Paint();
424		tilePaint.setColor(tileColor);
425		textPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
426		textPaint.setColor(FG_COLOR);
427		textPaint.setTypeface(Typeface.create("sans-serif-light",
428				Typeface.NORMAL));
429		textPaint.setTextSize((float) ((right - left) * 0.8));
430		Rect rect = new Rect();
431
432		canvas.drawRect(new Rect(left, top, right, bottom), tilePaint);
433		textPaint.getTextBounds(letter, 0, 1, rect);
434		float width = textPaint.measureText(letter);
435		canvas.drawText(letter, (right + left) / 2 - width / 2, (top + bottom)
436				/ 2 + rect.height() / 2, textPaint);
437		return true;
438	}
439
440	private boolean drawTile(Canvas canvas, MucOptions.User user, int left,
441						  int top, int right, int bottom) {
442		Contact contact = user.getContact();
443		if (contact != null) {
444			Uri uri = null;
445			if (contact.getProfilePhoto() != null) {
446				uri = Uri.parse(contact.getProfilePhoto());
447			} else if (contact.getAvatar() != null) {
448				uri = mXmppConnectionService.getFileBackend().getAvatarUri(
449						contact.getAvatar());
450			}
451			if (drawTile(canvas, uri, left, top, right, bottom)) {
452				return true;
453			}
454		} else if (user.getAvatar() != null) {
455			Uri uri = mXmppConnectionService.getFileBackend().getAvatarUri(user.getAvatar());
456			if (drawTile(canvas, uri, left, top, right, bottom)) {
457				return true;
458			}
459		}
460		if (contact != null) {
461			String seed = contact.getJid().asBareJid().toString();
462			drawTile(canvas, contact.getDisplayName(), seed, left, top, right, bottom);
463		} else {
464			String seed = user.getRealJid() == null ? null : user.getRealJid().asBareJid().toString();
465			drawTile(canvas, user.getName(), seed, left, top, right, bottom);
466		}
467		return true;
468	}
469
470	private boolean drawTile(Canvas canvas, Account account, int left, int top, int right, int bottom) {
471		String avatar = account.getAvatar();
472		if (avatar != null) {
473			Uri uri = mXmppConnectionService.getFileBackend().getAvatarUri(avatar);
474			if (uri != null) {
475				if (drawTile(canvas, uri, left, top, right, bottom)) {
476					return true;
477				}
478			}
479		}
480		String name = account.getJid().asBareJid().toString();
481		return drawTile(canvas, name, name, left, top, right, bottom);
482	}
483
484	private boolean drawTile(Canvas canvas, String name, String seed, int left, int top, int right, int bottom) {
485		if (name != null) {
486			final String letter = getFirstLetter(name);
487			final int color = UIHelper.getColorForName(seed == null ? name : seed);
488			drawTile(canvas, letter, color, left, top, right, bottom);
489			return true;
490		}
491		return false;
492	}
493
494	private static String getFirstLetter(String name) {
495		for(Character c : name.toCharArray()) {
496			if (Character.isLetterOrDigit(c)) {
497				return c.toString();
498			}
499		}
500		return "X";
501	}
502
503	private boolean drawTile(Canvas canvas, Uri uri, int left, int top, int right, int bottom) {
504		if (uri != null) {
505			Bitmap bitmap = mXmppConnectionService.getFileBackend()
506					.cropCenter(uri, bottom - top, right - left);
507			if (bitmap != null) {
508				drawTile(canvas, bitmap, left, top, right, bottom);
509				return true;
510			}
511		}
512		return false;
513	}
514
515	private boolean drawTile(Canvas canvas, Bitmap bm, int dstleft, int dsttop, int dstright, int dstbottom) {
516		Rect dst = new Rect(dstleft, dsttop, dstright, dstbottom);
517		canvas.drawBitmap(bm, null, dst, null);
518		return true;
519	}
520
521	@Override
522	public void onAdvancedStreamFeaturesAvailable(Account account) {
523		XmppConnection.Features features = account.getXmppConnection().getFeatures();
524		if (features.pep() && !features.pepPersistent()) {
525			Log.d(Config.LOGTAG,account.getJid().asBareJid()+": has pep but is not persistent");
526			if (account.getAvatar() != null) {
527				mXmppConnectionService.republishAvatarIfNeeded(account);
528			}
529		}
530	}
531}