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;
+    }
+}