GroupEditorFragment.java revision 1c07d4bf219bcdcc433b69a40ed3398ba7b3389f
1/* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17package com.android.contacts.group; 18 19import com.android.contacts.ContactPhotoManager; 20import com.android.contacts.ContactSaveService; 21import com.android.contacts.GroupMemberLoader; 22import com.android.contacts.GroupMetaDataLoader; 23import com.android.contacts.R; 24import com.android.contacts.activities.GroupEditorActivity; 25import com.android.contacts.editor.ContactEditorFragment.SaveMode; 26import com.android.contacts.editor.SelectAccountDialogFragment; 27import com.android.contacts.group.SuggestedMemberListAdapter.SuggestedMember; 28import com.android.contacts.model.AccountType; 29import com.android.contacts.model.AccountTypeManager; 30import com.android.contacts.model.AccountWithDataSet; 31import com.android.internal.util.Objects; 32 33import android.accounts.Account; 34import android.app.Activity; 35import android.app.AlertDialog; 36import android.app.Dialog; 37import android.app.DialogFragment; 38import android.app.Fragment; 39import android.app.LoaderManager; 40import android.app.LoaderManager.LoaderCallbacks; 41import android.content.ContentResolver; 42import android.content.ContentUris; 43import android.content.Context; 44import android.content.CursorLoader; 45import android.content.DialogInterface; 46import android.content.Intent; 47import android.content.Loader; 48import android.database.Cursor; 49import android.net.Uri; 50import android.os.Bundle; 51import android.provider.ContactsContract.Contacts; 52import android.provider.ContactsContract.Intents; 53import android.text.TextUtils; 54import android.util.Log; 55import android.view.LayoutInflater; 56import android.view.Menu; 57import android.view.MenuInflater; 58import android.view.MenuItem; 59import android.view.View; 60import android.view.View.OnClickListener; 61import android.view.ViewGroup; 62import android.widget.AdapterView; 63import android.widget.AdapterView.OnItemClickListener; 64import android.widget.AutoCompleteTextView; 65import android.widget.BaseAdapter; 66import android.widget.EditText; 67import android.widget.ImageView; 68import android.widget.ListView; 69import android.widget.QuickContactBadge; 70import android.widget.TextView; 71import android.widget.Toast; 72 73import java.util.ArrayList; 74import java.util.List; 75 76// TODO: Use savedInstanceState 77public class GroupEditorFragment extends Fragment implements SelectAccountDialogFragment.Listener { 78 79 private static final String TAG = "GroupEditorFragment"; 80 81 private static final String LEGACY_CONTACTS_AUTHORITY = "contacts"; 82 83 public static interface Listener { 84 /** 85 * Group metadata was not found, close the fragment now. 86 */ 87 public void onGroupNotFound(); 88 89 /** 90 * User has tapped Revert, close the fragment now. 91 */ 92 void onReverted(); 93 94 /** 95 * Title has been determined. 96 */ 97 void onTitleLoaded(int resourceId); 98 99 /** 100 * Contact was saved and the Fragment can now be closed safely. 101 */ 102 void onSaveFinished(int resultCode, Intent resultIntent); 103 } 104 105 private static final int LOADER_GROUP_METADATA = 1; 106 private static final int LOADER_EXISTING_MEMBERS = 2; 107 private static final int LOADER_NEW_GROUP_MEMBER = 3; 108 109 public static final String SAVE_MODE_EXTRA_KEY = "saveMode"; 110 111 private static final String MEMBER_RAW_CONTACT_ID_KEY = "rawContactId"; 112 private static final String MEMBER_LOOKUP_URI_KEY = "memberLookupUri"; 113 114 protected static final String[] PROJECTION_CONTACT = new String[] { 115 Contacts._ID, // 0 116 Contacts.DISPLAY_NAME_PRIMARY, // 1 117 Contacts.DISPLAY_NAME_ALTERNATIVE, // 2 118 Contacts.SORT_KEY_PRIMARY, // 3 119 Contacts.STARRED, // 4 120 Contacts.CONTACT_PRESENCE, // 5 121 Contacts.CONTACT_CHAT_CAPABILITY, // 6 122 Contacts.PHOTO_ID, // 7 123 Contacts.PHOTO_THUMBNAIL_URI, // 8 124 Contacts.LOOKUP_KEY, // 9 125 Contacts.PHONETIC_NAME, // 10 126 Contacts.HAS_PHONE_NUMBER, // 11 127 Contacts.IS_USER_PROFILE, // 12 128 }; 129 130 protected static final int CONTACT_ID_COLUMN_INDEX = 0; 131 protected static final int CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX = 1; 132 protected static final int CONTACT_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX = 2; 133 protected static final int CONTACT_SORT_KEY_PRIMARY_COLUMN_INDEX = 3; 134 protected static final int CONTACT_STARRED_COLUMN_INDEX = 4; 135 protected static final int CONTACT_PRESENCE_STATUS_COLUMN_INDEX = 5; 136 protected static final int CONTACT_CHAT_CAPABILITY_COLUMN_INDEX = 6; 137 protected static final int CONTACT_PHOTO_ID_COLUMN_INDEX = 7; 138 protected static final int CONTACT_PHOTO_URI_COLUMN_INDEX = 8; 139 protected static final int CONTACT_LOOKUP_KEY_COLUMN_INDEX = 9; 140 protected static final int CONTACT_PHONETIC_NAME_COLUMN_INDEX = 10; 141 protected static final int CONTACT_HAS_PHONE_COLUMN_INDEX = 11; 142 protected static final int CONTACT_IS_USER_PROFILE = 12; 143 144 /** 145 * Modes that specify the status of the editor 146 */ 147 public enum Status { 148 LOADING, // Loader is fetching the data 149 EDITING, // Not currently busy. We are waiting forthe user to enter data. 150 SAVING, // Data is currently being saved 151 CLOSING // Prevents any more saves 152 } 153 154 private Context mContext; 155 private String mAction; 156 private Bundle mIntentExtras; 157 private Uri mGroupUri; 158 private long mGroupId; 159 private Listener mListener; 160 161 private Status mStatus; 162 163 private ViewGroup mRootView; 164 private ListView mListView; 165 private LayoutInflater mLayoutInflater; 166 167 private TextView mGroupNameView; 168 private ImageView mAccountIcon; 169 private TextView mAccountTypeTextView; 170 private TextView mAccountNameTextView; 171 private AutoCompleteTextView mAutoCompleteTextView; 172 173 private boolean mGroupNameIsReadOnly; 174 private String mAccountName; 175 private String mAccountType; 176 private String mDataSet; 177 private String mOriginalGroupName = ""; 178 179 private MemberListAdapter mMemberListAdapter; 180 private ContactPhotoManager mPhotoManager; 181 182 private ContentResolver mContentResolver; 183 private SuggestedMemberListAdapter mAutoCompleteAdapter; 184 185 private List<Member> mListMembersToAdd = new ArrayList<Member>(); 186 private List<Member> mListMembersToRemove = new ArrayList<Member>(); 187 private List<Member> mListToDisplay = new ArrayList<Member>(); 188 189 public GroupEditorFragment() { 190 } 191 192 @Override 193 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { 194 setHasOptionsMenu(true); 195 mLayoutInflater = inflater; 196 mRootView = (ViewGroup) inflater.inflate(R.layout.group_editor_fragment, container, false); 197 return mRootView; 198 } 199 200 @Override 201 public void onAttach(Activity activity) { 202 super.onAttach(activity); 203 mContext = activity; 204 mPhotoManager = ContactPhotoManager.getInstance(mContext); 205 mMemberListAdapter = new MemberListAdapter(); 206 } 207 208 @Override 209 public void onActivityCreated(Bundle savedInstanceState) { 210 super.onActivityCreated(savedInstanceState); 211 212 // Edit an existing group 213 if (Intent.ACTION_EDIT.equals(mAction)) { 214 if (mListener != null) { 215 mListener.onTitleLoaded(R.string.editGroup_title_edit); 216 } 217 getLoaderManager().initLoader(LOADER_GROUP_METADATA, null, 218 mGroupMetaDataLoaderListener); 219 } else if (Intent.ACTION_INSERT.equals(mAction)) { 220 if (mListener != null) { 221 mListener.onTitleLoaded(R.string.editGroup_title_insert); 222 } 223 224 final Account account = mIntentExtras == null ? null : 225 (Account) mIntentExtras.getParcelable(Intents.Insert.ACCOUNT); 226 final String dataSet = mIntentExtras == null ? null : 227 mIntentExtras.getString(Intents.Insert.DATA_SET); 228 229 if (account != null) { 230 // Account specified in Intent - no data set can be specified in this manner. 231 mAccountName = account.name; 232 mAccountType = account.type; 233 mDataSet = dataSet; 234 setupEditorForAccount(); 235 } else { 236 // No Account specified. Let the user choose from a disambiguation dialog. 237 selectAccountAndCreateGroup(); 238 } 239 } else { 240 throw new IllegalArgumentException("Unknown Action String " + mAction + 241 ". Only support " + Intent.ACTION_EDIT + " or " + Intent.ACTION_INSERT); 242 } 243 } 244 245 public void setContentResolver(ContentResolver resolver) { 246 mContentResolver = resolver; 247 if (mAutoCompleteAdapter != null) { 248 mAutoCompleteAdapter.setContentResolver(mContentResolver); 249 } 250 } 251 252 private void selectAccountAndCreateGroup() { 253 final List<AccountWithDataSet> accounts = 254 AccountTypeManager.getInstance(mContext).getAccounts(true /* writeable */); 255 // No Accounts available 256 if (accounts.isEmpty()) { 257 throw new IllegalStateException("No accounts were found."); 258 } 259 260 // In the common case of a single account being writable, auto-select 261 // it without showing a dialog. 262 if (accounts.size() == 1) { 263 mAccountName = accounts.get(0).name; 264 mAccountType = accounts.get(0).type; 265 mDataSet = accounts.get(0).dataSet; 266 setupEditorForAccount(); 267 return; // Don't show a dialog. 268 } 269 270 final SelectAccountDialogFragment dialog = new SelectAccountDialogFragment( 271 R.string.dialog_new_group_account); 272 dialog.setTargetFragment(this, 0); 273 dialog.show(getFragmentManager(), SelectAccountDialogFragment.TAG); 274 } 275 276 @Override 277 public void onAccountChosen(int requestCode, AccountWithDataSet account) { 278 mAccountName = account.name; 279 mAccountType = account.type; 280 mDataSet = account.dataSet; 281 setupEditorForAccount(); 282 } 283 284 @Override 285 public void onAccountSelectorCancelled() { 286 if (mListener != null) { 287 // Exit the fragment because we cannot continue without selecting an account 288 mListener.onGroupNotFound(); 289 } 290 } 291 292 /** 293 * Sets up the editor based on the group's account name and type. 294 */ 295 private void setupEditorForAccount() { 296 final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(mContext); 297 final AccountType accountType = accountTypeManager.getAccountType(mAccountType, mDataSet); 298 final boolean editable = accountType.isGroupMembershipEditable(); 299 mMemberListAdapter.setIsGroupMembershipEditable(editable); 300 301 View editorView = mLayoutInflater.inflate(editable ? 302 R.layout.group_editor_view : R.layout.external_group_editor_view, mRootView, false); 303 304 mGroupNameView = (TextView) editorView.findViewById(R.id.group_name); 305 mAccountIcon = (ImageView) editorView.findViewById(R.id.account_icon); 306 mAccountTypeTextView = (TextView) editorView.findViewById(R.id.account_type); 307 mAccountNameTextView = (TextView) editorView.findViewById(R.id.account_name); 308 mAutoCompleteTextView = (AutoCompleteTextView) editorView.findViewById( 309 R.id.add_member_field); 310 311 mListView = (ListView) editorView.findViewById(android.R.id.list); 312 mListView.setAdapter(mMemberListAdapter); 313 314 // Setup the account header 315 CharSequence accountTypeDisplayLabel = accountType.getDisplayLabel(mContext); 316 if (!TextUtils.isEmpty(mAccountName)) { 317 mAccountNameTextView.setText( 318 mContext.getString(R.string.from_account_format, mAccountName)); 319 } 320 mAccountTypeTextView.setText(accountTypeDisplayLabel); 321 mAccountIcon.setImageDrawable(accountType.getDisplayIcon(mContext)); 322 323 // Setup the autocomplete adapter (for contacts to suggest to add to the group) based on the 324 // account name and type. For groups that cannot have membership edited, there will be no 325 // autocomplete text view. 326 if (mAutoCompleteTextView != null) { 327 mAutoCompleteAdapter = new SuggestedMemberListAdapter(mContext, 328 android.R.layout.simple_dropdown_item_1line); 329 mAutoCompleteAdapter.setContentResolver(mContentResolver); 330 mAutoCompleteAdapter.setAccountType(mAccountType); 331 mAutoCompleteAdapter.setAccountName(mAccountName); 332 mAutoCompleteAdapter.setDataSet(mDataSet); 333 mAutoCompleteTextView.setAdapter(mAutoCompleteAdapter); 334 mAutoCompleteTextView.setOnItemClickListener(new OnItemClickListener() { 335 @Override 336 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 337 SuggestedMember member = mAutoCompleteAdapter.getItem(position); 338 loadMemberToAddToGroup(member.getRawContactId(), 339 String.valueOf(member.getContactId())); 340 341 // Update the autocomplete adapter so the contact doesn't get suggested again 342 mAutoCompleteAdapter.addNewMember(member.getContactId()); 343 344 // Clear out the text field 345 mAutoCompleteTextView.setText(""); 346 } 347 }); 348 } 349 350 mRootView.addView(editorView); 351 mStatus = Status.EDITING; 352 } 353 354 public void load(String action, Uri groupUri, Bundle intentExtras) { 355 mAction = action; 356 mGroupUri = groupUri; 357 mGroupId = (groupUri != null) ? ContentUris.parseId(mGroupUri) : 0; 358 mIntentExtras = intentExtras; 359 } 360 361 private void bindGroupMetaData(Cursor cursor) { 362 if (cursor.getCount() == 0) { 363 if (mListener != null) { 364 mListener.onGroupNotFound(); 365 } 366 } 367 try { 368 cursor.moveToFirst(); 369 mOriginalGroupName = cursor.getString(GroupMetaDataLoader.TITLE); 370 mAccountName = cursor.getString(GroupMetaDataLoader.ACCOUNT_NAME); 371 mAccountType = cursor.getString(GroupMetaDataLoader.ACCOUNT_TYPE); 372 mGroupNameIsReadOnly = (cursor.getInt(GroupMetaDataLoader.IS_READ_ONLY) == 1); 373 } catch (Exception e) { 374 Log.i(TAG, "Group not found with URI: " + mGroupUri + " Closing activity now."); 375 if (mListener != null) { 376 mListener.onGroupNotFound(); 377 } 378 } finally { 379 cursor.close(); 380 } 381 setupEditorForAccount(); 382 // Setup the group metadata display (If the group name is ready only, don't let the user 383 // focus on the field). 384 mGroupNameView.setText(mOriginalGroupName); 385 mGroupNameView.setFocusable(!mGroupNameIsReadOnly); 386 } 387 388 public void loadMemberToAddToGroup(long rawContactId, String contactId) { 389 Bundle args = new Bundle(); 390 args.putLong(MEMBER_RAW_CONTACT_ID_KEY, rawContactId); 391 args.putString(MEMBER_LOOKUP_URI_KEY, contactId); 392 getLoaderManager().restartLoader(LOADER_NEW_GROUP_MEMBER, args, mContactLoaderListener); 393 } 394 395 public void setListener(Listener value) { 396 mListener = value; 397 } 398 399 public void doSaveAction() { 400 save(SaveMode.CLOSE); 401 } 402 403 @Override 404 public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) { 405 inflater.inflate(R.menu.edit_group, menu); 406 } 407 408 @Override 409 public boolean onOptionsItemSelected(MenuItem item) { 410 switch (item.getItemId()) { 411 case R.id.menu_discard: 412 return revert(); 413 } 414 return false; 415 } 416 417 private boolean revert() { 418 if (!hasNameChange() && !hasMembershipChange()) { 419 doRevertAction(); 420 } else { 421 CancelEditDialogFragment.show(this); 422 } 423 return true; 424 } 425 426 private void doRevertAction() { 427 // When this Fragment is closed we don't want it to auto-save 428 mStatus = Status.CLOSING; 429 if (mListener != null) mListener.onReverted(); 430 } 431 432 public static class CancelEditDialogFragment extends DialogFragment { 433 434 public static void show(GroupEditorFragment fragment) { 435 CancelEditDialogFragment dialog = new CancelEditDialogFragment(); 436 dialog.setTargetFragment(fragment, 0); 437 dialog.show(fragment.getFragmentManager(), "cancelEditor"); 438 } 439 440 @Override 441 public Dialog onCreateDialog(Bundle savedInstanceState) { 442 AlertDialog dialog = new AlertDialog.Builder(getActivity()) 443 .setIconAttribute(android.R.attr.alertDialogIcon) 444 .setTitle(R.string.cancel_confirmation_dialog_title) 445 .setMessage(R.string.cancel_confirmation_dialog_message) 446 .setPositiveButton(R.string.discard, 447 new DialogInterface.OnClickListener() { 448 @Override 449 public void onClick(DialogInterface dialog, int whichButton) { 450 ((GroupEditorFragment) getTargetFragment()).doRevertAction(); 451 } 452 } 453 ) 454 .setNegativeButton(android.R.string.cancel, null) 455 .create(); 456 return dialog; 457 } 458 } 459 460 /** 461 * Saves or creates the group based on the mode, and if successful 462 * finishes the activity. This actually only handles saving the group name. 463 * @return true when successful 464 */ 465 public boolean save(int saveMode) { 466 if (!hasValidGroupName() || mStatus != Status.EDITING) { 467 return false; 468 } 469 470 // If we are about to close the editor - there is no need to refresh the data 471 if (saveMode == SaveMode.CLOSE) { 472 getLoaderManager().destroyLoader(LOADER_EXISTING_MEMBERS); 473 } 474 475 // If there are no changes, then go straight to onSaveCompleted() 476 if (!hasNameChange() && !hasMembershipChange()) { 477 onSaveCompleted(false, SaveMode.CLOSE, mGroupUri); 478 return true; 479 } 480 481 mStatus = Status.SAVING; 482 483 Activity activity = getActivity(); 484 // If the activity is not there anymore, then we can't continue with the save process. 485 if (activity == null) { 486 return false; 487 } 488 Intent saveIntent = null; 489 if (Intent.ACTION_INSERT.equals(mAction)) { 490 // Create array of raw contact IDs for contacts to add to the group 491 long[] membersToAddArray = convertToArray(mListMembersToAdd); 492 493 // Create the save intent to create the group and add members at the same time 494 saveIntent = ContactSaveService.createNewGroupIntent(activity, 495 new AccountWithDataSet(mAccountName, mAccountType, mDataSet), 496 mGroupNameView.getText().toString(), 497 membersToAddArray, activity.getClass(), 498 GroupEditorActivity.ACTION_SAVE_COMPLETED); 499 } else if (Intent.ACTION_EDIT.equals(mAction)) { 500 // Create array of raw contact IDs for contacts to add to the group 501 long[] membersToAddArray = convertToArray(mListMembersToAdd); 502 503 // Create array of raw contact IDs for contacts to add to the group 504 long[] membersToRemoveArray = convertToArray(mListMembersToRemove); 505 506 // Create the update intent (which includes the updated group name if necessary) 507 saveIntent = ContactSaveService.createGroupUpdateIntent(activity, mGroupId, 508 getUpdatedName(), membersToAddArray, membersToRemoveArray, 509 activity.getClass(), GroupEditorActivity.ACTION_SAVE_COMPLETED); 510 } else { 511 throw new IllegalStateException("Invalid intent action type " + mAction); 512 } 513 activity.startService(saveIntent); 514 return true; 515 } 516 517 public void onSaveCompleted(boolean hadChanges, int saveMode, Uri groupUri) { 518 boolean success = groupUri != null; 519 Log.d(TAG, "onSaveCompleted(" + saveMode + ", " + groupUri + ")"); 520 if (hadChanges) { 521 Toast.makeText(mContext, success ? R.string.groupSavedToast : 522 R.string.groupSavedErrorToast, Toast.LENGTH_SHORT).show(); 523 } 524 switch (saveMode) { 525 case SaveMode.CLOSE: 526 case SaveMode.HOME: 527 final Intent resultIntent; 528 final int resultCode; 529 if (success && groupUri != null) { 530 final String requestAuthority = 531 groupUri == null ? null : groupUri.getAuthority(); 532 533 resultIntent = new Intent(); 534 if (LEGACY_CONTACTS_AUTHORITY.equals(requestAuthority)) { 535 // Build legacy Uri when requested by caller 536 final long groupId = ContentUris.parseId(groupUri); 537 final Uri legacyContentUri = Uri.parse("content://contacts/groups"); 538 final Uri legacyUri = ContentUris.withAppendedId( 539 legacyContentUri, groupId); 540 resultIntent.setData(legacyUri); 541 } else { 542 // Otherwise pass back the given Uri 543 resultIntent.setData(groupUri); 544 } 545 546 resultCode = Activity.RESULT_OK; 547 } else { 548 resultCode = Activity.RESULT_CANCELED; 549 resultIntent = null; 550 } 551 // It is already saved, so prevent that it is saved again 552 mStatus = Status.CLOSING; 553 if (mListener != null) { 554 mListener.onSaveFinished(resultCode, resultIntent); 555 } 556 break; 557 case SaveMode.RELOAD: 558 // TODO: Handle reloading the group list 559 default: 560 throw new IllegalStateException("Unsupported save mode " + saveMode); 561 } 562 } 563 564 private boolean hasValidGroupName() { 565 return mGroupNameView != null && !TextUtils.isEmpty(mGroupNameView.getText()); 566 } 567 568 private boolean hasNameChange() { 569 return mGroupNameView != null && 570 !mGroupNameView.getText().toString().equals(mOriginalGroupName); 571 } 572 573 private boolean hasMembershipChange() { 574 return mListMembersToAdd.size() > 0 || mListMembersToRemove.size() > 0; 575 } 576 577 /** 578 * Returns the group's new name or null if there is no change from the 579 * original name that was loaded for the group. 580 */ 581 private String getUpdatedName() { 582 String groupNameFromTextView = mGroupNameView.getText().toString(); 583 if (groupNameFromTextView.equals(mOriginalGroupName)) { 584 // No name change, so return null 585 return null; 586 } 587 return groupNameFromTextView; 588 } 589 590 private static long[] convertToArray(List<Member> listMembers) { 591 int size = listMembers.size(); 592 long[] membersArray = new long[size]; 593 for (int i = 0; i < size; i++) { 594 membersArray[i] = listMembers.get(i).getRawContactId(); 595 } 596 return membersArray; 597 } 598 599 private void addExistingMembers(List<Member> members, List<Long> listContactIds) { 600 mListToDisplay.addAll(members); 601 mMemberListAdapter.notifyDataSetChanged(); 602 603 // Update the autocomplete adapter (if there is one) so these contacts don't get suggested 604 if (mAutoCompleteAdapter != null) { 605 mAutoCompleteAdapter.updateExistingMembersList(listContactIds); 606 } 607 } 608 609 private void addMember(Member member) { 610 // Update the display list 611 mListMembersToAdd.add(member); 612 mListToDisplay.add(member); 613 mMemberListAdapter.notifyDataSetChanged(); 614 615 // Update the autocomplete adapter so the contact doesn't get suggested again 616 mAutoCompleteAdapter.addNewMember(member.getContactId()); 617 } 618 619 private void removeMember(Member member) { 620 // If the contact was just added during this session, remove it from the list of 621 // members to add 622 if (mListMembersToAdd.contains(member)) { 623 mListMembersToAdd.remove(member); 624 } else { 625 // Otherwise this contact was already part of the existing list of contacts, 626 // so we need to do a content provider deletion operation 627 mListMembersToRemove.add(member); 628 } 629 // In either case, update the UI so the contact is no longer in the list of 630 // members 631 mListToDisplay.remove(member); 632 mMemberListAdapter.notifyDataSetChanged(); 633 634 // Update the autocomplete adapter so the contact can get suggested again 635 mAutoCompleteAdapter.removeMember(member.getContactId()); 636 } 637 638 /** 639 * The listener for the group metadata (i.e. group name, account type, and account name) loader. 640 */ 641 private final LoaderManager.LoaderCallbacks<Cursor> mGroupMetaDataLoaderListener = 642 new LoaderCallbacks<Cursor>() { 643 644 @Override 645 public CursorLoader onCreateLoader(int id, Bundle args) { 646 return new GroupMetaDataLoader(mContext, mGroupUri); 647 } 648 649 @Override 650 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 651 bindGroupMetaData(data); 652 653 // Load existing members 654 getLoaderManager().initLoader(LOADER_EXISTING_MEMBERS, null, 655 mGroupMemberListLoaderListener); 656 } 657 658 @Override 659 public void onLoaderReset(Loader<Cursor> loader) {} 660 }; 661 662 /** 663 * The loader listener for the list of existing group members. 664 */ 665 private final LoaderManager.LoaderCallbacks<Cursor> mGroupMemberListLoaderListener = 666 new LoaderCallbacks<Cursor>() { 667 668 @Override 669 public CursorLoader onCreateLoader(int id, Bundle args) { 670 return new GroupMemberLoader(mContext, mGroupId); 671 } 672 673 @Override 674 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 675 List<Long> listContactIds = new ArrayList<Long>(); 676 List<Member> listExistingMembers = new ArrayList<Member>(); 677 try { 678 data.moveToPosition(-1); 679 while (data.moveToNext()) { 680 long contactId = data.getLong(GroupMemberLoader.CONTACT_ID_COLUMN_INDEX); 681 long rawContactId = data.getLong(GroupMemberLoader.RAW_CONTACT_ID_COLUMN_INDEX); 682 String lookupKey = data.getString( 683 GroupMemberLoader.CONTACT_LOOKUP_KEY_COLUMN_INDEX); 684 String displayName = data.getString( 685 GroupMemberLoader.CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX); 686 String photoUri = data.getString( 687 GroupMemberLoader.CONTACT_PHOTO_URI_COLUMN_INDEX); 688 listExistingMembers.add(new Member(rawContactId, lookupKey, contactId, 689 displayName, photoUri)); 690 listContactIds.add(contactId); 691 } 692 } finally { 693 data.close(); 694 } 695 696 // Update the display list 697 addExistingMembers(listExistingMembers, listContactIds); 698 699 // No more updates 700 // TODO: move to a runnable 701 getLoaderManager().destroyLoader(LOADER_EXISTING_MEMBERS); 702 } 703 704 @Override 705 public void onLoaderReset(Loader<Cursor> loader) {} 706 }; 707 708 /** 709 * The listener to load a summary of details for a contact. 710 */ 711 // TODO: Remove this step because showing the aggregate contact can be confusing when the user 712 // just selected a raw contact 713 private final LoaderManager.LoaderCallbacks<Cursor> mContactLoaderListener = 714 new LoaderCallbacks<Cursor>() { 715 716 private long mRawContactId; 717 718 @Override 719 public CursorLoader onCreateLoader(int id, Bundle args) { 720 String memberId = args.getString(MEMBER_LOOKUP_URI_KEY); 721 mRawContactId = args.getLong(MEMBER_RAW_CONTACT_ID_KEY); 722 return new CursorLoader(mContext, Uri.withAppendedPath(Contacts.CONTENT_URI, memberId), 723 PROJECTION_CONTACT, null, null, null); 724 } 725 726 @Override 727 public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { 728 // Retrieve the contact data fields that will be sufficient to update the adapter with 729 // a new entry for this contact 730 Member member = null; 731 try { 732 cursor.moveToFirst(); 733 long contactId = cursor.getLong(CONTACT_ID_COLUMN_INDEX); 734 String displayName = cursor.getString(CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX); 735 String lookupKey = cursor.getString(CONTACT_LOOKUP_KEY_COLUMN_INDEX); 736 String photoUri = cursor.getString(CONTACT_PHOTO_URI_COLUMN_INDEX); 737 getLoaderManager().destroyLoader(LOADER_NEW_GROUP_MEMBER); 738 member = new Member(mRawContactId, lookupKey, contactId, displayName, photoUri); 739 } finally { 740 cursor.close(); 741 } 742 743 if (member == null) { 744 return; 745 } 746 747 // Otherwise continue adding the member to list of members 748 addMember(member); 749 } 750 751 @Override 752 public void onLoaderReset(Loader<Cursor> loader) {} 753 }; 754 755 /** 756 * This represents a single member of the current group. 757 */ 758 public static class Member { 759 // TODO: Switch to just dealing with raw contact IDs everywhere if possible 760 private final long mRawContactId; 761 private final long mContactId; 762 private final Uri mLookupUri; 763 private final String mDisplayName; 764 private final Uri mPhotoUri; 765 766 public Member(long rawContactId, String lookupKey, long contactId, String displayName, 767 String photoUri) { 768 mRawContactId = rawContactId; 769 mContactId = contactId; 770 mLookupUri = Contacts.getLookupUri(contactId, lookupKey); 771 mDisplayName = displayName; 772 mPhotoUri = (photoUri != null) ? Uri.parse(photoUri) : null; 773 } 774 775 public long getRawContactId() { 776 return mRawContactId; 777 } 778 779 public long getContactId() { 780 return mContactId; 781 } 782 783 public Uri getLookupUri() { 784 return mLookupUri; 785 } 786 787 public String getDisplayName() { 788 return mDisplayName; 789 } 790 791 public Uri getPhotoUri() { 792 return mPhotoUri; 793 } 794 795 @Override 796 public boolean equals(Object object) { 797 if (object instanceof Member) { 798 Member otherMember = (Member) object; 799 return otherMember != null && Objects.equal(mLookupUri, otherMember.getLookupUri()); 800 } 801 return false; 802 } 803 } 804 805 /** 806 * This adapter displays a list of members for the current group being edited. 807 */ 808 private final class MemberListAdapter extends BaseAdapter { 809 810 private boolean mIsGroupMembershipEditable = true; 811 812 @Override 813 public View getView(int position, View convertView, ViewGroup parent) { 814 View result; 815 if (convertView == null) { 816 result = mLayoutInflater.inflate(mIsGroupMembershipEditable ? 817 R.layout.group_member_item : R.layout.external_group_member_item, 818 parent, false); 819 } else { 820 result = convertView; 821 } 822 final Member member = getItem(position); 823 824 QuickContactBadge badge = (QuickContactBadge) result.findViewById(R.id.badge); 825 badge.assignContactUri(member.getLookupUri()); 826 827 TextView name = (TextView) result.findViewById(R.id.name); 828 name.setText(member.getDisplayName()); 829 830 View deleteButton = result.findViewById(R.id.delete_button_container); 831 if (deleteButton != null) { 832 deleteButton.setOnClickListener(new OnClickListener() { 833 @Override 834 public void onClick(View v) { 835 removeMember(member); 836 } 837 }); 838 } 839 840 mPhotoManager.loadPhoto(badge, member.getPhotoUri()); 841 return result; 842 } 843 844 @Override 845 public int getCount() { 846 return mListToDisplay.size(); 847 } 848 849 @Override 850 public Member getItem(int position) { 851 return mListToDisplay.get(position); 852 } 853 854 @Override 855 public long getItemId(int position) { 856 return position; 857 } 858 859 public void setIsGroupMembershipEditable(boolean editable) { 860 mIsGroupMembershipEditable = editable; 861 } 862 } 863} 864