Adds photos to a-z list.

The implementation is meant to keep scrolling the list as smooth as
possible. Photo loading is only done once the list stops scrolling, at
which point, all visible photos are loaded and stuck in a cache.
diff --git a/src/com/android/contacts/ContactEntryAdapter.java b/src/com/android/contacts/ContactEntryAdapter.java
index 11c862a..ae373cf 100644
--- a/src/com/android/contacts/ContactEntryAdapter.java
+++ b/src/com/android/contacts/ContactEntryAdapter.java
@@ -35,41 +35,43 @@
     public static final String[] AGGREGATE_PROJECTION = new String[] {
         Aggregates.DISPLAY_NAME, // 0
         Aggregates.STARRED, //1
-        Data._ID, //2
-        Data.CONTACT_ID, //3
-        Contacts.PACKAGE, //4
-        Data.MIMETYPE, //5
-        Data.IS_PRIMARY, //6
-        Data.IS_SUPER_PRIMARY, //7
-        Data.DATA1, //8
-        Data.DATA2, //9
-        Data.DATA3, //10
-        Data.DATA4, //11
-        Data.DATA5, //12
-        Data.DATA6, //13
-        Data.DATA7, //14
-        Data.DATA8, //15
-        Data.DATA9, //16
-        Data.DATA10, //17
+        Aggregates.PHOTO_ID, //2
+        Data._ID, //3
+        Data.CONTACT_ID, //4
+        Contacts.PACKAGE, //5
+        Data.MIMETYPE, //6
+        Data.IS_PRIMARY, //7
+        Data.IS_SUPER_PRIMARY, //8
+        Data.DATA1, //9
+        Data.DATA2, //10
+        Data.DATA3, //11
+        Data.DATA4, //12
+        Data.DATA5, //13
+        Data.DATA6, //14
+        Data.DATA7, //15
+        Data.DATA8, //16
+        Data.DATA9, //17
+        Data.DATA10, //18
     };
     public static final int AGGREGATE_DISPLAY_NAME_COLUMN = 0;
     public static final int AGGREGATE_STARRED_COLUMN = 1;
-    public static final int DATA_ID_COLUMN = 2;
-    public static final int DATA_CONTACT_ID_COLUMN = 3;
-    public static final int DATA_PACKAGE_COLUMN = 4;
-    public static final int DATA_MIMETYPE_COLUMN = 5;
-    public static final int DATA_IS_PRIMARY_COLUMN = 6;
-    public static final int DATA_IS_SUPER_PRIMARY_COLUMN = 7;
-    public static final int DATA_1_COLUMN = 8;
-    public static final int DATA_2_COLUMN = 9;
-    public static final int DATA_3_COLUMN = 10;
-    public static final int DATA_4_COLUMN = 11;
-    public static final int DATA_5_COLUMN = 12;
-    public static final int DATA_6_COLUMN = 13;
-    public static final int DATA_7_COLUMN = 14;
-    public static final int DATA_8_COLUMN = 15;
-    public static final int DATA_9_COLUMN = 16;
-    public static final int DATA_10_COLUMN = 17;
+    public static final int AGGREGATE_PHOTO_ID = 2;
+    public static final int DATA_ID_COLUMN = 3;
+    public static final int DATA_CONTACT_ID_COLUMN = 4;
+    public static final int DATA_PACKAGE_COLUMN = 5;
+    public static final int DATA_MIMETYPE_COLUMN = 6;
+    public static final int DATA_IS_PRIMARY_COLUMN = 7;
+    public static final int DATA_IS_SUPER_PRIMARY_COLUMN = 8;
+    public static final int DATA_1_COLUMN = 9;
+    public static final int DATA_2_COLUMN = 10;
+    public static final int DATA_3_COLUMN = 11;
+    public static final int DATA_4_COLUMN = 12;
+    public static final int DATA_5_COLUMN = 13;
+    public static final int DATA_6_COLUMN = 14;
+    public static final int DATA_7_COLUMN = 15;
+    public static final int DATA_8_COLUMN = 16;
+    public static final int DATA_9_COLUMN = 17;
+    public static final int DATA_10_COLUMN = 18;
 
     protected ArrayList<ArrayList<E>> mSections;
     protected LayoutInflater mInflater;
diff --git a/src/com/android/contacts/ContactsListActivity.java b/src/com/android/contacts/ContactsListActivity.java
index 482f671..cf60b51 100644
--- a/src/com/android/contacts/ContactsListActivity.java
+++ b/src/com/android/contacts/ContactsListActivity.java
@@ -36,6 +36,8 @@
 import android.graphics.BitmapFactory;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
 import android.os.Parcelable;
 import android.preference.PreferenceManager;
 import android.provider.ContactsContract;
@@ -63,6 +65,8 @@
 import android.view.ViewGroup;
 import android.view.ContextMenu.ContextMenuInfo;
 import android.view.inputmethod.InputMethodManager;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AbsListView;
 import android.widget.AdapterView;
 import android.widget.AlphabetIndexer;
 import android.widget.Filter;
@@ -74,7 +78,7 @@
 
 import java.lang.ref.SoftReference;
 import java.lang.ref.WeakReference;
-import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.Locale;
 
 /*TODO(emillar) I commented most of the code that deals with modes and filtering. It should be
@@ -144,7 +148,7 @@
     /** Unknown mode */
     static final int MODE_UNKNOWN = 0;
     /** Default mode */
-    static final int MODE_DEFAULT = 4;
+    static final int MODE_DEFAULT = 4 | MODE_MASK_SHOW_PHOTOS;
     /** Custom mode */
     static final int MODE_CUSTOM = 8;
     /** Show all starred contacts */
@@ -154,7 +158,7 @@
     /** Show starred and the frequent */
     static final int MODE_STREQUENT = 35;
     /** Show all contacts and pick them when clicking */
-    static final int MODE_PICK_AGGREGATE = 40 | MODE_MASK_PICKER;
+    static final int MODE_PICK_AGGREGATE = 40 | MODE_MASK_PICKER | MODE_MASK_SHOW_PHOTOS;
     /** Show all contacts as well as the option to create a new one */
     static final int MODE_PICK_OR_CREATE_AGGREGATE = 42 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW;
     /** Show all contacts and pick them when clicking, and allow creating a new contact */
@@ -172,7 +176,7 @@
 
     /** Show join suggestions followed by an A-Z list */
     static final int MODE_JOIN_AGGREGATE = 70 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE
-            | MODE_MASK_NO_DATA;
+            | MODE_MASK_NO_DATA | MODE_MASK_SHOW_PHOTOS;
 
     /** Maximum number of suggestions shown for joining aggregates */
     static final int MAX_SUGGESTIONS = 4;
@@ -194,20 +198,22 @@
         Aggregates.STARRED, //2
         Aggregates.PRIMARY_PHONE_ID, //3
         Aggregates.TIMES_CONTACTED, //4
-        Presence.PRESENCE_STATUS, //5
-        CommonDataKinds.Phone.TYPE, //6
-        CommonDataKinds.Phone.LABEL, //7
-        CommonDataKinds.Phone.NUMBER, //8
+        Aggregates.PHOTO_ID, //5
+        Presence.PRESENCE_STATUS, //6
+        CommonDataKinds.Phone.TYPE, //7
+        CommonDataKinds.Phone.LABEL, //8
+        CommonDataKinds.Phone.NUMBER, //9
     };
     static final int ID_COLUMN_INDEX = 0;
     static final int SUMMARY_NAME_COLUMN_INDEX = 1;
     static final int SUMMARY_STARRED_COLUMN_INDEX = 2;
     static final int PRIMARY_PHONE_ID_COLUMN_INDEX = 3;
     static final int SUMMARY_TIMES_CONTACTED_COLUMN_INDEX = 4;
-    static final int SUMMARY_PRESENCE_STATUS_COLUMN_INDEX = 5;
-    static final int PRIMARY_PHONE_TYPE_COLUMN_INDEX = 6;
-    static final int PRIMARY_PHONE_LABEL_COLUMN_INDEX = 7;
-    static final int PRIMARY_PHONE_NUMBER_COLUMN_INDEX = 8;
+    static final int SUMMARY_PHOTO_ID_COLUMN_INDEX = 5;
+    static final int SUMMARY_PRESENCE_STATUS_COLUMN_INDEX = 6;
+    static final int PRIMARY_PHONE_TYPE_COLUMN_INDEX = 7;
+    static final int PRIMARY_PHONE_LABEL_COLUMN_INDEX = 8;
+    static final int PRIMARY_PHONE_NUMBER_COLUMN_INDEX = 9;
 
     static final String[] PHONES_PROJECTION = new String[] {
         Data._ID, //0
@@ -346,6 +352,7 @@
         } else if (Intent.ACTION_PICK.equals(action)) {
             // XXX These should be showing the data from the URI given in
             // the Intent.
+            mDisplayAll = true;
             final String type = intent.resolveType(this);
             if (Aggregates.CONTENT_TYPE.equals(type)) {
                 mMode = MODE_PICK_AGGREGATE;
@@ -465,6 +472,7 @@
 
         mAdapter = new ContactItemListAdapter(this);
         setListAdapter(mAdapter);
+        getListView().setOnScrollListener(mAdapter);
 
         // We manually save/restore the listview state
         list.setSaveEnabled(false);
@@ -1281,7 +1289,7 @@
     }
 
     private final class ContactItemListAdapter extends ResourceCursorAdapter
-            implements SectionIndexer {
+            implements SectionIndexer, OnScrollListener {
         private SectionIndexer mIndexer;
         private String mAlphabet;
         private boolean mLoading = true;
@@ -1290,15 +1298,20 @@
         private boolean mDisplayPhotos = false;
         private boolean mDisplayAdditionalData = true;
         private SparseArray<SoftReference<Bitmap>> mBitmapCache = null;
+        private HashSet<ImageView> mItemsMissingImages = null;
         private int mFrequentSeparatorPos = ListView.INVALID_POSITION;
         private boolean mDisplaySectionHeaders = true;
         private int[] mSectionPositions;
         private Cursor mSuggestionsCursor;
         private int mSuggestionsCursorCount;
+        private ImageFetchHandler mHandler;
+        private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE;
+        private static final int FETCH_IMAGE_MSG = 1;
 
         public ContactItemListAdapter(Context context) {
             super(context, R.layout.contacts_list_item, null, false);
 
+            mHandler = new ImageFetchHandler();
             mAlphabet = context.getString(com.android.internal.R.string.fast_scroll_alphabet);
 
             mUnknownNameText = context.getText(android.R.string.unknownName);
@@ -1335,6 +1348,7 @@
                 mDisplayPhotos = true;
                 setViewResource(R.layout.contacts_list_item_photo);
                 mBitmapCache = new SparseArray<SoftReference<Bitmap>>();
+                mItemsMissingImages = new HashSet<ImageView>();
             }
 
             if (mMode == MODE_STREQUENT || mMode == MODE_FREQUENT) {
@@ -1342,6 +1356,41 @@
             }
         }
 
+        private class ImageFetchHandler extends Handler {
+
+            @Override
+            public void handleMessage(Message message) {
+                switch(message.what) {
+                    case FETCH_IMAGE_MSG:
+                        ImageView imageView = (ImageView) message.obj;
+                        int pos = (Integer) imageView.getTag();
+                        Cursor cursor = (Cursor) getItem(pos);
+
+                        if (!cursor.isNull(SUMMARY_PHOTO_ID_COLUMN_INDEX)) {
+                            try {
+                                Bitmap photo = ContactsUtils.loadContactPhoto(
+                                        mContext, cursor.getInt(SUMMARY_PHOTO_ID_COLUMN_INDEX),
+                                        null);
+                                mBitmapCache.put(pos, new SoftReference<Bitmap>(photo));
+                                imageView.setImageBitmap(photo);
+                                mItemsMissingImages.remove(imageView);
+                            } catch (OutOfMemoryError e) {
+                                // Not enough memory for the photo, do nothing.
+                            }
+                        }
+
+                        if (imageView.getDrawable() == null) {
+                            imageView.setImageResource(R.drawable.ic_contact_list_picture);
+                        }
+                        break;
+                }
+            }
+
+            public void clearImageFecthing() {
+                removeMessages(FETCH_IMAGE_MSG);
+            }
+        }
+
         public void setSuggestionsCursor(Cursor cursor) {
             if (mSuggestionsCursor != null) {
                 mSuggestionsCursor.close();
@@ -1581,41 +1630,33 @@
             }
 
             // Set the photo, if requested
-            // TODO Either remove photos from this class completely or re-implement w/ asynchronous
-            // loading.
-            /*
             if (mDisplayPhotos) {
+                int pos = cursor.getPosition();
                 Bitmap photo = null;
+                cache.photoView.setImageBitmap(null);
+                cache.photoView.setTag(pos);
 
                 // Look for the cached bitmap
-                int pos = cursor.getPosition();
                 SoftReference<Bitmap> ref = mBitmapCache.get(pos);
                 if (ref != null) {
                     photo = ref.get();
                 }
 
-                if (photo == null) {
-                    // Bitmap cache miss, decode it from the cursor
-                    if (!cursor.isNull(PHOTO_COLUMN_INDEX)) {
-                        try {
-                            byte[] photoData = cursor.getBlob(PHOTO_COLUMN_INDEX);
-                            photo = BitmapFactory.decodeByteArray(photoData, 0,
-                                    photoData.length);
-                            mBitmapCache.put(pos, new SoftReference<Bitmap>(photo));
-                        } catch (OutOfMemoryError e) {
-                            // Not enough memory for the photo, use the default one instead
-                            photo = null;
-                        }
-                    }
-                }
-
                 // Bind the photo, or use the fallback no photo resource
                 if (photo != null) {
                     cache.photoView.setImageBitmap(photo);
                 } else {
+                    // Cache miss
                     cache.photoView.setImageResource(R.drawable.ic_contact_list_picture);
+                    if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
+                        // Scrolling is idle, go get the image right now.
+                        sendFetchImageMessage(cache.photoView);
+                    } else {
+                        // Add it to a set of images that will be populated when scrolling stops.
+                        mItemsMissingImages.add(cache.photoView);
+                    }
                 }
-            } */
+            }
         }
 
         private void bindSectionHeader(View view, int position, boolean displaySectionHeaders) {
@@ -1832,5 +1873,34 @@
             }
             return super.getItemId(getRealPosition(pos));
         }
+
+        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+                int totalItemCount) {
+            // no op
+        }
+
+        public void onScrollStateChanged(AbsListView view, int scrollState) {
+            mScrollState = scrollState;
+            if (scrollState != OnScrollListener.SCROLL_STATE_IDLE) {
+                // If we are not idle, stop loading images.
+                mHandler.clearImageFecthing();
+            } else if (mDisplayPhotos) {
+                processMissingImageItems(view);
+            }
+        }
+
+        private void processMissingImageItems(AbsListView view) {
+            for (ImageView iv : mItemsMissingImages) {
+                int pos = (Integer) iv.getTag();
+                sendFetchImageMessage(iv);
+            }
+        }
+
+        private void sendFetchImageMessage(ImageView view) {
+            Message msg = new Message();
+            msg.what = FETCH_IMAGE_MSG;
+            msg.obj = view;
+            mHandler.sendMessage(msg);
+        }
     }
 }
diff --git a/src/com/android/contacts/ContactsUtils.java b/src/com/android/contacts/ContactsUtils.java
index 0bba75c..74f9d38 100644
--- a/src/com/android/contacts/ContactsUtils.java
+++ b/src/com/android/contacts/ContactsUtils.java
@@ -23,6 +23,7 @@
 
 import android.net.Uri;
 import android.content.ContentResolver;
+import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
@@ -34,6 +35,7 @@
 import android.provider.ContactsContract.CommonDataKinds.Im;
 import android.provider.ContactsContract.CommonDataKinds.Organization;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
 import android.provider.ContactsContract.CommonDataKinds.Postal;
 import android.provider.Im.ProviderNames;
 import android.database.Cursor;
@@ -182,6 +184,31 @@
                 placeholderImageResource, options);
     }
 
+    public static Bitmap loadContactPhoto(Context context, int photoId,
+            BitmapFactory.Options options) {
+        Cursor photoCursor = null;
+        Bitmap photoBm = null;
+
+        try {
+            photoCursor = context.getContentResolver().query(
+                    ContentUris.withAppendedId(Data.CONTENT_URI, photoId),
+                    new String[] { Photo.PHOTO },
+                    null, null, null);
+
+            if (photoCursor.moveToFirst() && !photoCursor.isNull(0)) {
+                byte[] photoData = photoCursor.getBlob(0);
+                photoBm = BitmapFactory.decodeByteArray(photoData, 0,
+                        photoData.length, options);
+            }
+        } finally {
+            if (photoCursor != null) {
+                photoCursor.close();
+            }
+        }
+
+        return photoBm;
+    }
+
     /**
      * This looks up the provider name defined in
      * {@link android.provider.Im.ProviderNames} from the predefined IM protocol id.
diff --git a/src/com/android/contacts/ViewContactActivity.java b/src/com/android/contacts/ViewContactActivity.java
index 6414748..46cd26b 100644
--- a/src/com/android/contacts/ViewContactActivity.java
+++ b/src/com/android/contacts/ViewContactActivity.java
@@ -18,6 +18,7 @@
 
 import static com.android.contacts.ContactEntryAdapter.AGGREGATE_PROJECTION;
 import static com.android.contacts.ContactEntryAdapter.AGGREGATE_STARRED_COLUMN;
+import static com.android.contacts.ContactEntryAdapter.AGGREGATE_PHOTO_ID;
 import static com.android.contacts.ContactEntryAdapter.DATA_1_COLUMN;
 import static com.android.contacts.ContactEntryAdapter.DATA_2_COLUMN;
 import static com.android.contacts.ContactEntryAdapter.DATA_3_COLUMN;
@@ -287,6 +288,15 @@
             // Set the star
             mStarView.setChecked(mCursor.getInt(AGGREGATE_STARRED_COLUMN) == 1 ? true : false);
 
+            //Set the photo
+            int photoId = mCursor.getInt(AGGREGATE_PHOTO_ID);
+            Bitmap photoBitmap = ContactsUtils.loadContactPhoto(
+                    this, photoId, null);
+            if (photoBitmap == null) {
+                photoBitmap = ContactsUtils.loadPlaceholderPhoto(mNoPhotoResource, this, null);
+            }
+            mPhotoView.setImageBitmap(photoBitmap);
+
             // Build up the contact entries
             buildEntries(mCursor);
 
@@ -871,9 +881,6 @@
                     }
 
                     mOtherEntries.add(entry);
-                // Load the photo
-                } else if (mimetype.equals(CommonDataKinds.Photo.CONTENT_ITEM_TYPE)) {
-                    photoBitmap = ContactsUtils.loadContactPhoto(aggCursor, DATA_1_COLUMN, null);
                 }
 
 
@@ -921,11 +928,6 @@
 //              }
 
             }
-
-            if (photoBitmap == null) {
-                photoBitmap = ContactsUtils.loadPlaceholderPhoto(mNoPhotoResource, this, null);
-            }
-            mPhotoView.setImageBitmap(photoBitmap);
         }
     }