Add group member selection to ContentSelectionActivity (1/2)
* The GroupMemberPickListAdapter queries RawContacts to get
the list of potential group members
* The raw contact IDs of existing group members are passed
in to the pick fragment and these are filtered out of
results by a CursorWrapper.
* Since we can't get the contact photo ID and contact lookup
URI from the RawContacts query, these are also provided
by the CursorWrapper.
* Finally, we move the current search to add contact to group
functionality in place behind the ActionBar search icon,
moved to the overflow menu.
Bug 28707265
Bug 18641067
Change-Id: Ifde5446e8ce9c0ed27cd2f98fd704ca669c45f59
diff --git a/res/menu/view_group.xml b/res/menu/view_group.xml
index cf945fd..2fc36be 100644
--- a/res/menu/view_group.xml
+++ b/res/menu/view_group.xml
@@ -24,6 +24,11 @@
contacts:showAsAction="ifRoom" />
<item
+ android:id="@+id/menu_search"
+ android:icon="@drawable/ic_ab_search"
+ android:title="@string/menu_search" />
+
+ <item
android:id="@+id/menu_rename_group"
android:title="@string/menu_renameGroup"/>
diff --git a/src/com/android/contacts/GroupMemberLoader.java b/src/com/android/contacts/GroupMemberLoader.java
index 43a6427..9f55848 100644
--- a/src/com/android/contacts/GroupMemberLoader.java
+++ b/src/com/android/contacts/GroupMemberLoader.java
@@ -80,14 +80,6 @@
return new GroupMemberLoader(context, groupId, GroupEditorQuery.PROJECTION);
}
- /**
- * @return GroupMemberLoader object used in group detail page.
- */
- public static GroupMemberLoader constructLoaderForGroupDetailQuery(
- Context context, long groupId) {
- return new GroupMemberLoader(context, groupId, GroupDetailQuery.PROJECTION);
- }
-
private GroupMemberLoader(Context context, long groupId, String[] projection) {
super(context);
mGroupId = groupId;
diff --git a/src/com/android/contacts/activities/ContactSelectionActivity.java b/src/com/android/contacts/activities/ContactSelectionActivity.java
index 50f50dd..80a4acb 100644
--- a/src/com/android/contacts/activities/ContactSelectionActivity.java
+++ b/src/com/android/contacts/activities/ContactSelectionActivity.java
@@ -18,7 +18,6 @@
import android.app.ActionBar;
import android.app.ActionBar.LayoutParams;
-import android.app.Activity;
import android.app.Fragment;
import android.content.ActivityNotFoundException;
import android.content.Context;
@@ -26,7 +25,6 @@
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.Intents.Insert;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
@@ -46,6 +44,8 @@
import com.android.contacts.R;
import com.android.contacts.common.activity.RequestPermissionsActivity;
import com.android.contacts.common.list.ContactEntryListFragment;
+import com.android.contacts.list.GroupMemberPickerFragment;
+import com.android.contacts.common.model.account.AccountWithDataSet;
import com.android.contacts.editor.EditorIntents;
import com.android.contacts.list.ContactPickerFragment;
import com.android.contacts.list.ContactsIntentResolver;
@@ -62,7 +62,7 @@
import com.android.contacts.common.list.PhoneNumberPickerFragment;
import com.android.contacts.list.PostalAddressPickerFragment;
-import java.util.Set;
+import java.util.ArrayList;
/**
* Displays a list of contacts (or phone numbers or postal addresses) for the
@@ -139,9 +139,10 @@
.inflate(R.layout.custom_action_bar, null);
mSearchView = (SearchView) mSearchViewContainer.findViewById(R.id.search_view);
- // Postal address pickers (and legacy pickers) don't support search, so just show
+ // Postal address group member,and legacy pickers don't support search, so just show
// "HomeAsUp" button and title.
if (mRequest.getActionCode() == ContactsRequest.ACTION_PICK_POSTAL ||
+ mRequest.getActionCode() == ContactsRequest.ACTION_PICK_GROUP_MEMBERS ||
mRequest.isLegacyCompatibilityMode()) {
mSearchView.setVisibility(View.GONE);
if (actionBar != null) {
@@ -270,6 +271,11 @@
setTitle(R.string.titleJoinContactDataWith);
break;
}
+
+ case ContactsRequest.ACTION_PICK_GROUP_MEMBERS: {
+ setTitle(R.string.contactPickerActivityTitle);
+ break;
+ }
}
}
@@ -350,6 +356,15 @@
break;
}
+ case ContactsRequest.ACTION_PICK_GROUP_MEMBERS: {
+ final AccountWithDataSet account = getIntent().getParcelableExtra(
+ UiIntentActions.GROUP_ACCOUNT_WITH_DATA_SET);
+ final ArrayList<String> rawContactIds = getIntent().getStringArrayListExtra(
+ UiIntentActions.GROUP_RAW_CONTACT_IDS);
+ mListFragment = GroupMemberPickerFragment.newInstance(account, rawContactIds);
+ break;
+ }
+
default:
throw new IllegalStateException("Invalid action code: " + mActionCode);
}
@@ -389,6 +404,9 @@
} else if (mListFragment instanceof JoinContactListFragment) {
((JoinContactListFragment) mListFragment).setOnContactPickerActionListener(
new JoinContactActionListener());
+ } else if (mListFragment instanceof GroupMemberPickerFragment) {
+ ((GroupMemberPickerFragment) mListFragment).setListener(
+ new GroupMemberPickerListener());
} else {
throw new IllegalStateException("Unsupported list fragment type: " + mListFragment);
}
@@ -462,6 +480,14 @@
}
}
+ private final class GroupMemberPickerListener implements GroupMemberPickerFragment.Listener {
+
+ @Override
+ public void onGroupMemberClicked(Uri uri) {
+ returnPickerResult(uri);
+ }
+ }
+
private final class PostalAddressPickerActionListener implements
OnPostalAddressPickerActionListener {
@Override
diff --git a/src/com/android/contacts/activities/GroupMembersActivity.java b/src/com/android/contacts/activities/GroupMembersActivity.java
index c8d2c19..f41972e 100644
--- a/src/com/android/contacts/activities/GroupMembersActivity.java
+++ b/src/com/android/contacts/activities/GroupMembersActivity.java
@@ -24,6 +24,7 @@
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
+import android.provider.ContactsContract;
import android.provider.ContactsContract.Intents;
import android.support.v7.widget.Toolbar;
import android.util.Log;
@@ -41,13 +42,14 @@
import com.android.contacts.GroupMemberLoader.GroupEditorQuery;
import com.android.contacts.R;
import com.android.contacts.common.editor.SelectAccountDialogFragment;
-import com.android.contacts.common.logging.Logger;
import com.android.contacts.common.logging.ListEvent;
+import com.android.contacts.common.logging.Logger;
import com.android.contacts.common.logging.ScreenEvent.ScreenType;
import com.android.contacts.common.model.AccountTypeManager;
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;
@@ -57,6 +59,7 @@
import com.android.contacts.interactions.GroupDeletionDialogFragment;
import com.android.contacts.list.ContactsRequest;
import com.android.contacts.list.MultiSelectContactsListFragment;
+import com.android.contacts.list.UiIntentActions;
import com.android.contacts.quickcontact.QuickContactActivity;
import java.util.ArrayList;
@@ -90,6 +93,8 @@
private static final String ACTION_ADD_TO_GROUP = "addToGroup";
private static final String ACTION_REMOVE_FROM_GROUP = "removeFromGroup";
+ private static final int RESULT_GROUP_ADD_MEMBER = 100;
+
/** Loader callbacks for existing group members for the autocomplete text view. */
private final LoaderCallbacks<Cursor> mGroupMemberCallbacks = new LoaderCallbacks<Cursor>() {
@@ -312,7 +317,10 @@
final boolean isGroupReadOnly = mGroupMetadata != null && mGroupMetadata.readOnly;
setVisible(menu, R.id.menu_add,
- isGroupEditable &&!isSelectionMode && !isSearchMode);
+ isGroupEditable && !isSelectionMode && !isSearchMode);
+
+ setVisible(menu, R.id.menu_search,
+ isGroupEditable && !isSelectionMode && !isSearchMode);
setVisible(menu, R.id.menu_rename_group,
isGroupEditable && !isSelectionMode && !isSearchMode);
@@ -341,6 +349,16 @@
return true;
}
case R.id.menu_add: {
+ final Intent intent = new Intent(Intent.ACTION_PICK);
+ intent.setType(ContactsContract.Groups.CONTENT_ITEM_TYPE);
+ intent.putExtra(UiIntentActions.GROUP_ACCOUNT_WITH_DATA_SET,
+ mGroupMetadata.createAccountWithDataSet());
+ intent.putExtra(UiIntentActions.GROUP_RAW_CONTACT_IDS,
+ getExistingGroupMemberRawContactIds());
+ startActivityForResult(intent, RESULT_GROUP_ADD_MEMBER);
+ return true;
+ }
+ case R.id.menu_search: {
if (mActionBarAdapter != null) {
mActionBarAdapter.setSearchMode(true);
}
@@ -367,6 +385,17 @@
return super.onOptionsItemSelected(item);
}
+ private ArrayList<String> getExistingGroupMemberRawContactIds() {
+ final ArrayList<String> rawContactIds = new ArrayList<>();
+ final Cursor cursor = mMembersListFragment.getAdapter().getCursor(/* partition */ 0);
+ if (cursor != null && cursor.moveToFirst()) {
+ do {
+ rawContactIds.add(cursor.getString(GroupMembersQuery.RAW_CONTACT_ID));
+ } while (cursor.moveToNext());
+ }
+ return rawContactIds;
+ }
+
private void deleteGroup() {
if (mGroupMetadata.memberCount == 0) {
final Intent intent = ContactSaveService.createGroupDeletionIntent(
@@ -417,6 +446,33 @@
}
}
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == RESULT_GROUP_ADD_MEMBER && resultCode == RESULT_OK && data != null) {
+ final Uri rawContactUri = data.getData();
+ if (rawContactUri != null) {
+ long rawContactId = -1;
+ try {
+ rawContactId = Long.parseLong(rawContactUri.getLastPathSegment());
+ } catch (NumberFormatException ignored) {}
+ if (rawContactId < 0) {
+ Toast.makeText(this, R.string.groupSavedErrorToast, Toast.LENGTH_SHORT).show();
+ Log.w(TAG, "Failed to parse ID from pick group member result uri " +
+ rawContactUri);
+ return;
+ }
+ final long[] rawContactIdsToAdd = new long[1];
+ rawContactIdsToAdd[0] = rawContactId;
+ final Intent intent = ContactSaveService.createGroupUpdateIntent(
+ GroupMembersActivity.this, mGroupMetadata.groupId, /* newLabel */ null,
+ rawContactIdsToAdd, /* rawContactIdsToRemove */ null,
+ GroupMembersActivity.class, GroupMembersActivity.ACTION_ADD_TO_GROUP);
+ startService(intent);
+ }
+ }
+ }
+
private boolean isSelectAccountDialogFound() {
return getFragmentManager().findFragmentByTag(TAG_SELECT_ACCOUNT_DIALOG) != null;
}
diff --git a/src/com/android/contacts/group/GroupMembersListAdapter.java b/src/com/android/contacts/group/GroupMembersListAdapter.java
index fe692e7..86e75b1 100644
--- a/src/com/android/contacts/group/GroupMembersListAdapter.java
+++ b/src/com/android/contacts/group/GroupMembersListAdapter.java
@@ -35,7 +35,7 @@
/** Group members cursor adapter. */
public class GroupMembersListAdapter extends MultiSelectEntryContactListAdapter {
- private static class GroupMembersQuery {
+ public static class GroupMembersQuery {
private static final String[] PROJECTION_PRIMARY = new String[] {
Data.CONTACT_ID,
diff --git a/src/com/android/contacts/group/GroupMembersListFragment.java b/src/com/android/contacts/group/GroupMembersListFragment.java
index 5c86d04..47eed0c 100644
--- a/src/com/android/contacts/group/GroupMembersListFragment.java
+++ b/src/com/android/contacts/group/GroupMembersListFragment.java
@@ -31,9 +31,9 @@
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.common.logging.ListEvent.ListType;
import com.android.contacts.list.MultiSelectContactsListFragment;
/** Displays the members of a group. */
diff --git a/src/com/android/contacts/list/ContactsIntentResolver.java b/src/com/android/contacts/list/ContactsIntentResolver.java
index 259e0c7..ded72c2 100644
--- a/src/com/android/contacts/list/ContactsIntentResolver.java
+++ b/src/com/android/contacts/list/ContactsIntentResolver.java
@@ -28,11 +28,14 @@
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.Intents;
import android.provider.ContactsContract.Intents.Insert;
import android.text.TextUtils;
import android.util.Log;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+
/**
* Parses a Contacts intent, extracting all relevant parts and packaging them
* as a {@link ContactsRequest} object.
@@ -89,6 +92,12 @@
request.setLegacyCompatibilityMode(true);
} else if (Email.CONTENT_TYPE.equals(resolvedType)) {
request.setActionCode(ContactsRequest.ACTION_PICK_EMAIL);
+ } else if (Groups.CONTENT_ITEM_TYPE.equals(resolvedType)) {
+ request.setActionCode(ContactsRequest.ACTION_PICK_GROUP_MEMBERS);
+ request.setAccountWithDataSet(intent.<AccountWithDataSet> getParcelableExtra(
+ UiIntentActions.GROUP_ACCOUNT_WITH_DATA_SET));
+ request.setRawContactIds(intent.getStringArrayListExtra(
+ UiIntentActions.GROUP_RAW_CONTACT_IDS));
}
} else if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) {
String component = intent.getComponent().getClassName();
diff --git a/src/com/android/contacts/list/ContactsRequest.java b/src/com/android/contacts/list/ContactsRequest.java
index a1428be..a686752 100644
--- a/src/com/android/contacts/list/ContactsRequest.java
+++ b/src/com/android/contacts/list/ContactsRequest.java
@@ -21,6 +21,10 @@
import android.os.Parcel;
import android.os.Parcelable;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+
+import java.util.ArrayList;
+
/**
* Parsed form of the intent sent to the Contacts application.
*/
@@ -38,6 +42,9 @@
/** Show contents of a specific group */
public static final int ACTION_GROUP = 20;
+ /** Show potential new members of a specific group */
+ public static final int ACTION_PICK_GROUP_MEMBERS = 21;
+
/** Show all starred contacts */
public static final int ACTION_STARRED = 30;
@@ -89,6 +96,8 @@
private boolean mLegacyCompatibilityMode;
private boolean mDirectorySearchEnabled = true;
private Uri mContactUri;
+ private AccountWithDataSet mAccountWithDataSet;
+ private ArrayList<String> mRawContactIds;
@Override
public String toString() {
@@ -101,6 +110,8 @@
+ " mLegacyCompatibilityMode=" + mLegacyCompatibilityMode
+ " mDirectorySearchEnabled=" + mDirectorySearchEnabled
+ " mContactUri=" + mContactUri
+ + " mAccountWithDataSet=" + mAccountWithDataSet
+ + " mRawContactIds=" + mRawContactIds
+ "}";
}
@@ -179,4 +190,20 @@
public void setContactUri(Uri contactUri) {
this.mContactUri = contactUri;
}
+
+ public AccountWithDataSet getAccountWithDataSet() {
+ return mAccountWithDataSet;
+ }
+
+ public void setAccountWithDataSet(AccountWithDataSet accountWithDataSet) {
+ mAccountWithDataSet = accountWithDataSet;
+ }
+
+ public ArrayList<String> getRawContactIds() {
+ return mRawContactIds;
+ }
+
+ public void setRawContactIds(ArrayList<String> rawContactIds) {
+ mRawContactIds = rawContactIds;
+ }
}
diff --git a/src/com/android/contacts/list/GroupMemberPickListAdapter.java b/src/com/android/contacts/list/GroupMemberPickListAdapter.java
new file mode 100644
index 0000000..7a1d355
--- /dev/null
+++ b/src/com/android/contacts/list/GroupMemberPickListAdapter.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2016 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 android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+import com.android.contacts.common.list.ContactEntryListAdapter;
+import com.android.contacts.common.list.ContactListItemView;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.preference.ContactsPreferences;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Adapter for raw contacts owned by an account that are not already members of a given group.
+ */
+public class GroupMemberPickListAdapter extends ContactEntryListAdapter {
+
+ static class GroupMembersQuery {
+
+ private static final String[] PROJECTION_PRIMARY = new String[] {
+ RawContacts._ID, // 0
+ RawContacts.CONTACT_ID, // 1
+ RawContacts.DISPLAY_NAME_PRIMARY, // 2
+ // Dummy columns overwritten by the cursor wrapper
+ RawContacts.SYNC1, // 3
+ RawContacts.SYNC2 // 4
+ };
+
+ private static final String[] PROJECTION_ALTERNATIVE = new String[] {
+ RawContacts._ID, // 0
+ RawContacts.CONTACT_ID, // 1
+ RawContacts.DISPLAY_NAME_ALTERNATIVE, // 2
+ // Dummy columns overwritten by the cursor wrapper
+ RawContacts.SYNC1, // 3
+ RawContacts.SYNC2 // 4
+ };
+
+ static final int RAW_CONTACT_ID = 0;
+ static final int CONTACT_ID = 1;
+ static final int CONTACT_DISPLAY_NAME = 2;
+ // Provided by the cursor wrapper.
+ static final int CONTACT_PHOTO_ID = 3;
+ // Provided by the cursor wrapper.
+ static final int CONTACT_LOOKUP_KEY = 4;
+
+ private GroupMembersQuery() {
+ }
+ }
+
+ private AccountWithDataSet mAccount;
+ private final Set<String> mRawContactIds = new HashSet<>();
+
+ private final CharSequence mUnknownNameText;
+
+ public GroupMemberPickListAdapter(Context context) {
+ super(context);
+ mUnknownNameText = context.getText(android.R.string.unknownName);
+ }
+
+ public GroupMemberPickListAdapter setAccount(AccountWithDataSet account) {
+ mAccount = account;
+ return this;
+ }
+
+ public GroupMemberPickListAdapter setRawContactIds(ArrayList<String> rawContactIds) {
+ mRawContactIds.clear();
+ mRawContactIds.addAll(rawContactIds);
+ return this;
+ }
+
+ @Override
+ public String getContactDisplayName(int position) {
+ final Cursor cursor = (Cursor) getItem(position);
+ return cursor.getString(GroupMembersQuery.CONTACT_DISPLAY_NAME);
+ }
+
+ @Override
+ public void configureLoader(CursorLoader loader, long directoryId) {
+ loader.setUri(RawContacts.CONTENT_URI);
+
+ loader.setProjection(
+ getContactNameDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY
+ ? GroupMembersQuery.PROJECTION_PRIMARY
+ : GroupMembersQuery.PROJECTION_ALTERNATIVE);
+ loader.setSelection(getSelection());
+ loader.setSelectionArgs(getSelectionArgs());
+
+ loader.setSortOrder(getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY
+ ? Data.SORT_KEY_PRIMARY : Data.SORT_KEY_ALTERNATIVE
+ + " COLLATE LOCALIZED ASC");
+ }
+
+ private String getSelection() {
+ // Select raw contacts by account
+ String result = RawContacts.ACCOUNT_NAME + "=? AND " + RawContacts.ACCOUNT_TYPE + "=? AND ";
+ if (TextUtils.isEmpty(mAccount.dataSet)) {
+ result += Data.DATA_SET + " IS NULL";
+ } else {
+ result += Data.DATA_SET + "=?";
+ }
+ return result;
+ }
+
+ private String[] getSelectionArgs() {
+ final ArrayList<String> result = new ArrayList<>();
+ result.add(mAccount.name);
+ result.add(mAccount.type);
+ if (!TextUtils.isEmpty(mAccount.dataSet)) result.add(mAccount.dataSet);
+ return result.toArray(new String[0]);
+ }
+
+ public Uri getRawContactUri(int position) {
+ final Cursor cursor = (Cursor) getItem(position);
+ final long rawContactId = cursor.getLong(GroupMembersQuery.RAW_CONTACT_ID);
+ return RawContacts.CONTENT_URI.buildUpon()
+ .appendPath(Long.toString(rawContactId))
+ .build();
+ }
+
+ @Override
+ protected ContactListItemView newView(Context context, int partition, Cursor cursor,
+ int position, ViewGroup parent) {
+ final ContactListItemView view =
+ super.newView(context, partition, cursor, position, parent);
+ view.setUnknownNameText(mUnknownNameText);
+ return view;
+ }
+
+ @Override
+ protected void bindView(View v, int partition, Cursor cursor, int position) {
+ super.bindView(v, partition, cursor, position);
+ final ContactListItemView view = (ContactListItemView) v;
+ bindName(view, cursor);
+ bindViewId(view, cursor, GroupMembersQuery.RAW_CONTACT_ID);
+ bindPhoto(view, cursor);
+ }
+
+ private void bindName(ContactListItemView view, Cursor cursor) {
+ view.showDisplayName(cursor, GroupMembersQuery.CONTACT_DISPLAY_NAME,
+ getContactNameDisplayOrder());
+ }
+
+ private void bindPhoto(final ContactListItemView view, Cursor cursor) {
+ final long photoId = cursor.isNull(GroupMembersQuery.CONTACT_PHOTO_ID)
+ ? 0 : cursor.getLong(GroupMembersQuery.CONTACT_PHOTO_ID);
+ final DefaultImageRequest imageRequest = photoId == 0
+ ? getDefaultImageRequestFromCursor(cursor, GroupMembersQuery.CONTACT_DISPLAY_NAME,
+ GroupMembersQuery.CONTACT_LOOKUP_KEY)
+ : null;
+ getPhotoLoader().loadThumbnail(view.getPhotoView(), photoId, false, getCircularPhotos(),
+ imageRequest);
+ }
+}
diff --git a/src/com/android/contacts/list/GroupMemberPickerFragment.java b/src/com/android/contacts/list/GroupMemberPickerFragment.java
new file mode 100644
index 0000000..ae82b21
--- /dev/null
+++ b/src/com/android/contacts/list/GroupMemberPickerFragment.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2016 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 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.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.list.ContactEntryListFragment;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.list.GroupMemberPickListAdapter.GroupMembersQuery;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Fragment containing raw contacts for a specified account that are not already in a group.
+ */
+public class GroupMemberPickerFragment extends
+ ContactEntryListFragment<GroupMemberPickListAdapter> {
+
+ private static final String KEY_ACCOUNT = "account";
+ private static final String KEY_RAW_CONTACT_IDS = "rawContactIds";
+
+ private static final String ARG_ACCOUNT = "account";
+ private static final String ARG_RAW_CONTACT_IDS = "rawContactIds";
+
+ /** Callbacks for host of {@link GroupMemberPickerFragment}. */
+ public interface Listener {
+
+ /** Invoked when a potential group member is selected. */
+ void onGroupMemberClicked(Uri uri);
+ }
+
+ /**
+ * 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.
+ */
+ 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];
+ for (int i = 0; i < mCount; i++) {
+ super.moveToPosition(i);
+ final String rawContactId = getString(GroupMembersQuery.RAW_CONTACT_ID);
+ if (!mRawContactIds.contains(rawContactId)) {
+ mIndex[mPos++] = i;
+ }
+ }
+ mCount = mPos;
+ mPos = 0;
+ super.moveToFirst();
+ }
+
+ @Override
+ public int getColumnIndex(String columnName) {
+ final int index = getColumnIndexForContactColumn(columnName);
+ return index < 0 ? super.getColumnIndex(columnName) : index;
+ }
+
+ @Override
+ public int getColumnIndexOrThrow(String columnName) {
+ final int index = getColumnIndexForContactColumn(columnName);
+ return index < 0 ? super.getColumnIndexOrThrow(columnName) : index;
+ }
+
+ private int getColumnIndexForContactColumn(String columnName) {
+ if (Contacts.PHOTO_ID.equals(columnName)) {
+ return GroupMembersQuery.CONTACT_PHOTO_ID;
+ }
+ if (Contacts.LOOKUP_KEY.equals(columnName)) {
+ return GroupMembersQuery.CONTACT_LOOKUP_KEY;
+ }
+ return -1;
+ }
+
+ @Override
+ public String[] getColumnNames() {
+ final String displayNameColumnName =
+ getContactNameDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY
+ ? RawContacts.DISPLAY_NAME_PRIMARY
+ : RawContacts.DISPLAY_NAME_ALTERNATIVE;
+ return new String[] {
+ RawContacts._ID,
+ RawContacts.CONTACT_ID,
+ displayNameColumnName,
+ Contacts.PHOTO_ID,
+ Contacts.LOOKUP_KEY,
+ };
+ }
+
+ @Override
+ public String getString(int columnIndex) {
+ if (columnIndex == GroupMembersQuery.CONTACT_LOOKUP_KEY) {
+ if (columnIndex == GroupMembersQuery.CONTACT_PHOTO_ID) {
+ final long contactId = getLong(GroupMembersQuery.CONTACT_ID);
+ final Pair<Long,String> pair = getContactPhotoPair(contactId);
+ if (pair != null) {
+ return pair.second;
+ }
+ }
+ return null;
+ }
+ return super.getString(columnIndex);
+ }
+
+ @Override
+ public long getLong(int columnIndex) {
+ if (columnIndex == GroupMembersQuery.CONTACT_PHOTO_ID) {
+ final long contactId = getLong(GroupMembersQuery.CONTACT_ID);
+ final Pair<Long,String> pair = getContactPhotoPair(contactId);
+ if (pair != null) {
+ return pair.first;
+ }
+ return 0;
+ }
+ return super.getLong(columnIndex);
+ }
+
+ @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 AccountWithDataSet mAccount;
+ private ArrayList<String> mRawContactIds;
+ private Map<Long, Pair<Long,String>> mContactPhotoMap = new HashMap();
+
+ private Listener mListener;
+
+ public static GroupMemberPickerFragment newInstance(AccountWithDataSet account,
+ ArrayList<String> rawContactids) {
+ final Bundle args = new Bundle();
+ args.putParcelable(ARG_ACCOUNT, account);
+ args.putStringArrayList(ARG_RAW_CONTACT_IDS, rawContactids);
+
+ final GroupMemberPickerFragment fragment = new GroupMemberPickerFragment();
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ public GroupMemberPickerFragment() {
+ setQuickContactEnabled(false);
+ setPhotoLoaderEnabled(true);
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onCreate(Bundle savedState) {
+ super.onCreate(savedState);
+ if (savedState == null) {
+ mAccount = getArguments().getParcelable(ARG_ACCOUNT);
+ mRawContactIds = getArguments().getStringArrayList(ARG_RAW_CONTACT_IDS);
+ } else {
+ mAccount = savedState.getParcelable(KEY_ACCOUNT);
+ mRawContactIds = savedState.getStringArrayList(KEY_RAW_CONTACT_IDS);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putParcelable(KEY_ACCOUNT, mAccount);
+ outState.putStringArrayList(KEY_RAW_CONTACT_IDS, mRawContactIds);
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ protected View inflateView(LayoutInflater inflater, ViewGroup container) {
+ return inflater.inflate(R.layout.contact_list_content, null);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ super.onLoadFinished(loader, new FilterCursorWrapper(data));
+ }
+
+ @Override
+ protected GroupMemberPickListAdapter createListAdapter() {
+ final GroupMemberPickListAdapter adapter = new GroupMemberPickListAdapter(getActivity());
+ adapter.setDisplayPhotos(true);
+ return adapter;
+ }
+
+ @Override
+ protected void configureAdapter() {
+ super.configureAdapter();
+ getAdapter().setAccount(mAccount);
+ getAdapter().setRawContactIds(mRawContactIds);
+ }
+
+ @Override
+ protected void onItemClick(int position, long id) {
+ if (mListener != null) {
+ mListener.onGroupMemberClicked(getAdapter().getRawContactUri(position));
+ }
+ }
+
+ // TODO(wjang): unacceptable scrolling performance for big groups
+ private Pair<Long,String> getContactPhotoPair(long contactId) {
+ if (mContactPhotoMap.containsKey(contactId)) {
+ return mContactPhotoMap.get(contactId);
+ }
+ final Uri uri = Data.CONTENT_URI.buildUpon()
+ .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
+ String.valueOf(Directory.DEFAULT))
+ .build();
+ final String[] projection = new String[] { Data.PHOTO_ID, Data.LOOKUP_KEY };
+ final String selection = Data.CONTACT_ID + "=?";
+ final String[] selectionArgs = new String[] { Long.toString(contactId) };
+ Cursor cursor = null;
+ try {
+ cursor = getActivity().getContentResolver().query(
+ uri, projection, selection, selectionArgs, /* sortOrder */ null);
+ if (cursor != null && cursor.moveToFirst()) {
+ final Pair<Long, String> pair = new Pair(cursor.getLong(0), cursor.getString(1));
+ mContactPhotoMap.put(contactId, pair);
+ return pair;
+ }
+ } finally {
+ if (cursor != null) cursor.close();
+ }
+ return null;
+ }
+}
diff --git a/src/com/android/contacts/list/UiIntentActions.java b/src/com/android/contacts/list/UiIntentActions.java
index 5539635..31e5ade 100644
--- a/src/com/android/contacts/list/UiIntentActions.java
+++ b/src/com/android/contacts/list/UiIntentActions.java
@@ -40,6 +40,18 @@
public static final String GROUP_NAME_EXTRA_KEY = "com.android.contacts.extra.GROUP";
/**
+ * The account used to filter potential raw contact groups members.
+ */
+ public static final String GROUP_ACCOUNT_WITH_DATA_SET =
+ "com.android.contacts.extra.GROUP_ACCOUNT_WITH_DATA_SET";
+
+ /**
+ * The raw contact IDs for existing group members.
+ */
+ public static final String GROUP_RAW_CONTACT_IDS =
+ "com.android.contacts.extra.GROUP_RAW_CONTACT_IDS";
+
+ /**
* The action for the all contacts list tab.
*/
public static final String LIST_ALL_CONTACTS_ACTION =