Display a generated avatar icon rather than a single default.

If the contact doesn't have an image, render a color-coded
background and the first letter of the contact's name.

1) Don't display anything during recipient resolution.

2) Display a # icon in material gray for recipients with no name.

3) Display a material group icon in material gray for groups with
   no avatar icon set.

Closes #3104

// FREEBIE
This commit is contained in:
Moxie Marlinspike
2015-05-04 11:36:18 -07:00
parent 356d9949b7
commit 41cad291f9
24 changed files with 342 additions and 422 deletions

View File

@@ -1,15 +1,31 @@
package org.thoughtcrime.securesms.contacts;
import android.content.Context;
import android.database.Cursor;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Contacts;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.Gravity;
import android.widget.ImageView;
import com.amulyakhare.textdrawable.TextDrawable;
import com.amulyakhare.textdrawable.util.ColorGenerator;
import com.makeramen.RoundedDrawable;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -24,62 +40,56 @@ import java.util.Map;
public class ContactPhotoFactory {
private static final String TAG = ContactPhotoFactory.class.getSimpleName();
private static final ColorGenerator COLOR_GENERATOR = ColorGenerator.MATERIAL;
private static final int UNKNOWN_COLOR = 0xff9E9E9E;
private static final Object defaultPhotoLock = new Object();
private static final Object defaultGroupPhotoLock = new Object();
private static final Object loadingPhotoLock = new Object();
private static Bitmap defaultContactPhoto;
private static Bitmap defaultGroupContactPhoto;
private static Drawable defaultContactPhoto;
private static Drawable defaultGroupContactPhoto;
private static Drawable loadingPhoto;
private static final Map<Uri,Bitmap> localUserContactPhotoCache =
Collections.synchronizedMap(new LRUCache<Uri,Bitmap>(2));
private static final String[] CONTENT_URI_PROJECTION = new String[] {
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Contacts.LOOKUP_KEY
};
public static Drawable getLoadingPhoto(Context context) {
synchronized (loadingPhotoLock) {
if (loadingPhoto == null)
loadingPhoto = RoundedDrawable.fromDrawable(context.getResources().getDrawable(android.R.color.transparent));
return loadingPhoto;
}
}
public static Drawable getDefaultContactPhoto(@Nullable String name) {
if (name != null && !name.isEmpty()) {
return TextDrawable.builder().buildRound(String.valueOf(name.charAt(0)),
COLOR_GENERATOR.getColor(name));
}
public static Bitmap getDefaultContactPhoto(Context context) {
synchronized (defaultPhotoLock) {
if (defaultContactPhoto == null)
defaultContactPhoto = BitmapFactory.decodeResource(context.getResources(),
R.drawable.ic_contact_picture);
defaultContactPhoto = TextDrawable.builder().buildRound("#", UNKNOWN_COLOR);
return defaultContactPhoto;
}
}
public static Bitmap getDefaultGroupPhoto(Context context) {
public static Drawable getDefaultGroupPhoto(Context context) {
synchronized (defaultGroupPhotoLock) {
if (defaultGroupContactPhoto == null)
defaultGroupContactPhoto = BitmapFactory.decodeResource(context.getResources(),
R.drawable.ic_group_photo);
return defaultGroupContactPhoto;
}
}
if (defaultGroupContactPhoto == null) {
Drawable background = TextDrawable.builder().buildRound(" ", UNKNOWN_COLOR);
RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(context.getResources().getDrawable(R.drawable.ic_group_white_24dp));
foreground.setScaleType(ImageView.ScaleType.CENTER);
public static Bitmap getLocalUserContactPhoto(Context context, Uri uri) {
if (uri == null) return getDefaultContactPhoto(context);
Bitmap contactPhoto = localUserContactPhotoCache.get(uri);
if (contactPhoto == null) {
Cursor cursor = context.getContentResolver().query(uri, CONTENT_URI_PROJECTION,
null, null, null);
try {
if (cursor != null && cursor.moveToFirst()) {
contactPhoto = getContactPhoto(context, Uri.withAppendedPath(Contacts.CONTENT_URI,
cursor.getLong(0) + ""));
} else {
contactPhoto = getDefaultContactPhoto(context);
}
} finally {
if (cursor != null) cursor.close();
defaultGroupContactPhoto = new ExpandingLayerDrawable(new Drawable[] {background, foreground});
}
localUserContactPhotoCache.put(uri, contactPhoto);
return defaultGroupContactPhoto;
}
return contactPhoto;
}
public static void clearCache() {
@@ -91,25 +101,32 @@ public class ContactPhotoFactory {
localUserContactPhotoCache.remove(recipient.getContactUri());
}
public static Bitmap getContactPhoto(Context context, Uri uri) {
public static Drawable getContactPhoto(Context context, Uri uri, String name) {
final InputStream inputStream = getContactPhotoStream(context, uri);
final int targetSize = context.getResources().getDimensionPixelSize(R.dimen.contact_photo_target_size);
Bitmap contactPhoto = null;
if (inputStream != null) {
try {
contactPhoto = BitmapUtil.createScaledBitmap(inputStream,
getContactPhotoStream(context, uri),
targetSize,
targetSize);
return RoundedDrawable.fromBitmap(BitmapUtil.createScaledBitmap(inputStream,
getContactPhotoStream(context, uri),
targetSize,
targetSize))
.setScaleType(ImageView.ScaleType.CENTER_CROP)
.setOval(true);
} catch (BitmapDecodingException bde) {
Log.w(TAG, bde);
}
}
if (contactPhoto == null) {
contactPhoto = ContactPhotoFactory.getDefaultContactPhoto(context);
}
return contactPhoto;
return getDefaultContactPhoto(name);
}
public static Drawable getGroupContactPhoto(Context context, @Nullable byte[] avatar) {
if (avatar == null) return getDefaultGroupPhoto(context);
return RoundedDrawable.fromBitmap(BitmapFactory.decodeByteArray(avatar, 0, avatar.length))
.setScaleType(ImageView.ScaleType.CENTER_CROP)
.setOval(true);
}
private static InputStream getContactPhotoStream(Context context, Uri uri) {
@@ -119,4 +136,21 @@ public class ContactPhotoFactory {
return ContactsContract.Contacts.openContactPhotoInputStream(context.getContentResolver(), uri);
}
}
private static class ExpandingLayerDrawable extends LayerDrawable {
public ExpandingLayerDrawable(Drawable[] layers) {
super(layers);
}
@Override
public int getIntrinsicWidth() {
return -1;
}
@Override
public int getIntrinsicHeight() {
return -1;
}
}
}

View File

@@ -19,7 +19,6 @@ package org.thoughtcrime.securesms.contacts;
import android.content.Context;
import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.provider.ContactsContract;
import android.support.v4.widget.CursorAdapter;
import android.text.Spannable;
@@ -73,7 +72,6 @@ public class ContactSelectionListAdapter extends CursorAdapter
private final boolean multiSelect;
private final LayoutInflater li;
private final TypedArray drawables;
private final Bitmap defaultPhoto;
private final int scaledPhotoSize;
private final HashMap<Long, ContactAccessor.ContactData> selectedContacts = new HashMap<>();
@@ -84,7 +82,6 @@ public class ContactSelectionListAdapter extends CursorAdapter
this.li = LayoutInflater.from(context);
this.drawables = context.obtainStyledAttributes(STYLE_ATTRIBUTES);
this.multiSelect = multiSelect;
this.defaultPhoto = ContactPhotoFactory.getDefaultContactPhoto(context);
this.scaledPhotoSize = context.getResources().getDimensionPixelSize(R.dimen.contact_selection_photo_size);
}
@@ -180,7 +177,7 @@ public class ContactSelectionListAdapter extends CursorAdapter
numberLabelSpan.setSpan(new ForegroundColorSpan(drawables.getColor(2, 0xff444444)), contactData.number.length(), numberWithLabel.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
holder.number.setText(numberLabelSpan);
}
holder.contactPhoto.setImageBitmap(defaultPhoto);
holder.contactPhoto.setImageDrawable(ContactPhotoFactory.getLoadingPhoto(context));
if (contactData.id > -1) loadBitmap(contactData.number, holder.contactPhoto);
}
@@ -229,11 +226,14 @@ public class ContactSelectionListAdapter extends CursorAdapter
return true;
}
// FIXME -- It should be unnecessary to duplicate the existing asynchronous resolution
// infrastructure we've built for Recipient objects here.
public void loadBitmap(String number, ImageView imageView) {
if (cancelPotentialWork(number, imageView)) {
final BitmapWorkerRunnable runnable = new BitmapWorkerRunnable(context, imageView, defaultPhoto, number, scaledPhotoSize);
final BitmapWorkerRunnable runnable = new BitmapWorkerRunnable(context, imageView, number, scaledPhotoSize);
final TaggedFutureTask<?> task = new TaggedFutureTask<Void>(runnable, null, number);
final AsyncDrawable asyncDrawable = new AsyncDrawable(defaultPhoto, task);
final AsyncDrawable asyncDrawable = new AsyncDrawable(task);
imageView.setImageDrawable(asyncDrawable);
if (!task.isCancelled()) photoResolver.execute(new FutureTask<Void>(task, null));