Show group photos asynchronously
Bug: 4689452
Change-Id: If559d698f673849e7f65ad7b7eccdcec94a998d7
diff --git a/res/layout/group_browse_list_item.xml b/res/layout/group_browse_list_item.xml
index ecdc132..b9b272c 100644
--- a/res/layout/group_browse_list_item.xml
+++ b/res/layout/group_browse_list_item.xml
@@ -19,6 +19,8 @@
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:minHeight="@dimen/detail_min_line_item_height"
+ android:paddingRight="20dip"
android:paddingBottom="10dip"
style="@style/GroupBrowseListItem">
@@ -37,25 +39,76 @@
layout="@layout/group_browse_list_account_header"
android:visibility="gone" />
- <TextView
- android:id="@+id/label"
- android:layout_height="wrap_content"
- android:layout_width="wrap_content"
- android:paddingLeft="10dip"
- android:paddingRight="10dip"
- android:textAppearance="?android:attr/textAppearanceMedium"
- android:ellipsize="end"
- android:singleLine="true" />
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
- <TextView
- android:id="@+id/count"
- android:layout_height="wrap_content"
- android:layout_width="wrap_content"
- android:paddingLeft="10dip"
- android:paddingRight="10dip"
- android:textAppearance="?android:attr/textAppearanceSmall"
- android:textColor="?android:attr/textColorTertiary"
- android:ellipsize="end"
- android:singleLine="true" />
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_toLeftOf="@+id/icons"
+ android:layout_alignParentLeft="true"
+ android:layout_centerVertical="true">
+ <TextView
+ android:id="@+id/label"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:paddingLeft="10dip"
+ android:paddingRight="10dip"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:ellipsize="end"
+ android:singleLine="true" />
+
+ <TextView
+ android:id="@+id/count"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:paddingLeft="10dip"
+ android:paddingRight="10dip"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:attr/textColorTertiary"
+ android:ellipsize="end"
+ android:singleLine="true" />
+
+ </LinearLayout>
+
+ <TableLayout
+ android:id="@+id/icons"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true">
+ <TableRow
+ android:layout_marginBottom="1dip">
+ <ImageView
+ android:id="@+id/icon_1"
+ android:layout_width="@dimen/group_list_icon_size"
+ android:layout_height="@dimen/group_list_icon_size"
+ android:layout_marginRight="1dip"
+ android:src="@drawable/ic_contact_picture" />
+ <ImageView
+ android:id="@+id/icon_2"
+ android:layout_width="@dimen/group_list_icon_size"
+ android:layout_height="@dimen/group_list_icon_size"
+ android:src="@drawable/ic_contact_picture" />
+ </TableRow>
+ <TableRow>
+ <ImageView
+ android:id="@+id/icon_3"
+ android:layout_width="@dimen/group_list_icon_size"
+ android:layout_height="@dimen/group_list_icon_size"
+ android:layout_marginRight="1dip"
+ android:src="@drawable/ic_contact_picture" />
+ <ImageView
+ android:id="@+id/icon_4"
+ android:layout_width="@dimen/group_list_icon_size"
+ android:layout_height="@dimen/group_list_icon_size"
+ android:src="@drawable/ic_contact_picture" />
+ </TableRow>
+
+ </TableLayout>
+ </RelativeLayout>
</LinearLayout>
+
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 569f00b..a4a6112 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -173,6 +173,9 @@
<!-- Border padding for the group list header for each account -->
<dimen name="group_list_header_padding">5dip</dimen>
+ <!-- Size of group list icons -->
+ <dimen name="group_list_icon_size">32dip</dimen>
+
<!-- Border padding for the group detail fragment header -->
<dimen name="group_detail_border_padding">20dip</dimen>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index e267a19..6f1e84b 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -248,7 +248,6 @@
</style>
<style name="GroupBrowseListItem">
- <item name="android:paddingRight">20dip</item>
</style>
<style name="DialtactsDigitsTextAppearance">
diff --git a/src/com/android/contacts/group/GroupBrowseListAdapter.java b/src/com/android/contacts/group/GroupBrowseListAdapter.java
index 630a397..1f06029 100644
--- a/src/com/android/contacts/group/GroupBrowseListAdapter.java
+++ b/src/com/android/contacts/group/GroupBrowseListAdapter.java
@@ -16,33 +16,159 @@
package com.android.contacts.group;
+import com.android.contacts.ContactPhotoManager;
import com.android.contacts.GroupListLoader;
import com.android.contacts.R;
import com.android.contacts.model.AccountType;
import com.android.contacts.model.AccountTypeManager;
-import com.android.contacts.model.AccountWithDataSet;
import com.android.internal.util.Objects;
+import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Groups;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
+import android.widget.ImageView;
import android.widget.TextView;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
/**
* Adapter to populate the list of groups.
*/
public class GroupBrowseListAdapter extends BaseAdapter {
+ private static final int MAX_ICONS_PER_GROUP_ROW = 4;
+
+ private static final String[] PROJECTION_GROUP_MEMBERSHIP_INFO = new String[] {
+ GroupMembership._ID,
+ GroupMembership.PHOTO_ID
+ };
+ private static final int GROUP_MEMBERSHIP_COLUMN_PHOTO_ID = 1;
+
+ /**
+ * Arguments for asynchronous photo ID loading. See {@link AsyncPhotoIdLoadTask}
+ */
+ private static class AsyncPhotoIdLoadArg {
+ public final View icons;
+ public final long groupId;
+ public final Map<Long, ArrayList<Long>> groupPhotoIdMap;
+ public final ContentResolver contentResolver;
+ public final ContactPhotoManager contactPhotoManager;
+
+ public AsyncPhotoIdLoadArg(
+ View icons, long groupId, Map<Long, ArrayList<Long>> groupPhotoIdMap,
+ ContentResolver contentResolver, ContactPhotoManager contactPhotoManager) {
+ this.icons = icons;
+ this.groupId = groupId;
+ this.groupPhotoIdMap = groupPhotoIdMap;
+ this.contentResolver = contentResolver;
+ this.contactPhotoManager = contactPhotoManager;
+ }
+ }
+
+ /**
+ * Loads photo IDs associated with a group ID supplied from {@link AsyncPhotoIdLoadArg#groupId},
+ * storing them in {@link GroupBrowseListAdapter#mGroupPhotoIdMap}.
+ *
+ * This AsyncTask also remembers a View which is associated with the group ID at the moment it
+ * is initiated (we use {@link View#setTag(Object) and View#getTag() to associate them}. If the
+ * View is still associated with the group ID after the asynchronous photo ID load, this class
+ * also asks {@link ContactPhotoManager} to load actual photo contents. Its parent (typically
+ * ListView) may reuse Views for different group IDs, so the photo content load often don't
+ * occur.
+ */
+ private static class AsyncPhotoIdLoadTask extends
+ AsyncTask<AsyncPhotoIdLoadArg, Void, ArrayList<Long>> {
+
+ private View mIcons;
+ private long mGroupId;
+ private Map<Long, ArrayList<Long>> mGroupPhotoIdMap;
+ private ContentResolver mContentResolver;
+ private ContactPhotoManager mContactPhotoManager;
+
+ @Override
+ protected ArrayList<Long> doInBackground(AsyncPhotoIdLoadArg... params) {
+ final AsyncPhotoIdLoadArg arg = params[0];
+ mIcons = arg.icons;
+ mGroupId = arg.groupId;
+ mGroupPhotoIdMap = arg.groupPhotoIdMap;
+ mContentResolver = arg.contentResolver;
+ mContactPhotoManager = arg.contactPhotoManager;
+
+ // Multiple requests for one group ID is possible. We just ignore duplicates,
+ // assuming query results won't change.
+ if (mGroupPhotoIdMap.containsKey(mGroupId)) {
+ return null;
+ }
+
+ final ArrayList<Long> photoIds = new ArrayList<Long>(MAX_ICONS_PER_GROUP_ROW);
+ Cursor cursor = null;
+ try {
+ cursor = mContentResolver.query(Data.CONTENT_URI,
+ PROJECTION_GROUP_MEMBERSHIP_INFO,
+ GroupMembership.MIMETYPE + "=? AND "
+ + GroupMembership.PHOTO_ID + " IS NOT NULL AND "
+ + GroupMembership.GROUP_ROW_ID + "=?",
+ new String[] { GroupMembership.CONTENT_ITEM_TYPE,
+ String.valueOf(mGroupId) }, null);
+ if (cursor != null) {
+ int count = 0;
+ while (cursor.moveToNext() && count < MAX_ICONS_PER_GROUP_ROW) {
+ photoIds.add(cursor.getLong(GROUP_MEMBERSHIP_COLUMN_PHOTO_ID));
+ count++;
+ }
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ return photoIds;
+ }
+
+ @Override
+ protected void onPostExecute(ArrayList<Long> photoIds) {
+ if (photoIds == null) {
+ return;
+ }
+
+ mGroupPhotoIdMap.put(mGroupId, photoIds);
+
+ final View icons = mIcons;
+ // If the original group ID, which was supplied when this AsyncTask was executed, is
+ // consistent with the ID inside mArgs, it means the View isn't reused by the
+ // other groups, and thus we can assume these Views are available for the group ID.
+ final Long currentGroupId = (Long) icons.getTag();
+ if (currentGroupId == mGroupId) {
+ final ImageView[] children = getIconViewsSordedByFillOrder(icons);
+ for (int i = 0; i < children.length; i++) {
+ final long photoId = i < photoIds.size() ? photoIds.get(i) : 0;
+ mContactPhotoManager.loadPhoto(children[i], photoId);
+ }
+ }
+ }
+ }
+
private final Context mContext;
private final LayoutInflater mLayoutInflater;
private final AccountTypeManager mAccountTypeManager;
+ private final Map<Long, ArrayList<Long>> mGroupPhotoIdMap =
+ new ConcurrentHashMap<Long, ArrayList<Long>>();
+
+ private final ContactPhotoManager mContactPhotoManager;
+
private Cursor mCursor;
private boolean mSelectionVisible;
@@ -52,6 +178,7 @@
mContext = context;
mLayoutInflater = LayoutInflater.from(context);
mAccountTypeManager = AccountTypeManager.getInstance(mContext);
+ mContactPhotoManager = ContactPhotoManager.getInstance(mContext);
}
public void setCursor(Cursor cursor) {
@@ -179,6 +306,32 @@
viewCache.groupTitle.setText(entry.getTitle());
viewCache.groupMemberCount.setText(memberCountString);
+ final View icons = result.findViewById(R.id.icons);
+ final ImageView[] children = getIconViewsSordedByFillOrder(icons);
+ final ArrayList<Long> photoIds = mGroupPhotoIdMap.get(entry.getGroupId());
+
+ // Let the icon holder remember its associated group ID.
+ // Each AsyncTask loading photo IDs will compare this ID with the AsyncTask's argument, and
+ // check if the bound View is reused by the other list items or not. If the View is reused,
+ // the group ID set here will be overridden by the new owner, thus ID inconsistency happens.
+ icons.setTag(entry.getGroupId());
+ if (photoIds != null) {
+ // Cache is available. Let the photo manager load those IDs.
+ for (int i = 0; i < children.length; i++) {
+ final long photoId = i < photoIds.size() ? photoIds.get(i) : 0;
+ mContactPhotoManager.loadPhoto(children[i], photoId);
+ }
+ } else {
+ // Cache is not available. Load photo IDs asynchronously.
+ for (ImageView child : children) {
+ mContactPhotoManager.loadPhoto(child, 0);
+ }
+ new AsyncPhotoIdLoadTask().execute(
+ new AsyncPhotoIdLoadArg(icons, entry.getGroupId(),
+ mGroupPhotoIdMap, mContext.getContentResolver(),
+ mContactPhotoManager));
+ }
+
if (mSelectionVisible) {
result.setActivated(isSelectedGroup(groupUri));
}
@@ -201,6 +354,19 @@
}
/**
+ * Get ImageView objects inside the given View, sorted by the order photos should be filled.
+ */
+ private static ImageView[] getIconViewsSordedByFillOrder(View icons) {
+ final ImageView[] children = new ImageView[] {
+ (ImageView) icons.findViewById(R.id.icon_4),
+ (ImageView) icons.findViewById(R.id.icon_2),
+ (ImageView) icons.findViewById(R.id.icon_3),
+ (ImageView) icons.findViewById(R.id.icon_1)
+ };
+ return children;
+ }
+
+ /**
* Cache of the children views of a contact detail entry represented by a
* {@link GroupListItem}
*/