Various refactorings to prepare proper big picture loading
- Added a function to load a picture with ideal sampleSize
- Renamed xmls/java files for tiles
- ContactTileView is now abstract and has subclasses for each use-case
- Added mechanism to estimate the image-size in a tile before layout
- Changed the cross-fade in ContactPhotoManager to apply the fade to the
current picture instead of the default avatar. Reduces flickering
Bug:6202229
Change-Id: Ic8636d1b3349473163fb2424b5f74476fd435fb5
diff --git a/res/layout/contact_tile_frequent.xml b/res/layout/contact_tile_frequent.xml
index 992c643..9219f56 100644
--- a/res/layout/contact_tile_frequent.xml
+++ b/res/layout/contact_tile_frequent.xml
@@ -15,7 +15,7 @@
-->
<view
xmlns:android="http://schemas.android.com/apk/res/android"
- class="com.android.contacts.list.ContactTileView"
+ class="com.android.contacts.list.ContactTileFrequentView"
android:focusable="true"
android:background="?android:attr/selectableItemBackground"
android:nextFocusRight="@+id/contact_tile_quick">
diff --git a/res/layout/contact_tile_starred_secondary_target.xml b/res/layout/contact_tile_phone_starred.xml
similarity index 97%
rename from res/layout/contact_tile_starred_secondary_target.xml
rename to res/layout/contact_tile_phone_starred.xml
index ea15b86..053ffa6 100644
--- a/res/layout/contact_tile_starred_secondary_target.xml
+++ b/res/layout/contact_tile_phone_starred.xml
@@ -18,7 +18,7 @@
android:background="@null"
android:paddingBottom="1dip"
android:paddingRight="1dip"
- class="com.android.contacts.list.ContactTileSecondaryTargetView" >
+ class="com.android.contacts.list.ContactTilePhoneStarredView" >
<RelativeLayout
android:layout_width="match_parent"
diff --git a/src/com/android/contacts/CallDetailActivity.java b/src/com/android/contacts/CallDetailActivity.java
index 8e58cd9..12a1592 100644
--- a/src/com/android/contacts/CallDetailActivity.java
+++ b/src/com/android/contacts/CallDetailActivity.java
@@ -607,7 +607,8 @@
/** Load the contact photos and places them in the corresponding views. */
private void loadContactPhotos(Uri photoUri) {
- mContactPhotoManager.loadPhoto(mContactBackgroundView, photoUri, true, true);
+ mContactPhotoManager.loadPhoto(mContactBackgroundView, photoUri,
+ mContactBackgroundView.getWidth(), true);
}
static final class ViewEntry {
diff --git a/src/com/android/contacts/ContactPhotoManager.java b/src/com/android/contacts/ContactPhotoManager.java
index 35585a6..8688426 100644
--- a/src/com/android/contacts/ContactPhotoManager.java
+++ b/src/com/android/contacts/ContactPhotoManager.java
@@ -17,6 +17,7 @@
package com.android.contacts;
import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.util.BitmapUtil;
import com.android.contacts.util.MemoryUtils;
import com.android.contacts.util.UriUtils;
import com.google.android.collect.Lists;
@@ -27,9 +28,13 @@
import android.content.ContentUris;
import android.content.Context;
import android.content.res.Configuration;
+import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
@@ -47,6 +52,7 @@
import android.text.TextUtils;
import android.util.Log;
import android.util.LruCache;
+import android.util.TypedValue;
import android.widget.ImageView;
import java.io.ByteArrayOutputStream;
@@ -65,9 +71,31 @@
public abstract class ContactPhotoManager implements ComponentCallbacks2 {
static final String TAG = "ContactPhotoManager";
static final boolean DEBUG = false; // Don't submit with true
+ static final boolean DEBUG_SIZES = false; // Don't submit with true
+
+ /** Caches 180dip in pixel. This is used to detect whether to show the hires or lores version
+ * of the default avatar */
+ private static int s180DipInPixel = -1;
public static final String CONTACT_PHOTO_SERVICE = "contactPhotos";
+ /**
+ * Returns the resource id of the default avatar. Tries to find a resource that is bigger
+ * than the given extent (width or height). If extent=-1, a thumbnail avatar is returned
+ */
+ public static int getDefaultAvatarResId(Context context, int extent, boolean darkTheme) {
+ // TODO: Is it worth finding a nicer way to do hires/lores here? In practice, the
+ // default avatar doesn't look too different when stretched
+ if (s180DipInPixel == -1) {
+ Resources r = context.getResources();
+ s180DipInPixel = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 180,
+ r.getDisplayMetrics());
+ }
+
+ final boolean hires = (extent != -1) && (extent > s180DipInPixel);
+ return getDefaultAvatarResId(hires, darkTheme);
+ }
+
public static int getDefaultAvatarResId(boolean hires, boolean darkTheme) {
if (hires && darkTheme) return R.drawable.ic_contact_picture_180_holo_dark;
if (hires) return R.drawable.ic_contact_picture_180_holo_light;
@@ -76,13 +104,17 @@
}
public static abstract class DefaultImageProvider {
- public abstract void applyDefaultImage(ImageView view, boolean hires, boolean darkTheme);
+ /**
+ * Applies the default avatar to the ImageView. Extent is an indicator for the size (width
+ * or height). If darkTheme is set, the avatar is one that looks better on dark background
+ */
+ public abstract void applyDefaultImage(ImageView view, int extent, boolean darkTheme);
}
private static class AvatarDefaultImageProvider extends DefaultImageProvider {
@Override
- public void applyDefaultImage(ImageView view, boolean hires, boolean darkTheme) {
- view.setImageResource(getDefaultAvatarResId(hires, darkTheme));
+ public void applyDefaultImage(ImageView view, int extent, boolean darkTheme) {
+ view.setImageResource(getDefaultAvatarResId(view.getContext(), extent, darkTheme));
}
}
@@ -90,7 +122,7 @@
private static Drawable sDrawable;
@Override
- public void applyDefaultImage(ImageView view, boolean hires, boolean darkTheme) {
+ public void applyDefaultImage(ImageView view, int extent, boolean darkTheme) {
if (sDrawable == null) {
Context context = view.getContext();
sDrawable = new ColorDrawable(context.getResources().getColor(
@@ -124,35 +156,53 @@
}
/**
- * Load photo into the supplied image view. If the photo is already cached,
+ * Load thumbnail image into the supplied image view. If the photo is already cached,
* it is displayed immediately. Otherwise a request is sent to load the photo
* from the database.
*/
- public abstract void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme,
+ public abstract void loadThumbnail(ImageView view, long photoId, boolean darkTheme,
DefaultImageProvider defaultProvider);
/**
- * Calls {@link #loadPhoto(ImageView, long, boolean, boolean, DefaultImageProvider)} with
+ * Calls {@link #loadThumbnail(ImageView, long, boolean, DefaultImageProvider)} with
* {@link #DEFAULT_AVATAR}.
*/
- public final void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme) {
- loadPhoto(view, photoId, hires, darkTheme, DEFAULT_AVATAR);
+ public final void loadThumbnail(ImageView view, long photoId, boolean darkTheme) {
+ loadThumbnail(view, photoId, darkTheme, DEFAULT_AVATAR);
}
/**
- * Load photo into the supplied image view. If the photo is already cached,
- * it is displayed immediately. Otherwise a request is sent to load the photo
+ * Load photo into the supplied image view. If the photo is already cached,
+ * it is displayed immediately. Otherwise a request is sent to load the photo
* from the location specified by the URI.
+ * @param view The target view
+ * @param photoUri The uri of the photo to load
+ * @param requestedExtent Specifies an approximate Max(width, height) of the targetView.
+ * This is useful if the source image can be a lot bigger that the target, so that the decoding
+ * is done using efficient sampling. If requestedExtent is specified, no sampling of the image
+ * is performed
+ * @param darkTheme Whether the background is dark. This is used for default avatars
+ * @param defaultProvider The provider of default avatars (this is used if photoUri doesn't
+ * refer to an existing image)
*/
- public abstract void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme,
- DefaultImageProvider defaultProvider);
+ public abstract void loadPhoto(ImageView view, Uri photoUri, int requestedExtent,
+ boolean darkTheme, DefaultImageProvider defaultProvider);
/**
* Calls {@link #loadPhoto(ImageView, Uri, boolean, boolean, DefaultImageProvider)} with
* {@link #DEFAULT_AVATAR}.
*/
- public final void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme) {
- loadPhoto(view, photoUri, hires, darkTheme, DEFAULT_AVATAR);
+ public final void loadPhoto(ImageView view, Uri photoUri, int requestedExtent,
+ boolean darkTheme) {
+ loadPhoto(view, photoUri, requestedExtent, darkTheme, DEFAULT_AVATAR);
+ }
+
+ /**
+ * Calls {@link #loadPhoto(ImageView, Uri, boolean, boolean, DefaultImageProvider)} with
+ * {@link #DEFAULT_AVATAR} and with the assumption, that the image is a thumbnail
+ */
+ public final void loadDirectoryPhoto(ImageView view, Uri photoUri, boolean darkTheme) {
+ loadPhoto(view, photoUri, -1, darkTheme, DEFAULT_AVATAR);
}
/**
@@ -234,14 +284,17 @@
*/
private static class BitmapHolder {
final byte[] bytes;
+ final int originalSmallerExtent;
volatile boolean fresh;
Bitmap bitmap;
Reference<Bitmap> bitmapRef;
+ int decodedSampleSize;
- public BitmapHolder(byte[] bytes) {
+ public BitmapHolder(byte[] bytes, int originalSmallerExtent) {
this.bytes = bytes;
this.fresh = true;
+ this.originalSmallerExtent = originalSmallerExtent;
}
}
@@ -417,29 +470,29 @@
}
@Override
- public void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme,
+ public void loadThumbnail(ImageView view, long photoId, boolean darkTheme,
DefaultImageProvider defaultProvider) {
if (photoId == 0) {
// No photo is needed
- defaultProvider.applyDefaultImage(view, hires, darkTheme);
+ defaultProvider.applyDefaultImage(view, -1, darkTheme);
mPendingRequests.remove(view);
} else {
if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoId);
- loadPhotoByIdOrUri(view, Request.createFromId(photoId, hires, darkTheme,
+ loadPhotoByIdOrUri(view, Request.createFromThumbnailId(photoId, darkTheme,
defaultProvider));
}
}
@Override
- public void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme,
+ public void loadPhoto(ImageView view, Uri photoUri, int requestedExtent, boolean darkTheme,
DefaultImageProvider defaultProvider) {
if (photoUri == null) {
// No photo is needed
- defaultProvider.applyDefaultImage(view, hires, darkTheme);
+ defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme);
mPendingRequests.remove(view);
} else {
if (DEBUG) Log.d(TAG, "loadPhoto request: " + photoUri);
- loadPhotoByIdOrUri(view, Request.createFromUri(photoUri, hires, darkTheme,
+ loadPhotoByIdOrUri(view, Request.createFromUri(photoUri, requestedExtent, darkTheme,
defaultProvider));
}
}
@@ -482,12 +535,6 @@
* @return false if the photo needs to be (re)loaded from the provider.
*/
private boolean loadCachedPhoto(ImageView view, Request request, boolean fadeIn) {
- Bitmap bitmap = mBitmapCache.get(request.getKey());
- if (bitmap != null) {
- view.setImageBitmap(bitmap);
- return true;
- }
-
BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
if (holder == null) {
// The bitmap has not been loaded - should display the placeholder image.
@@ -501,12 +548,25 @@
}
// Optionally decode bytes into a bitmap
- inflateBitmap(holder);
+ final long inflateStart = System.currentTimeMillis();
+ final boolean decodingPicture = holder.bitmap == null;
+ inflateBitmap(holder, request.getRequestedExtent());
+ if (decodingPicture && DEBUG) {
+ Log.d(TAG, "Inflated a picture on the foreground thread. Took " +
+ (System.currentTimeMillis() - inflateStart) + "ms");
+ }
- if (fadeIn) {
- Drawable[] layers = new Drawable[2];
- layers[0] = mContext.getResources().getDrawable(
- getDefaultAvatarResId(request.mHires, request.mDarkTheme));
+ final Drawable previousDrawable = view.getDrawable();
+ if (fadeIn && previousDrawable != null) {
+ final Drawable[] layers = new Drawable[2];
+ // Prevent cascade of TransitionDrawables.
+ if (previousDrawable instanceof TransitionDrawable) {
+ final TransitionDrawable transitionDrawable = (TransitionDrawable) previousDrawable;
+ layers[0] =
+ transitionDrawable.getDrawable(transitionDrawable.getNumberOfLayers() - 1);
+ } else {
+ layers[0] = previousDrawable;
+ }
layers[1] = new BitmapDrawable(mContext.getResources(), holder.bitmap);
TransitionDrawable drawable = new TransitionDrawable(layers);
view.setImageDrawable(drawable);
@@ -531,23 +591,45 @@
* bitmap is held either by {@link #mBitmapCache} or by a soft reference in
* the holder, it will not be necessary to decode the bitmap.
*/
- private static void inflateBitmap(BitmapHolder holder) {
+ private static void inflateBitmap(BitmapHolder holder, int requestedExtent) {
+ final int sampleSize =
+ BitmapUtil.findOptimalSampleSize(holder.originalSmallerExtent, requestedExtent);
byte[] bytes = holder.bytes;
if (bytes == null || bytes.length == 0) {
return;
}
- // Check the soft reference. If will be retained if the bitmap is also
- // in the LRU cache, so we don't need to check the LRU cache explicitly.
- if (holder.bitmapRef != null) {
- holder.bitmap = holder.bitmapRef.get();
- if (holder.bitmap != null) {
- return;
+ if (sampleSize == holder.decodedSampleSize) {
+ // Check the soft reference. If will be retained if the bitmap is also
+ // in the LRU cache, so we don't need to check the LRU cache explicitly.
+ if (holder.bitmapRef != null) {
+ holder.bitmap = holder.bitmapRef.get();
+ if (holder.bitmap != null) {
+ return;
+ }
}
}
try {
- Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length, null);
+ Bitmap bitmap = BitmapUtil.decodeBitmapFromBytes(bytes, sampleSize);
+
+ // make bitmap mutable and draw size onto it
+ if (DEBUG_SIZES) {
+ Bitmap original = bitmap;
+ bitmap = bitmap.copy(bitmap.getConfig(), true);
+ original.recycle();
+ Canvas canvas = new Canvas(bitmap);
+ Paint paint = new Paint();
+ paint.setTextSize(16);
+ paint.setColor(Color.BLUE);
+ paint.setStyle(Style.FILL);
+ canvas.drawRect(0.0f, 0.0f, 50.0f, 20.0f, paint);
+ paint.setColor(Color.WHITE);
+ paint.setAntiAlias(true);
+ canvas.drawText(bitmap.getWidth() + "/" + sampleSize, 0, 15, paint);
+ }
+
+ holder.decodedSampleSize = sampleSize;
holder.bitmap = bitmap;
holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
if (DEBUG) {
@@ -662,7 +744,7 @@
/**
* Stores the supplied bitmap in cache.
*/
- private void cacheBitmap(Object key, byte[] bytes, boolean preloading) {
+ private void cacheBitmap(Object key, byte[] bytes, boolean preloading, int requestedExtent) {
if (DEBUG) {
BitmapHolder prev = mBitmapHolderCache.get(key);
if (prev != null && prev.bytes != null) {
@@ -673,14 +755,16 @@
mStaleCacheOverwrite.incrementAndGet();
}
}
- Log.d(TAG, "Caching data: key=" + key + ", " + btk(bytes.length));
+ Log.d(TAG, "Caching data: key=" + key + ", " +
+ (bytes == null ? "<null>" : btk(bytes.length)));
}
- BitmapHolder holder = new BitmapHolder(bytes);
+ BitmapHolder holder = new BitmapHolder(bytes,
+ bytes == null ? -1 : BitmapUtil.getSmallerExtentFromBytes(bytes));
// Unless this image is being preloaded, decode it right away while
// we are still on the background thread.
if (!preloading) {
- inflateBitmap(holder);
+ inflateBitmap(holder, requestedExtent);
}
mBitmapHolderCache.put(key, holder);
@@ -689,8 +773,11 @@
@Override
public void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes) {
- Request request = Request.createFromUri(photoUri, true, false, DEFAULT_AVATAR);
- BitmapHolder holder = new BitmapHolder(photoBytes);
+ final int smallerExtent = Math.min(bitmap.getWidth(), bitmap.getHeight());
+ // We can pretend here that the extent of the photo was the size that we originally
+ // requested
+ Request request = Request.createFromUri(photoUri, smallerExtent, false, DEFAULT_AVATAR);
+ BitmapHolder holder = new BitmapHolder(photoBytes, smallerExtent);
mBitmapHolderCache.put(request.getKey(), holder);
mBitmapHolderCacheAllUnfresh = false;
mBitmapCache.put(request.getKey(), bitmap);
@@ -700,7 +787,7 @@
* Populates an array of photo IDs that need to be loaded.
*/
private void obtainPhotoIdsAndUrisToLoad(Set<Long> photoIds,
- Set<String> photoIdsAsStrings, Set<Uri> uris) {
+ Set<String> photoIdsAsStrings, Set<Request> uris) {
photoIds.clear();
photoIdsAsStrings.clear();
uris.clear();
@@ -719,9 +806,9 @@
BitmapHolder holder = mBitmapHolderCache.get(request);
if (holder == null || !holder.fresh) {
if (request.isUriRequest()) {
- uris.add(request.mUri);
+ uris.add(request);
} else {
- photoIds.add(request.mId);
+ photoIds.add(request.getId());
photoIdsAsStrings.add(String.valueOf(request.mId));
}
}
@@ -756,7 +843,7 @@
private final StringBuilder mStringBuilder = new StringBuilder();
private final Set<Long> mPhotoIds = Sets.newHashSet();
private final Set<String> mPhotoIdsAsStrings = Sets.newHashSet();
- private final Set<Uri> mPhotoUris = Sets.newHashSet();
+ private final Set<Request> mPhotoUris = Sets.newHashSet();
private final List<Long> mPreloadPhotoIds = Lists.newArrayList();
private Handler mLoaderThreadHandler;
@@ -869,7 +956,7 @@
mPreloadPhotoIds.remove(preloadSize);
}
- loadPhotosFromDatabase(true);
+ loadThumbnails(true);
if (preloadSize == 0) {
mPreloadStatus = PRELOAD_STATUS_DONE;
@@ -910,12 +997,13 @@
private void loadPhotosInBackground() {
obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
- loadPhotosFromDatabase(false);
- loadRemotePhotos();
+ loadThumbnails(false);
+ loadUriBasedPhotos();
requestPreloading();
}
- private void loadPhotosFromDatabase(boolean preloading) {
+ /** Loads thumbnail photos with ids */
+ private void loadThumbnails(boolean preloading) {
if (mPhotoIds.isEmpty()) {
return;
}
@@ -954,7 +1042,7 @@
while (cursor.moveToNext()) {
Long id = cursor.getLong(0);
byte[] bytes = cursor.getBlob(1);
- cacheBitmap(id, bytes, preloading);
+ cacheBitmap(id, bytes, preloading, -1);
mPhotoIds.remove(id);
}
}
@@ -974,10 +1062,10 @@
COLUMNS, null, null, null);
if (profileCursor != null && profileCursor.moveToFirst()) {
cacheBitmap(profileCursor.getLong(0), profileCursor.getBlob(1),
- preloading);
+ preloading, -1);
} else {
// Couldn't load a photo this way either.
- cacheBitmap(id, null, preloading);
+ cacheBitmap(id, null, preloading, -1);
}
} finally {
if (profileCursor != null) {
@@ -986,15 +1074,20 @@
}
} else {
// Not a profile photo and not found - mark the cache accordingly
- cacheBitmap(id, null, preloading);
+ cacheBitmap(id, null, preloading, -1);
}
}
mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
}
- private void loadRemotePhotos() {
- for (Uri uri : mPhotoUris) {
+ /**
+ * Loads photos referenced with Uris. Those can be remote thumbnails
+ * (from directory searches), display photos etc
+ */
+ private void loadUriBasedPhotos() {
+ for (Request uriRequest : mPhotoUris) {
+ Uri uri = uriRequest.getUri();
if (mBuffer == null) {
mBuffer = new byte[BUFFER_SIZE];
}
@@ -1011,15 +1104,16 @@
} finally {
is.close();
}
- cacheBitmap(uri, baos.toByteArray(), false);
+ cacheBitmap(uri, baos.toByteArray(), false,
+ uriRequest.getRequestedExtent());
mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
} else {
Log.v(TAG, "Cannot load photo " + uri);
- cacheBitmap(uri, null, false);
+ cacheBitmap(uri, null, false, uriRequest.getRequestedExtent());
}
} catch (Exception ex) {
Log.v(TAG, "Cannot load photo " + uri, ex);
- cacheBitmap(uri, null, false);
+ cacheBitmap(uri, null, false, uriRequest.getRequestedExtent());
}
}
}
@@ -1033,57 +1127,68 @@
private final long mId;
private final Uri mUri;
private final boolean mDarkTheme;
- private final boolean mHires;
+ private final int mRequestedExtent;
private final DefaultImageProvider mDefaultProvider;
- private Request(long id, Uri uri, boolean hires, boolean darkTheme,
+ private Request(long id, Uri uri, int requestedExtent, boolean darkTheme,
DefaultImageProvider defaultProvider) {
mId = id;
mUri = uri;
mDarkTheme = darkTheme;
- mHires = hires;
+ mRequestedExtent = requestedExtent;
mDefaultProvider = defaultProvider;
}
- public static Request createFromId(long id, boolean hires, boolean darkTheme,
+ public static Request createFromThumbnailId(long id, boolean darkTheme,
DefaultImageProvider defaultProvider) {
- return new Request(id, null /* no URI */, hires, darkTheme, defaultProvider);
+ return new Request(id, null /* no URI */, -1, darkTheme, defaultProvider);
}
- public static Request createFromUri(Uri uri, boolean hires, boolean darkTheme,
+ public static Request createFromUri(Uri uri, int requestedExtent, boolean darkTheme,
DefaultImageProvider defaultProvider) {
- return new Request(0 /* no ID */, uri, hires, darkTheme, defaultProvider);
- }
-
- public boolean isDarkTheme() {
- return mDarkTheme;
- }
-
- public boolean isHires() {
- return mHires;
+ return new Request(0 /* no ID */, uri, requestedExtent, darkTheme, defaultProvider);
}
public boolean isUriRequest() {
return mUri != null;
}
- @Override
- public int hashCode() {
- if (mUri != null) return mUri.hashCode();
+ public Uri getUri() {
+ return mUri;
+ }
- // copied over from Long.hashCode()
- return (int) (mId ^ (mId >>> 32));
+ public long getId() {
+ return mId;
+ }
+
+ public int getRequestedExtent() {
+ return mRequestedExtent;
}
@Override
- public boolean equals(Object o) {
- if (!(o instanceof Request)) return false;
- final Request that = (Request) o;
- // Don't compare equality of mHires and mDarkTheme fields because these are only used
- // in the default contact photo case. When the contact does have a photo, the contact
- // photo is the same regardless of mHires and mDarkTheme, so we shouldn't need to put
- // the photo request on the queue twice.
- return mId == that.mId && UriUtils.areEqual(mUri, that.mUri);
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (int) (mId ^ (mId >>> 32));
+ result = prime * result + mRequestedExtent;
+ result = prime * result + ((mUri == null) ? 0 : mUri.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+ final Request that = (Request) obj;
+ if (mId != that.mId) return false;
+ if (mRequestedExtent != that.mRequestedExtent) return false;
+ if (!UriUtils.areEqual(mUri, that.mUri)) return false;
+ // Don't compare equality of mDarkTheme because it is only used in the default contact
+ // photo case. When the contact does have a photo, the contact photo is the same
+ // regardless of mDarkTheme, so we shouldn't need to put the photo request on the queue
+ // twice.
+ return true;
}
public Object getKey() {
@@ -1091,7 +1196,7 @@
}
public void applyDefaultImage(ImageView view) {
- mDefaultProvider.applyDefaultImage(view, mHires, mDarkTheme);
+ mDefaultProvider.applyDefaultImage(view, mRequestedExtent, mDarkTheme);
}
}
}
diff --git a/src/com/android/contacts/ContactsUtils.java b/src/com/android/contacts/ContactsUtils.java
index 687bd0d..ea567fb 100644
--- a/src/com/android/contacts/ContactsUtils.java
+++ b/src/com/android/contacts/ContactsUtils.java
@@ -17,7 +17,6 @@
package com.android.contacts;
import com.android.contacts.activities.DialtactsActivity;
-import com.android.contacts.calllog.PhoneNumberHelper;
import com.android.contacts.model.AccountType;
import com.android.contacts.model.AccountTypeManager;
import com.android.contacts.model.AccountWithDataSet;
@@ -27,12 +26,14 @@
import android.content.Context;
import android.content.Intent;
+import android.database.Cursor;
import android.graphics.Rect;
import android.location.CountryDetector;
import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Im;
import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.DisplayPhoto;
import android.provider.ContactsContract.QuickContact;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
@@ -45,6 +46,7 @@
private static final String TAG = "ContactsUtils";
private static final String WAIT_SYMBOL_AS_STRING = String.valueOf(PhoneNumberUtils.WAIT);
+ private static int sThumbnailSize = -1;
// TODO find a proper place for the canonical version of these
public interface ProviderNames {
@@ -305,4 +307,24 @@
rect.bottom = (int) ((pos[1] + view.getHeight()) * appScale + 0.5f);
return rect;
}
+
+ /**
+ * Returns the size (width and height) of thumbnail pictures as configured in the provider. This
+ * can safely be called from the UI thread, as the provider can serve this without performing
+ * a database access
+ */
+ public static int getThumbnailSize(Context context) {
+ if (sThumbnailSize == -1) {
+ final Cursor c = context.getContentResolver().query(
+ DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI,
+ new String[] { DisplayPhoto.THUMBNAIL_MAX_DIM }, null, null, null);
+ try {
+ c.moveToFirst();
+ sThumbnailSize = c.getInt(0);
+ } finally {
+ c.close();
+ }
+ }
+ return sThumbnailSize;
+ }
}
diff --git a/src/com/android/contacts/activities/PhotoSelectionActivity.java b/src/com/android/contacts/activities/PhotoSelectionActivity.java
index 4a6b187..d76af25 100644
--- a/src/com/android/contacts/activities/PhotoSelectionActivity.java
+++ b/src/com/android/contacts/activities/PhotoSelectionActivity.java
@@ -35,9 +35,9 @@
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
+import android.util.Log;
import android.view.View;
import android.view.ViewGroup.MarginLayoutParams;
-
import android.widget.FrameLayout.LayoutParams;
import android.widget.ImageView;
@@ -48,6 +48,8 @@
*/
public class PhotoSelectionActivity extends Activity {
+ private static final String TAG = "PhotoSelectionActivity";
+
/** Number of ms for the animation to expand the photo. */
private static final int PHOTO_EXPAND_DURATION = 100;
@@ -243,15 +245,6 @@
}
private void displayPhoto() {
- // Load the photo.
- if (mPhotoUri != null) {
- // If we have a URI, the bitmap should be cached directly.
- ContactPhotoManager.getInstance(this).loadPhoto(mPhotoView, mPhotoUri, true, false);
- } else {
- // Fall back to avatar image.
- mPhotoView.setImageResource(ContactPhotoManager.getDefaultAvatarResId(true, false));
- }
-
// Animate the photo view into its end location.
final int[] pos = new int[2];
mBackdrop.getLocationOnScreen(pos);
@@ -267,6 +260,19 @@
mPhotoView.setLayoutParams(layoutParams);
mPhotoView.requestLayout();
+ // Load the photo.
+ int photoWidth = getPhotoEndParams().width;
+ Log.d(TAG, "Photo width: " + photoWidth);
+ if (mPhotoUri != null) {
+ // If we have a URI, the bitmap should be cached directly.
+ ContactPhotoManager.getInstance(this).loadPhoto(mPhotoView, mPhotoUri, photoWidth,
+ false);
+ } else {
+ // Fall back to avatar image.
+ mPhotoView.setImageResource(ContactPhotoManager.getDefaultAvatarResId(this, photoWidth,
+ false));
+ }
+
mPhotoView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
diff --git a/src/com/android/contacts/calllog/CallLogAdapter.java b/src/com/android/contacts/calllog/CallLogAdapter.java
index 8c5005e..2275a3d 100644
--- a/src/com/android/contacts/calllog/CallLogAdapter.java
+++ b/src/com/android/contacts/calllog/CallLogAdapter.java
@@ -737,7 +737,7 @@
private void setPhoto(CallLogListItemViews views, long photoId, Uri contactUri) {
views.quickContactView.assignContactUri(contactUri);
- mContactPhotoManager.loadPhoto(views.quickContactView, photoId, false, true);
+ mContactPhotoManager.loadThumbnail(views.quickContactView, photoId, true);
}
/**
diff --git a/src/com/android/contacts/detail/ContactDetailDisplayUtils.java b/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
index 319cba5..404148d 100644
--- a/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
+++ b/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
@@ -20,8 +20,6 @@
import com.android.contacts.ContactLoader.Result;
import com.android.contacts.ContactPhotoManager;
import com.android.contacts.R;
-import com.android.contacts.activities.PhotoSelectionActivity;
-import com.android.contacts.model.EntityDeltaList;
import com.android.contacts.preference.ContactsPreferences;
import com.android.contacts.util.ContactBadgeUtil;
import com.android.contacts.util.HtmlUtils;
@@ -35,14 +33,10 @@
import android.content.Context;
import android.content.Entity;
import android.content.Entity.NamedContentValues;
-import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.provider.ContactsContract;
@@ -57,10 +51,7 @@
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
-import android.view.View.OnClickListener;
import android.view.ViewGroup;
-import android.view.animation.AccelerateInterpolator;
-import android.view.animation.AlphaAnimation;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
@@ -75,8 +66,6 @@
public class ContactDetailDisplayUtils {
private static final String TAG = "ContactDetailDisplayUtils";
- private static final int PHOTO_FADE_IN_ANIMATION_DURATION_MILLIS = 100;
-
/**
* Tag object used for stream item photos.
*/
@@ -195,88 +184,6 @@
}
/**
- * Sets the contact photo to display in the given {@link ImageView}. If bitmap is null, the
- * default placeholder image is shown.
- * @param context The context.
- * @param contactData The contact loader result.
- * @param photoView The photo view that will host the image and act as the basis for the
- * photo selector.
- * @param expandPhotoOnClick Whether the photo should be expanded to fill more of the screen
- * when clicked.
- * @return The onclick listener for the photo. When clicked, a photo selection activity will
- * be launched.
- */
- public static OnClickListener setPhoto(Context context, Result contactData,
- ImageView photoView, boolean expandPhotoOnClick) {
- byte[] photo = contactData.getPhotoBinaryData();
- Bitmap bitmap = photo != null ? BitmapFactory.decodeByteArray(photo, 0, photo.length)
- : ContactBadgeUtil.loadDefaultAvatarPhoto(context, true, false);
- boolean fadeIn = contactData.isDirectoryEntry();
- if (photoView.getDrawable() == null && fadeIn) {
- AlphaAnimation animation = new AlphaAnimation(0, 1);
- animation.setDuration(PHOTO_FADE_IN_ANIMATION_DURATION_MILLIS);
- animation.setInterpolator(new AccelerateInterpolator());
- photoView.startAnimation(animation);
- }
- photoView.setImageBitmap(bitmap);
-
- // Set up the photo to display a full-screen photo selection activity when clicked.
- OnClickListener clickListener = new PhotoClickListener(context, contactData, bitmap,
- photo, expandPhotoOnClick);
- photoView.setOnClickListener(clickListener);
- return clickListener;
- }
-
- private static final class PhotoClickListener implements OnClickListener {
-
- private final Context mContext;
- private final Result mContactData;
- private final Bitmap mPhotoBitmap;
- private final byte[] mPhotoBytes;
- private final boolean mExpandPhotoOnClick;
- public PhotoClickListener(Context context, Result contactData, Bitmap photoBitmap,
- byte[] photoBytes, boolean expandPhotoOnClick) {
- mContext = context;
- mContactData = contactData;
- mPhotoBitmap = photoBitmap;
- mPhotoBytes = photoBytes;
- mExpandPhotoOnClick = expandPhotoOnClick;
- }
-
- @Override
- public void onClick(View v) {
- // Assemble the intent.
- EntityDeltaList delta = EntityDeltaList.fromIterator(
- mContactData.getEntities().iterator());
-
- // Find location and bounds of target view, adjusting based on the
- // assumed local density.
- final float appScale =
- mContext.getResources().getCompatibilityInfo().applicationScale;
- final int[] pos = new int[2];
- v.getLocationOnScreen(pos);
-
- final Rect rect = new Rect();
- rect.left = (int) (pos[0] * appScale + 0.5f);
- rect.top = (int) (pos[1] * appScale + 0.5f);
- rect.right = (int) ((pos[0] + v.getWidth()) * appScale + 0.5f);
- rect.bottom = (int) ((pos[1] + v.getHeight()) * appScale + 0.5f);
-
- Uri photoUri = null;
- if (mContactData.getPhotoUri() != null) {
- photoUri = Uri.parse(mContactData.getPhotoUri());
- }
- Intent photoSelectionIntent = PhotoSelectionActivity.buildIntent(mContext,
- photoUri, mPhotoBitmap, mPhotoBytes, rect, delta, mContactData.isUserProfile(),
- mContactData.isDirectoryEntry(), mExpandPhotoOnClick);
- // Cache the bitmap directly, so the activity can pull it from the photo manager.
- ContactPhotoManager.getInstance(mContext).cacheBitmap(photoUri, mPhotoBitmap,
- mPhotoBytes);
- mContext.startActivity(photoSelectionIntent);
- }
- }
-
- /**
* Sets the starred state of this contact.
*/
public static void configureStarredImageView(ImageView starredView, boolean isDirectoryEntry,
@@ -340,7 +247,7 @@
setDataOrHideIfNone(snippet, statusView);
if (photoUri != null) {
ContactPhotoManager.getInstance(context).loadPhoto(
- statusPhotoView, Uri.parse(photoUri), true, false,
+ statusPhotoView, Uri.parse(photoUri), -1, false,
ContactPhotoManager.DEFAULT_BLANK);
statusPhotoView.setVisibility(View.VISIBLE);
} else {
@@ -445,7 +352,7 @@
pushLayerView.setClickable(false);
pushLayerView.setEnabled(false);
}
- contactPhotoManager.loadPhoto(imageView, Uri.parse(streamItemPhoto.getPhotoUri()), true,
+ contactPhotoManager.loadPhoto(imageView, Uri.parse(streamItemPhoto.getPhotoUri()), -1,
false, ContactPhotoManager.DEFAULT_BLANK);
}
diff --git a/src/com/android/contacts/detail/TransformableImageView.java b/src/com/android/contacts/detail/TransformableImageView.java
index 6edc42b..241df41 100644
--- a/src/com/android/contacts/detail/TransformableImageView.java
+++ b/src/com/android/contacts/detail/TransformableImageView.java
@@ -16,11 +16,9 @@
package com.android.contacts.detail;
import android.content.Context;
-import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.util.AttributeSet;
-import android.view.View;
import android.widget.ImageView;
/**
diff --git a/src/com/android/contacts/group/GroupDetailFragment.java b/src/com/android/contacts/group/GroupDetailFragment.java
index b4f642a..068a641 100644
--- a/src/com/android/contacts/group/GroupDetailFragment.java
+++ b/src/com/android/contacts/group/GroupDetailFragment.java
@@ -222,6 +222,11 @@
// No need to call phone number directly from People app.
Log.w(TAG, "unexpected invocation of onCallNumberDirectly()");
}
+
+ @Override
+ public int getApproximateTileWidth() {
+ return getView().getWidth() / mAdapter.getColumnCount();
+ }
};
/**
diff --git a/src/com/android/contacts/group/GroupEditorFragment.java b/src/com/android/contacts/group/GroupEditorFragment.java
index 6ed652f..6940efb 100644
--- a/src/com/android/contacts/group/GroupEditorFragment.java
+++ b/src/com/android/contacts/group/GroupEditorFragment.java
@@ -29,6 +29,7 @@
import com.android.contacts.model.AccountTypeManager;
import com.android.contacts.model.AccountWithDataSet;
import com.android.contacts.util.AccountsListAdapter.AccountListFilter;
+import com.android.contacts.util.ViewUtil;
import com.android.internal.util.Objects;
import android.accounts.Account;
@@ -959,7 +960,8 @@
});
}
- mPhotoManager.loadPhoto(badge, member.getPhotoUri(), false, false);
+ mPhotoManager.loadPhoto(badge, member.getPhotoUri(),
+ ViewUtil.getConstantPreLayoutWidth(badge), false);
return result;
}
diff --git a/src/com/android/contacts/list/ContactEntryListAdapter.java b/src/com/android/contacts/list/ContactEntryListAdapter.java
index ac4d399..d947f06 100644
--- a/src/com/android/contacts/list/ContactEntryListAdapter.java
+++ b/src/com/android/contacts/list/ContactEntryListAdapter.java
@@ -638,11 +638,11 @@
getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn));
if (photoId != 0 || photoUriColumn == -1) {
- getPhotoLoader().loadPhoto(quickContact, photoId, false, mDarkTheme);
+ getPhotoLoader().loadThumbnail(quickContact, photoId, mDarkTheme);
} else {
final String photoUriString = cursor.getString(photoUriColumn);
final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
- getPhotoLoader().loadPhoto(quickContact, photoUri, false, mDarkTheme);
+ getPhotoLoader().loadPhoto(quickContact, photoUri, -1, mDarkTheme);
}
}
diff --git a/src/com/android/contacts/list/ContactListAdapter.java b/src/com/android/contacts/list/ContactListAdapter.java
index 81fcb84..18f7e04 100644
--- a/src/com/android/contacts/list/ContactListAdapter.java
+++ b/src/com/android/contacts/list/ContactListAdapter.java
@@ -225,11 +225,11 @@
}
if (photoId != 0) {
- getPhotoLoader().loadPhoto(view.getPhotoView(), photoId, false, false);
+ getPhotoLoader().loadThumbnail(view.getPhotoView(), photoId, false);
} else {
final String photoUriString = cursor.getString(ContactQuery.CONTACT_PHOTO_URI);
final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
- getPhotoLoader().loadPhoto(view.getPhotoView(), photoUri, false, false);
+ getPhotoLoader().loadDirectoryPhoto(view.getPhotoView(), photoUri, false);
}
}
diff --git a/src/com/android/contacts/list/ContactTileAdapter.java b/src/com/android/contacts/list/ContactTileAdapter.java
index 26df446..9ea0468 100644
--- a/src/com/android/contacts/list/ContactTileAdapter.java
+++ b/src/com/android/contacts/list/ContactTileAdapter.java
@@ -23,13 +23,11 @@
import com.android.contacts.GroupMemberLoader;
import com.android.contacts.GroupMemberLoader.GroupDetailQuery;
import com.android.contacts.R;
-import com.android.contacts.list.ContactTileAdapter.DisplayType;
import android.content.ContentUris;
import android.content.Context;
import android.content.res.Resources;
import android.database.Cursor;
-import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.provider.ContactsContract.CommonDataKinds.Phone;
@@ -315,6 +313,10 @@
return entryCount == 0 ? 0 : ((entryCount - 1) / mColumnCount) + 1;
}
+ public int getColumnCount() {
+ return mColumnCount;
+ }
+
/**
* Returns an ArrayList of the {@link ContactEntry}s that are to appear
* on the row for the given position.
@@ -418,8 +420,8 @@
case ViewTypes.FREQUENT:
return mDisplayType == DisplayType.STREQUENT_PHONE_ONLY ?
R.layout.contact_tile_frequent_phone : R.layout.contact_tile_frequent;
- case ViewTypes.STARRED_WITH_SECONDARY_ACTION:
- return R.layout.contact_tile_starred_secondary_target;
+ case ViewTypes.STARRED_PHONE:
+ return R.layout.contact_tile_phone_starred;
default:
throw new IllegalArgumentException("Unrecognized viewType " + viewType);
}
@@ -450,7 +452,7 @@
}
case STREQUENT_PHONE_ONLY:
if (position < getRowCount(mDividerPosition)) {
- return ViewTypes.STARRED_WITH_SECONDARY_ACTION;
+ return ViewTypes.STARRED_PHONE;
} else if (position == getRowCount(mDividerPosition)) {
return ViewTypes.DIVIDER;
} else {
@@ -529,7 +531,7 @@
contactTile.loadFromContact(entry);
switch (mItemViewType) {
- case ViewTypes.STARRED_WITH_SECONDARY_ACTION:
+ case ViewTypes.STARRED_PHONE:
case ViewTypes.STARRED:
// Setting divider visibilities
contactTile.setPadding(0, 0,
@@ -548,9 +550,9 @@
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
switch (mItemViewType) {
- case ViewTypes.STARRED_WITH_SECONDARY_ACTION:
+ case ViewTypes.STARRED_PHONE:
case ViewTypes.STARRED:
- onLayoutForTiles(left, top, right, bottom);
+ onLayoutForTiles();
return;
default:
super.onLayout(changed, left, top, right, bottom);
@@ -558,9 +560,8 @@
}
}
- private void onLayoutForTiles(int left, int top, int right, int bottom) {
+ private void onLayoutForTiles() {
final int count = getChildCount();
- final int width = right - left;
// Just line up children horizontally.
int childLeft = 0;
@@ -577,9 +578,9 @@
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
switch (mItemViewType) {
- case ViewTypes.STARRED_WITH_SECONDARY_ACTION:
+ case ViewTypes.STARRED_PHONE:
case ViewTypes.STARRED:
- onMeasureForTiles(widthMeasureSpec, heightMeasureSpec);
+ onMeasureForTiles(widthMeasureSpec);
return;
default:
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
@@ -587,7 +588,7 @@
}
}
- private void onMeasureForTiles(int widthMeasureSpec, int heightMeasureSpec) {
+ private void onMeasureForTiles(int widthMeasureSpec) {
final int width = MeasureSpec.getSize(widthMeasureSpec);
final int childCount = getChildCount();
@@ -655,6 +656,6 @@
public static final int STARRED = 0;
public static final int DIVIDER = 1;
public static final int FREQUENT = 2;
- public static final int STARRED_WITH_SECONDARY_ACTION = 3;
+ public static final int STARRED_PHONE = 3;
}
}
diff --git a/src/com/android/contacts/list/ContactTileFrequentFragment.java b/src/com/android/contacts/list/ContactTileFrequentFragment.java
index d958c95..73ff6cc 100644
--- a/src/com/android/contacts/list/ContactTileFrequentFragment.java
+++ b/src/com/android/contacts/list/ContactTileFrequentFragment.java
@@ -15,27 +15,13 @@
*/
package com.android.contacts.list;
-import com.android.contacts.ContactPhotoManager;
-import com.android.contacts.ContactTileLoaderFactory;
import com.android.contacts.ContactsUtils;
import com.android.contacts.R;
-import com.android.contacts.list.ContactTileAdapter.DisplayType;
-import android.app.Activity;
-import android.app.Fragment;
-import android.app.LoaderManager;
-import android.app.LoaderManager.LoaderCallbacks;
-import android.content.CursorLoader;
-import android.content.Loader;
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.ListView;
-import android.widget.TextView;
/**
* Fragment containing a list of frequently contacted people.
diff --git a/src/com/android/contacts/list/ContactTileFrequentView.java b/src/com/android/contacts/list/ContactTileFrequentView.java
new file mode 100644
index 0000000..0bd6729
--- /dev/null
+++ b/src/com/android/contacts/list/ContactTileFrequentView.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.list;
+
+import com.android.contacts.util.ViewUtil;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+/**
+ * A {@link ContactTileView} that is used for most frequently contacted in the People app
+ */
+public class ContactTileFrequentView extends ContactTileView {
+ public ContactTileFrequentView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected boolean isDarkTheme() {
+ return false;
+ }
+
+ @Override
+ protected int getApproximateImageSize() {
+ return ViewUtil.getConstantPreLayoutWidth(getQuickContact());
+ }
+}
diff --git a/src/com/android/contacts/list/ContactTileListFragment.java b/src/com/android/contacts/list/ContactTileListFragment.java
index fbd7fec..92ff41d 100644
--- a/src/com/android/contacts/list/ContactTileListFragment.java
+++ b/src/com/android/contacts/list/ContactTileListFragment.java
@@ -179,5 +179,10 @@
mListener.onCallNumberDirectly(phoneNumber);
}
}
+
+ @Override
+ public int getApproximateTileWidth() {
+ return getView().getWidth() / mAdapter.getColumnCount();
+ }
};
}
diff --git a/src/com/android/contacts/list/ContactTilePhoneFrequentView.java b/src/com/android/contacts/list/ContactTilePhoneFrequentView.java
index d924b03..88796ba 100644
--- a/src/com/android/contacts/list/ContactTilePhoneFrequentView.java
+++ b/src/com/android/contacts/list/ContactTilePhoneFrequentView.java
@@ -15,18 +15,18 @@
*/
package com.android.contacts.list;
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.text.TextUtils;
-
import com.android.contacts.ContactsUtils;
import com.android.contacts.list.ContactTileAdapter.ContactEntry;
+import com.android.contacts.util.ViewUtil;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
/**
* A dark version of the {@link ContactTileView} that is used in Dialtacts
- * for frequently called contacts. Slightly different behavior from superclass...
+ * for frequently called contacts. Slightly different behavior from superclass...
* when you tap it, you want to call the frequently-called number for the
* contact, even if that is not the default number for that contact.
*/
@@ -43,6 +43,11 @@
}
@Override
+ protected int getApproximateImageSize() {
+ return ViewUtil.getConstantPreLayoutWidth(getQuickContact());
+ }
+
+ @Override
public void loadFromContact(ContactEntry entry) {
super.loadFromContact(entry);
mPhoneNumberString = null; // ... in case we're reusing the view
diff --git a/src/com/android/contacts/list/ContactTileSecondaryTargetView.java b/src/com/android/contacts/list/ContactTilePhoneStarredView.java
similarity index 77%
rename from src/com/android/contacts/list/ContactTileSecondaryTargetView.java
rename to src/com/android/contacts/list/ContactTilePhoneStarredView.java
index 25f353d..fc53782 100644
--- a/src/com/android/contacts/list/ContactTileSecondaryTargetView.java
+++ b/src/com/android/contacts/list/ContactTilePhoneStarredView.java
@@ -24,17 +24,13 @@
import android.widget.ImageButton;
/**
- * A {@link ContactTileSecondaryTargetView} displays the contact's picture overlayed with their name
- * in a perfect square like the {@link ContactTileStarredView}. However it adds in an additional
- * touch target for a secondary action.
+ * Displays the contact's picture overlayed with their name
+ * in a perfect square. It also has an additional touch target for a secondary action.
*/
-public class ContactTileSecondaryTargetView extends ContactTileStarredView {
-
- private final static String TAG = ContactTileSecondaryTargetView.class.getSimpleName();
-
+public class ContactTilePhoneStarredView extends ContactTileView {
private ImageButton mSecondaryButton;
- public ContactTileSecondaryTargetView(Context context, AttributeSet attrs) {
+ public ContactTilePhoneStarredView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@@ -59,4 +55,10 @@
protected boolean isDarkTheme() {
return true;
}
+
+ @Override
+ protected int getApproximateImageSize() {
+ // The picture is the full size of the tile (minus some padding, but we can be generous)
+ return mListener.getApproximateTileWidth();
+ }
}
diff --git a/src/com/android/contacts/list/ContactTileStarredView.java b/src/com/android/contacts/list/ContactTileStarredView.java
index 3be6bf2..ee76d4d 100644
--- a/src/com/android/contacts/list/ContactTileStarredView.java
+++ b/src/com/android/contacts/list/ContactTileStarredView.java
@@ -20,18 +20,22 @@
/**
* A {@link ContactTileStarredView} displays the contact's picture overlayed with their name
- * in a square. The actual dimensions are set by
+ * in a square. The actual dimensions are set by
* {@link com.android.contacts.list.ContactTileAdapter.ContactTileRow}.
*/
public class ContactTileStarredView extends ContactTileView {
- private final static String TAG = ContactTileStarredView.class.getSimpleName();
-
public ContactTileStarredView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
- protected boolean isDefaultIconHires() {
- return true;
+ protected boolean isDarkTheme() {
+ return false;
+ }
+
+ @Override
+ protected int getApproximateImageSize() {
+ // The picture is the full size of the tile (minus some padding, but we can be generous)
+ return mListener.getApproximateTileWidth();
}
}
diff --git a/src/com/android/contacts/list/ContactTileView.java b/src/com/android/contacts/list/ContactTileView.java
index e73b9c1..528e7a3 100644
--- a/src/com/android/contacts/list/ContactTileView.java
+++ b/src/com/android/contacts/list/ContactTileView.java
@@ -32,9 +32,9 @@
import android.widget.TextView;
/**
- * A ContactTile displays the contact's picture overlayed with their name
+ * A ContactTile displays a contact's picture and name
*/
-public class ContactTileView extends FrameLayout {
+public abstract class ContactTileView extends FrameLayout {
private final static String TAG = ContactTileView.class.getSimpleName();
private Uri mLookupUri;
@@ -66,7 +66,6 @@
mPushState = findViewById(R.id.contact_tile_push_state);
mHorizontalDivider = findViewById(R.id.contact_tile_horizontal_divider);
-
OnClickListener listener = createClickListener();
if(mPushState != null) {
@@ -126,7 +125,7 @@
if (mPhotoManager != null) {
if (mPhoto != null) {
- mPhotoManager.loadPhoto(mPhoto, entry.photoUri, isDefaultIconHires(),
+ mPhotoManager.loadPhoto(mPhoto, entry.photoUri, getApproximateImageSize(),
isDarkTheme());
if (mQuickContact != null) {
@@ -134,10 +133,9 @@
}
} else if (mQuickContact != null) {
mQuickContact.assignContactUri(mLookupUri);
- mPhotoManager.loadPhoto(mQuickContact, entry.photoUri, isDefaultIconHires(),
- isDarkTheme());
+ mPhotoManager.loadPhoto(mQuickContact, entry.photoUri,
+ getApproximateImageSize(), isDarkTheme());
}
-
} else {
Log.w(TAG, "contactPhotoManager not set");
}
@@ -164,13 +162,17 @@
return mLookupUri;
}
- protected boolean isDefaultIconHires() {
- return false;
+ protected QuickContactBadge getQuickContact() {
+ return mQuickContact;
}
- protected boolean isDarkTheme() {
- return false;
- }
+ /**
+ * Implemented by subclasses to estimate the size of the picture. This can return -1 if only
+ * a thumbnail is shown anyway
+ */
+ protected abstract int getApproximateImageSize();
+
+ protected abstract boolean isDarkTheme();
public interface Listener {
/**
@@ -181,5 +183,10 @@
* Notification that the specified number is to be called.
*/
void onCallNumberDirectly(String phoneNumber);
+ /**
+ * @return The width of each tile. This doesn't have to be a precise number (e.g. paddings
+ * can be ignored), but is used to load the correct picture size from the database
+ */
+ int getApproximateTileWidth();
}
}
diff --git a/src/com/android/contacts/list/EmailAddressListAdapter.java b/src/com/android/contacts/list/EmailAddressListAdapter.java
index 13853a1..c85abdd 100644
--- a/src/com/android/contacts/list/EmailAddressListAdapter.java
+++ b/src/com/android/contacts/list/EmailAddressListAdapter.java
@@ -173,7 +173,7 @@
photoId = cursor.getLong(EmailQuery.EMAIL_PHOTO_ID);
}
- getPhotoLoader().loadPhoto(view.getPhotoView(), photoId, false, false);
+ getPhotoLoader().loadThumbnail(view.getPhotoView(), photoId, false);
}
//
// protected void bindSearchSnippet(final ContactListItemView view, Cursor cursor) {
diff --git a/src/com/android/contacts/list/PhoneFavoriteFragment.java b/src/com/android/contacts/list/PhoneFavoriteFragment.java
index c24af93..2e62d1a 100644
--- a/src/com/android/contacts/list/PhoneFavoriteFragment.java
+++ b/src/com/android/contacts/list/PhoneFavoriteFragment.java
@@ -151,6 +151,11 @@
mListener.onCallNumberDirectly(phoneNumber);
}
}
+
+ @Override
+ public int getApproximateTileWidth() {
+ return getView().getWidth() / mContactTileAdapter.getColumnCount();
+ }
}
private class FilterHeaderClickListener implements OnClickListener {
diff --git a/src/com/android/contacts/list/PhoneNumberListAdapter.java b/src/com/android/contacts/list/PhoneNumberListAdapter.java
index 2b231a7..bc73f73 100644
--- a/src/com/android/contacts/list/PhoneNumberListAdapter.java
+++ b/src/com/android/contacts/list/PhoneNumberListAdapter.java
@@ -309,7 +309,7 @@
photoId = cursor.getLong(PhoneQuery.PHONE_PHOTO_ID);
}
- getPhotoLoader().loadPhoto(view.getPhotoView(), photoId, false, false);
+ getPhotoLoader().loadThumbnail(view.getPhotoView(), photoId, false);
}
public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) {
diff --git a/src/com/android/contacts/list/PostalAddressListAdapter.java b/src/com/android/contacts/list/PostalAddressListAdapter.java
index 2750e11..5e3be30 100644
--- a/src/com/android/contacts/list/PostalAddressListAdapter.java
+++ b/src/com/android/contacts/list/PostalAddressListAdapter.java
@@ -163,7 +163,7 @@
photoId = cursor.getLong(PostalQuery.POSTAL_PHOTO_ID);
}
- getPhotoLoader().loadPhoto(view.getPhotoView(), photoId, false, false);
+ getPhotoLoader().loadThumbnail(view.getPhotoView(), photoId, false);
}
//
// protected void bindSearchSnippet(final ContactListItemView view, Cursor cursor) {
diff --git a/src/com/android/contacts/util/BitmapUtil.java b/src/com/android/contacts/util/BitmapUtil.java
new file mode 100644
index 0000000..6f6650f
--- /dev/null
+++ b/src/com/android/contacts/util/BitmapUtil.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.util;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+/**
+ * Provides static functions to decode bitmaps at the optimal size
+ */
+public class BitmapUtil {
+ private BitmapUtil() {}
+
+ /**
+ * Returns Width or Height of the picture, depending on which size is smaller. Doesn't actually
+ * decode the picture, so it is pretty efficient to run.
+ */
+ public static int getSmallerExtentFromBytes(byte[] bytes) {
+ final BitmapFactory.Options options = new BitmapFactory.Options();
+
+ // don't actually decode the picture, just return its bounds
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
+
+ // test what the best sample size is
+ return Math.min(options.outWidth, options.outHeight);
+ }
+
+ /**
+ * Finds the optimal sampleSize for loading the picture
+ * @param originalSmallerExtent Width or height of the picture, whichever is smaller
+ * @param targetExtent Width or height of the target view, whichever is bigger.
+ *
+ * If either one of the parameters is 0 or smaller, no sampling is applied
+ */
+ public static int findOptimalSampleSize(int originalSmallerExtent, int targetExtent) {
+ // If we don't know sizes, we can't do sampling.
+ if (targetExtent < 1) return 1;
+ if (originalSmallerExtent < 1) return 1;
+
+ // test what the best sample size is
+ int extent = originalSmallerExtent;
+ int sampleSize = 1;
+ while ((extent >> 1) >= targetExtent) {
+ sampleSize <<= 1;
+ extent >>= 1;
+ }
+
+ return sampleSize;
+ }
+
+ /**
+ * Decodes the bitmap with the given sample size
+ */
+ public static Bitmap decodeBitmapFromBytes(byte[] bytes, int sampleSize) {
+ final BitmapFactory.Options options;
+ if (sampleSize <= 1) {
+ options = null;
+ } else {
+ options = new BitmapFactory.Options();
+ options.inSampleSize = sampleSize;
+ }
+ return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
+ }
+}
diff --git a/src/com/android/contacts/util/ImageViewDrawableSetter.java b/src/com/android/contacts/util/ImageViewDrawableSetter.java
index 09c6e5c..5cde022 100644
--- a/src/com/android/contacts/util/ImageViewDrawableSetter.java
+++ b/src/com/android/contacts/util/ImageViewDrawableSetter.java
@@ -16,25 +16,18 @@
package com.android.contacts.util;
-import android.content.Context;
-import android.content.Intent;
+import com.android.contacts.ContactLoader.Result;
+import com.android.contacts.ContactPhotoManager;
+
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
-import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
-import android.net.Uri;
import android.util.Log;
-import android.view.View;
-import android.view.View.OnClickListener;
import android.widget.ImageView;
-import com.android.contacts.ContactPhotoManager;
-import com.android.contacts.ContactLoader.Result;
-import com.android.contacts.activities.PhotoSelectionActivity;
-import com.android.contacts.model.EntityDeltaList;
import java.util.Arrays;
@@ -58,7 +51,7 @@
public void setupContactPhoto(Result contactData, ImageView photoView) {
setTarget(photoView);
- Bitmap bitmap = setCompressedImage(contactData.getPhotoBinaryData());
+ setCompressedImage(contactData.getPhotoBinaryData());
}
public ImageView getTarget() {
diff --git a/src/com/android/contacts/util/ViewUtil.java b/src/com/android/contacts/util/ViewUtil.java
new file mode 100644
index 0000000..89ab73d
--- /dev/null
+++ b/src/com/android/contacts/util/ViewUtil.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.util;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Provides static functions to work with views
+ */
+public class ViewUtil {
+ private ViewUtil() {}
+
+ /**
+ * Returns the width as specified in the LayoutParams
+ * @throws IllegalStateException Thrown if the view's width is unknown before a layout pass
+ * s
+ */
+ public static int getConstantPreLayoutWidth(View view) {
+ // We haven't been layed out yet, so get the size from the LayoutParams
+ final ViewGroup.LayoutParams p = view.getLayoutParams();
+ if (p.width < 0) {
+ throw new IllegalStateException("Expecting view's width to be a constant rather " +
+ "than a result of the layout pass");
+ }
+ return p.width;
+ }
+}
diff --git a/tests/src/com/android/contacts/tests/mocks/MockContactPhotoManager.java b/tests/src/com/android/contacts/tests/mocks/MockContactPhotoManager.java
index 67b7c0c..10682c1 100644
--- a/tests/src/com/android/contacts/tests/mocks/MockContactPhotoManager.java
+++ b/tests/src/com/android/contacts/tests/mocks/MockContactPhotoManager.java
@@ -28,15 +28,15 @@
*/
public class MockContactPhotoManager extends ContactPhotoManager {
@Override
- public void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme,
+ public void loadThumbnail(ImageView view, long photoId, boolean darkTheme,
DefaultImageProvider defaultProvider) {
- defaultProvider.applyDefaultImage(view, hires, darkTheme);
+ defaultProvider.applyDefaultImage(view, -1, darkTheme);
}
@Override
- public void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme,
+ public void loadPhoto(ImageView view, Uri photoUri, int requestedExtent, boolean darkTheme,
DefaultImageProvider defaultProvider) {
- defaultProvider.applyDefaultImage(view, hires, darkTheme);
+ defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme);
}
@Override
diff --git a/tests/src/com/android/contacts/util/BitmapUtilTests.java b/tests/src/com/android/contacts/util/BitmapUtilTests.java
new file mode 100644
index 0000000..554fc97
--- /dev/null
+++ b/tests/src/com/android/contacts/util/BitmapUtilTests.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.util;
+
+import android.graphics.Bitmap;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * Tests for {@link BitmapUtil}.
+ */
+@SmallTest
+public class BitmapUtilTests extends AndroidTestCase {
+ public void testGetSmallerExtentFromBytes1() throws Exception {
+ assertEquals(100, BitmapUtil.getSmallerExtentFromBytes(createJpegRawData(100, 100)));
+ assertEquals(100, BitmapUtil.getSmallerExtentFromBytes(createPngRawData(100, 100)));
+ }
+
+ public void testGetSmallerExtentFromBytes2() throws Exception {
+ assertEquals(50, BitmapUtil.getSmallerExtentFromBytes(createJpegRawData(200, 50)));
+ assertEquals(50, BitmapUtil.getSmallerExtentFromBytes(createPngRawData(200, 50)));
+ }
+
+ public void testGetSmallerExtentFromBytes3() throws Exception {
+ assertEquals(40, BitmapUtil.getSmallerExtentFromBytes(createJpegRawData(40, 150)));
+ assertEquals(40, BitmapUtil.getSmallerExtentFromBytes(createPngRawData(40, 150)));
+ }
+
+ public void testFindOptimalSampleSizeExact() throws Exception {
+ assertEquals(1, BitmapUtil.findOptimalSampleSize(512, 512));
+ }
+
+ public void testFindOptimalSampleSizeBigger() throws Exception {
+ assertEquals(1, BitmapUtil.findOptimalSampleSize(512, 1024));
+ }
+
+ public void testFindOptimalSampleSizeSmaller1() throws Exception {
+ assertEquals(2, BitmapUtil.findOptimalSampleSize(512, 256));
+ }
+
+ public void testFindOptimalSampleSizeSmaller2() throws Exception {
+ assertEquals(2, BitmapUtil.findOptimalSampleSize(512, 230));
+ }
+
+ public void testFindOptimalSampleSizeSmaller3() throws Exception {
+ assertEquals(2, BitmapUtil.findOptimalSampleSize(512, 129));
+ }
+
+ public void testFindOptimalSampleSizeSmaller4() throws Exception {
+ assertEquals(4, BitmapUtil.findOptimalSampleSize(512, 128));
+ }
+
+ public void testFindOptimalSampleSizeUnknownOriginal() throws Exception {
+ assertEquals(1, BitmapUtil.findOptimalSampleSize(-1, 128));
+ }
+
+ public void testFindOptimalSampleSizeUnknownTarget() throws Exception {
+ assertEquals(1, BitmapUtil.findOptimalSampleSize(128, -1));
+ }
+
+ public void testDecodeWithSampleSize1() throws IOException {
+ assertBitmapSize(128, 64, BitmapUtil.decodeBitmapFromBytes(createJpegRawData(128, 64), 1));
+ assertBitmapSize(128, 64, BitmapUtil.decodeBitmapFromBytes(createPngRawData(128, 64), 1));
+ }
+
+ public void testDecodeWithSampleSize2() throws IOException {
+ assertBitmapSize(64, 32, BitmapUtil.decodeBitmapFromBytes(createJpegRawData(128, 64), 2));
+ assertBitmapSize(64, 32, BitmapUtil.decodeBitmapFromBytes(createPngRawData(128, 64), 2));
+ }
+
+ public void testDecodeWithSampleSize2a() throws IOException {
+ assertBitmapSize(25, 20, BitmapUtil.decodeBitmapFromBytes(createJpegRawData(50, 40), 2));
+ assertBitmapSize(25, 20, BitmapUtil.decodeBitmapFromBytes(createPngRawData(50, 40), 2));
+ }
+
+ public void testDecodeWithSampleSize4() throws IOException {
+ assertBitmapSize(32, 16, BitmapUtil.decodeBitmapFromBytes(createJpegRawData(128, 64), 4));
+ assertBitmapSize(32, 16, BitmapUtil.decodeBitmapFromBytes(createPngRawData(128, 64), 4));
+ }
+
+ private void assertBitmapSize(int expectedWidth, int expectedHeight, Bitmap bitmap) {
+ assertEquals(expectedWidth, bitmap.getWidth());
+ assertEquals(expectedHeight, bitmap.getHeight());
+ }
+
+ private byte[] createJpegRawData(int sourceWidth, int sourceHeight) throws IOException {
+ return createRawData(Bitmap.CompressFormat.JPEG, sourceWidth, sourceHeight);
+ }
+
+ private byte[] createPngRawData(int sourceWidth, int sourceHeight) throws IOException {
+ return createRawData(Bitmap.CompressFormat.PNG, sourceWidth, sourceHeight);
+ }
+
+ private byte[] createRawData(Bitmap.CompressFormat format, int sourceWidth,
+ int sourceHeight) throws IOException {
+ // Create a temp bitmap as our source
+ Bitmap b = Bitmap.createBitmap(sourceWidth, sourceHeight, Bitmap.Config.ARGB_8888);
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ b.compress(format, 50, outputStream);
+ final byte[] data = outputStream.toByteArray();
+ outputStream.close();
+ return data;
+ }
+}