Merge "Show distinct group member aggregate contacts"
diff --git a/src/com/android/contacts/activities/GroupMembersActivity.java b/src/com/android/contacts/activities/GroupMembersActivity.java
index 01999bb..7ee8653 100644
--- a/src/com/android/contacts/activities/GroupMembersActivity.java
+++ b/src/com/android/contacts/activities/GroupMembersActivity.java
@@ -17,10 +17,8 @@
 
 import android.accounts.Account;
 import android.app.FragmentManager;
-import android.content.Context;
 import android.app.FragmentTransaction;
-import android.app.LoaderManager.LoaderCallbacks;
-import android.content.CursorLoader;
+import android.content.Context;
 import android.content.Intent;
 import android.database.Cursor;
 import android.net.Uri;
@@ -30,7 +28,6 @@
 import android.provider.ContactsContract.Intents;
 import android.provider.ContactsContract.RawContacts;
 import android.support.v4.view.GravityCompat;
-import android.support.v7.widget.Toolbar;
 import android.util.Log;
 import android.view.Menu;
 import android.view.MenuItem;
@@ -38,8 +35,6 @@
 
 import com.android.contacts.ContactSaveService;
 import com.android.contacts.ContactsDrawerActivity;
-import com.android.contacts.GroupMemberLoader;
-import com.android.contacts.GroupMemberLoader.GroupEditorQuery;
 import com.android.contacts.R;
 import com.android.contacts.common.editor.SelectAccountDialogFragment;
 import com.android.contacts.common.logging.ListEvent;
@@ -49,7 +44,6 @@
 import com.android.contacts.common.model.account.AccountWithDataSet;
 import com.android.contacts.common.util.AccountsListAdapter.AccountListFilter;
 import com.android.contacts.common.util.ImplicitIntentsUtil;
-import com.android.contacts.group.GroupMembersListAdapter.GroupMembersQuery;
 import com.android.contacts.group.GroupMembersListFragment;
 import com.android.contacts.group.GroupMetadata;
 import com.android.contacts.group.GroupNameEditDialogFragment;
@@ -59,7 +53,6 @@
 import com.android.contacts.list.UiIntentActions;
 import com.android.contacts.quickcontact.QuickContactActivity;
 
-import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -355,7 +348,7 @@
 
     @Override
     public boolean onCreateOptionsMenu(Menu menu) {
-        if (mGroupMetadata == null || mGroupMetadata.memberCount < 0) {
+        if (mGroupMetadata == null) {
             // Hide menu options until metadata is fully loaded
             return false;
         }
@@ -399,7 +392,7 @@
                 intent.putExtra(UiIntentActions.GROUP_ACCOUNT_TYPE, mGroupMetadata.accountType);
                 intent.putExtra(UiIntentActions.GROUP_ACCOUNT_DATA_SET, mGroupMetadata.dataSet);
                 intent.putExtra(UiIntentActions.GROUP_CONTACT_IDS,
-                        getExistingGroupMemberContactIds());
+                        mMembersListFragment.getMemberContactIds());
                 startActivityForResult(intent, RESULT_GROUP_ADD_MEMBER);
                 return true;
             }
@@ -424,19 +417,8 @@
         return super.onOptionsItemSelected(item);
     }
 
-    private ArrayList<String> getExistingGroupMemberContactIds() {
-        final ArrayList<String> contactIds = new ArrayList<>();
-        final Cursor cursor = mMembersListFragment.getAdapter().getCursor(/* partition */ 0);
-        if (cursor != null && cursor.moveToFirst()) {
-            do {
-                contactIds.add(cursor.getString(GroupMembersQuery.CONTACT_ID));
-            } while (cursor.moveToNext());
-        }
-        return contactIds;
-    }
-
     private void deleteGroup() {
-        if (mGroupMetadata.memberCount == 0) {
+        if (mMembersListFragment.getMemberCount() == 0) {
             final Intent intent = ContactSaveService.createGroupDeletionIntent(
                     this, mGroupMetadata.groupId,
                     GroupMembersActivity.class, ACTION_DELETE_GROUP);
diff --git a/src/com/android/contacts/group/GroupMembersListFragment.java b/src/com/android/contacts/group/GroupMembersListFragment.java
index 47eed0c..056e251 100644
--- a/src/com/android/contacts/group/GroupMembersListFragment.java
+++ b/src/com/android/contacts/group/GroupMembersListFragment.java
@@ -19,25 +19,30 @@
 import android.content.CursorLoader;
 import android.content.Loader;
 import android.database.Cursor;
+import android.database.CursorWrapper;
 import android.net.Uri;
 import android.os.Bundle;
-import android.provider.ContactsContract;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.TextView;
 
-import com.android.contacts.GroupListLoader;
 import com.android.contacts.GroupMetaDataLoader;
 import com.android.contacts.R;
 import com.android.contacts.common.logging.ListEvent.ListType;
 import com.android.contacts.common.model.AccountTypeManager;
 import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.group.GroupMembersListAdapter.GroupMembersQuery;
 import com.android.contacts.list.MultiSelectContactsListFragment;
 
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
 /** Displays the members of a group. */
-public class GroupMembersListFragment extends MultiSelectContactsListFragment {
+public class GroupMembersListFragment extends
+        MultiSelectContactsListFragment<GroupMembersListAdapter> {
 
     private static final String TAG = "GroupMembers";
 
@@ -47,7 +52,6 @@
     private static final String ARG_GROUP_URI = "groupUri";
 
     private static final int LOADER_GROUP_METADATA = 0;
-    private static final int LOADER_GROUP_LIST_DETAILS = 1;
 
     /** Callbacks for hosts of {@link GroupMembersListFragment}. */
     public interface GroupMembersListListener {
@@ -62,7 +66,83 @@
         void onGroupMemberListItemClicked(int position, Uri contactLookupUri);
     }
 
-    /** Step 1 of loading group metadata. */
+    /** Filters out duplicate contacts. */
+    private class FilterCursorWrapper extends CursorWrapper {
+
+        private int[] mIndex;
+        private int mCount = 0;
+        private int mPos = 0;
+
+        public FilterCursorWrapper(Cursor cursor) {
+            super(cursor);
+
+            mCount = super.getCount();
+            mIndex = new int[mCount];
+
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "Group members CursorWrapper start: " + mCount);
+            }
+
+            mGroupMemberContactIds.clear();
+            for (int i = 0; i < mCount; i++) {
+                super.moveToPosition(i);
+                final String contactId = getString(GroupMembersQuery.CONTACT_ID);
+                if (!mGroupMemberContactIds.contains(contactId)) {
+                    mIndex[mPos++] = i;
+                    mGroupMemberContactIds.add(contactId);
+                }
+            }
+            mCount = mPos;
+            mPos = 0;
+            super.moveToFirst();
+
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "Group members CursorWrapper end: " + mCount);
+            }
+        }
+
+        @Override
+        public boolean move(int offset) {
+            return moveToPosition(mPos + offset);
+        }
+
+        @Override
+        public boolean moveToNext() {
+            return moveToPosition(mPos + 1);
+        }
+
+        @Override
+        public boolean moveToPrevious() {
+            return moveToPosition(mPos - 1);
+        }
+
+        @Override
+        public boolean moveToFirst() {
+            return moveToPosition(0);
+        }
+
+        @Override
+        public boolean moveToLast() {
+            return moveToPosition(mCount - 1);
+        }
+
+        @Override
+        public boolean moveToPosition(int position) {
+            if (position >= mCount || position < 0) return false;
+            return super.moveToPosition(mIndex[position]);
+        }
+
+        @Override
+        public int getCount() {
+            return mCount;
+        }
+
+        @Override
+        public int getPosition() {
+            return mPos;
+        }
+    }
+
     private final LoaderCallbacks<Cursor> mGroupMetadataCallbacks = new LoaderCallbacks<Cursor>() {
 
         @Override
@@ -94,39 +174,6 @@
                     mGroupMetadata.accountType, mGroupMetadata.dataSet);
             mGroupMetadata.editable = accountType.isGroupMembershipEditable();
 
-            getLoaderManager().restartLoader(LOADER_GROUP_LIST_DETAILS, null, mGroupListCallbacks);
-        }
-
-        @Override
-        public void onLoaderReset(Loader<Cursor> loader) {}
-    };
-
-    /** Step 2 of loading group metadata. */
-    private final LoaderCallbacks<Cursor> mGroupListCallbacks = new LoaderCallbacks<Cursor>() {
-
-        @Override
-        public CursorLoader onCreateLoader(int id, Bundle args) {
-            final GroupListLoader groupListLoader = new GroupListLoader(getActivity());
-
-            groupListLoader.setSelection(GroupListLoader.DEFAULT_SELECTION
-                    + " AND " + ContactsContract.Groups._ID + "=?");
-
-            final String[] selectionArgs = new String[1];
-            selectionArgs[0] = Long.toString(mGroupMetadata.groupId);
-            groupListLoader.setSelectionArgs(selectionArgs);
-
-            return groupListLoader;
-        }
-
-        @Override
-        public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
-            if (cursor == null || cursor.isClosed()) {
-                Log.e(TAG, "Failed to load group list details");
-                return;
-            }
-            if (cursor.moveToNext()) {
-                mGroupMetadata.memberCount = cursor.getInt(GroupListLoader.MEMBER_COUNT);
-            }
             onGroupMetadataLoaded();
         }
 
@@ -140,6 +187,8 @@
 
     private GroupMetadata mGroupMetadata;
 
+    private Set<String> mGroupMemberContactIds = new HashSet();
+
     public static GroupMembersListFragment newInstance(Uri groupUri) {
         final Bundle args = new Bundle();
         args.putParcelable(ARG_GROUP_URI, groupUri);
@@ -164,6 +213,14 @@
         mListener = listener;
     }
 
+    public ArrayList<String> getMemberContactIds() {
+        return  new ArrayList<>(mGroupMemberContactIds);
+    }
+
+    public int getMemberCount() {
+        return mGroupMemberContactIds.size();
+    }
+
     @Override
     public void onCreate(Bundle savedState) {
         super.onCreate(savedState);
@@ -185,6 +242,30 @@
     }
 
     @Override
+    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+        if (data != null) {
+            final FilterCursorWrapper cursorWrapper = new FilterCursorWrapper(data);
+            bindMembersCount(cursorWrapper.getCount());
+            super.onLoadFinished(loader, cursorWrapper);
+        }
+    }
+
+    private void bindMembersCount(int memberCount) {
+        final View accountFilterContainer = getView().findViewById(
+                R.id.account_filter_header_container);
+        if (memberCount >= 0) {
+            accountFilterContainer.setVisibility(View.VISIBLE);
+
+            final TextView accountFilterHeader = (TextView) accountFilterContainer.findViewById(
+                    R.id.account_filter_header);
+            accountFilterHeader.setText(getResources().getQuantityString(
+                    R.plurals.group_members_count, memberCount, memberCount));
+        } else {
+            accountFilterContainer.setVisibility(View.GONE);
+        }
+    }
+
+    @Override
     public void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
         outState.putParcelable(KEY_GROUP_URI, mGroupUri);
@@ -196,21 +277,6 @@
 
         maybeAttachCheckBoxListener();
 
-        // Bind the members count
-        final View accountFilterContainer = getView().findViewById(
-                R.id.account_filter_header_container);
-        if (mGroupMetadata.memberCount >= 0) {
-            accountFilterContainer.setVisibility(View.VISIBLE);
-
-            final TextView accountFilterHeader = (TextView) accountFilterContainer.findViewById(
-                    R.id.account_filter_header);
-            accountFilterHeader.setText(getResources().getQuantityString(
-                    R.plurals.group_members_count, mGroupMetadata.memberCount,
-                    mGroupMetadata.memberCount));
-        } else {
-            accountFilterContainer.setVisibility(View.GONE);
-        }
-
         if (mListener != null) {
             mListener.onGroupMetadataLoaded(mGroupMetadata);
         }
@@ -239,11 +305,6 @@
     }
 
     @Override
-    public GroupMembersListAdapter getAdapter() {
-        return (GroupMembersListAdapter) super.getAdapter();
-    }
-
-    @Override
     protected void configureAdapter() {
         super.configureAdapter();
         if (mGroupMetadata != null) {
diff --git a/src/com/android/contacts/group/GroupMetadata.java b/src/com/android/contacts/group/GroupMetadata.java
index fcf5dc2..712500a 100644
--- a/src/com/android/contacts/group/GroupMetadata.java
+++ b/src/com/android/contacts/group/GroupMetadata.java
@@ -46,7 +46,6 @@
     public String groupName;
     public boolean readOnly;
     public boolean editable;
-    public int memberCount = -1;
 
     public GroupMetadata() {
     }
@@ -64,7 +63,6 @@
         groupName = source.readString();
         readOnly = source.readInt() == 1;
         editable = source.readInt() == 1;
-        memberCount = source.readInt();
     }
 
     @Override
@@ -77,7 +75,6 @@
         dest.writeString(groupName);
         dest.writeInt(readOnly ? 1 : 0);
         dest.writeInt(editable ? 1 : 0);
-        dest.writeInt(memberCount);
     }
 
     /** Whether all metadata fields are set. */
@@ -85,8 +82,7 @@
         return uri != null
                 && !TextUtils.isEmpty(accountName)
                 && !TextUtils.isEmpty(groupName)
-                && groupId > 0
-                && memberCount >= 0;
+                && groupId > 0;
     }
 
     public AccountWithDataSet createAccountWithDataSet() {
@@ -114,7 +110,6 @@
                 " groupName=" + groupName +
                 " readOnly=" + readOnly +
                 " editable=" + editable +
-                " memberCount=" + memberCount +
                 " isValid=" + isValid() +
                 "]";
     }
diff --git a/src/com/android/contacts/list/GroupMemberPickerFragment.java b/src/com/android/contacts/list/GroupMemberPickerFragment.java
index 605758f..de856a4 100644
--- a/src/com/android/contacts/list/GroupMemberPickerFragment.java
+++ b/src/com/android/contacts/list/GroupMemberPickerFragment.java
@@ -57,10 +57,7 @@
         void onGroupMemberClicked(long contactId);
     }
 
-    /**
-     * Filters out raw contacts that are already in the group and also handles queries for contact
-     * photo IDs and lookup keys which cannot be retrieved from the raw contact table directly.
-     */
+    /** Filters out raw contacts that are already in the group. */
     private class FilterCursorWrapper extends CursorWrapper {
 
         private int[] mIndex;
@@ -74,7 +71,7 @@
             mIndex = new int[mCount];
 
             if (Log.isLoggable(TAG, Log.VERBOSE)) {
-                Log.v(TAG, "FilterCursorWrapper starting cursor size is " + mCount);
+                Log.v(TAG, "RawContacts CursorWrapper start: " + mCount);
             }
 
             for (int i = 0; i < mCount; i++) {
@@ -89,7 +86,7 @@
             super.moveToFirst();
 
             if (Log.isLoggable(TAG, Log.VERBOSE)) {
-                Log.v(TAG, "FilterCursorWrapper ending cursor size is" + mCount);
+                Log.v(TAG, "RawContacts CursorWrapper end: " + mCount);
             }
         }