GroupEditorFragment.java revision f75900ed2bafa5411c46e8fde0b8dccbc6753176
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 !TextUtils.isEmpty(mGroupNameView.getText()); 566 } 567 568 private boolean hasNameChange() { 569 return !mGroupNameView.getText().toString().equals(mOriginalGroupName); 570 } 571 572 private boolean hasMembershipChange() { 573 return mListMembersToAdd.size() > 0 || mListMembersToRemove.size() > 0; 574 } 575 576 /** 577 * Returns the group's new name or null if there is no change from the 578 * original name that was loaded for the group. 579 */ 580 private String getUpdatedName() { 581 String groupNameFromTextView = mGroupNameView.getText().toString(); 582 if (groupNameFromTextView.equals(mOriginalGroupName)) { 583 // No name change, so return null 584 return null; 585 } 586 return groupNameFromTextView; 587 } 588 589 private static long[] convertToArray(List<Member> listMembers) { 590 int size = listMembers.size(); 591 long[] membersArray = new long[size]; 592 for (int i = 0; i < size; i++) { 593 membersArray[i] = listMembers.get(i).getRawContactId(); 594 } 595 return membersArray; 596 } 597 598 private void addExistingMembers(List<Member> members, List<Long> listContactIds) { 599 mListToDisplay.addAll(members); 600 mMemberListAdapter.notifyDataSetChanged(); 601 602 // Update the autocomplete adapter (if there is one) so these contacts don't get suggested 603 if (mAutoCompleteAdapter != null) { 604 mAutoCompleteAdapter.updateExistingMembersList(listContactIds); 605 } 606 } 607 608 private void addMember(Member member) { 609 // Update the display list 610 mListMembersToAdd.add(member); 611 mListToDisplay.add(member); 612 mMemberListAdapter.notifyDataSetChanged(); 613 614 // Update the autocomplete adapter so the contact doesn't get suggested again 615 mAutoCompleteAdapter.addNewMember(member.getContactId()); 616 } 617 618 private void removeMember(Member member) { 619 // If the contact was just added during this session, remove it from the list of 620 // members to add 621 if (mListMembersToAdd.contains(member)) { 622 mListMembersToAdd.remove(member); 623 } else { 624 // Otherwise this contact was already part of the existing list of contacts, 625 // so we need to do a content provider deletion operation 626 mListMembersToRemove.add(member); 627 } 628 // In either case, update the UI so the contact is no longer in the list of 629 // members 630 mListToDisplay.remove(member); 631 mMemberListAdapter.notifyDataSetChanged(); 632 633 // Update the autocomplete adapter so the contact can get suggested again 634 mAutoCompleteAdapter.removeMember(member.getContactId()); 635 } 636 637 /** 638 * The listener for the group metadata (i.e. group name, account type, and account name) loader. 639 */ 640 private final LoaderManager.LoaderCallbacks<Cursor> mGroupMetaDataLoaderListener = 641 new LoaderCallbacks<Cursor>() { 642 643 @Override 644 public CursorLoader onCreateLoader(int id, Bundle args) { 645 return new GroupMetaDataLoader(mContext, mGroupUri); 646 } 647 648 @Override 649 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 650 bindGroupMetaData(data); 651 652 // Load existing members 653 getLoaderManager().initLoader(LOADER_EXISTING_MEMBERS, null, 654 mGroupMemberListLoaderListener); 655 } 656 657 @Override 658 public void onLoaderReset(Loader<Cursor> loader) {} 659 }; 660 661 /** 662 * The loader listener for the list of existing group members. 663 */ 664 private final LoaderManager.LoaderCallbacks<Cursor> mGroupMemberListLoaderListener = 665 new LoaderCallbacks<Cursor>() { 666 667 @Override 668 public CursorLoader onCreateLoader(int id, Bundle args) { 669 return new GroupMemberLoader(mContext, mGroupId); 670 } 671 672 @Override 673 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 674 List<Long> listContactIds = new ArrayList<Long>(); 675 List<Member> listExistingMembers = new ArrayList<Member>(); 676 try { 677 data.moveToPosition(-1); 678 while (data.moveToNext()) { 679 long contactId = data.getLong(GroupMemberLoader.CONTACT_ID_COLUMN_INDEX); 680 long rawContactId = data.getLong(GroupMemberLoader.RAW_CONTACT_ID_COLUMN_INDEX); 681 String lookupKey = data.getString( 682 GroupMemberLoader.CONTACT_LOOKUP_KEY_COLUMN_INDEX); 683 String displayName = data.getString( 684 GroupMemberLoader.CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX); 685 String photoUri = data.getString( 686 GroupMemberLoader.CONTACT_PHOTO_URI_COLUMN_INDEX); 687 listExistingMembers.add(new Member(rawContactId, lookupKey, contactId, 688 displayName, photoUri)); 689 listContactIds.add(contactId); 690 } 691 } finally { 692 data.close(); 693 } 694 695 // Update the display list 696 addExistingMembers(listExistingMembers, listContactIds); 697 698 // No more updates 699 // TODO: move to a runnable 700 getLoaderManager().destroyLoader(LOADER_EXISTING_MEMBERS); 701 } 702 703 @Override 704 public void onLoaderReset(Loader<Cursor> loader) {} 705 }; 706 707 /** 708 * The listener to load a summary of details for a contact. 709 */ 710 // TODO: Remove this step because showing the aggregate contact can be confusing when the user 711 // just selected a raw contact 712 private final LoaderManager.LoaderCallbacks<Cursor> mContactLoaderListener = 713 new LoaderCallbacks<Cursor>() { 714 715 private long mRawContactId; 716 717 @Override 718 public CursorLoader onCreateLoader(int id, Bundle args) { 719 String memberId = args.getString(MEMBER_LOOKUP_URI_KEY); 720 mRawContactId = args.getLong(MEMBER_RAW_CONTACT_ID_KEY); 721 return new CursorLoader(mContext, Uri.withAppendedPath(Contacts.CONTENT_URI, memberId), 722 PROJECTION_CONTACT, null, null, null); 723 } 724 725 @Override 726 public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { 727 // Retrieve the contact data fields that will be sufficient to update the adapter with 728 // a new entry for this contact 729 Member member = null; 730 try { 731 cursor.moveToFirst(); 732 long contactId = cursor.getLong(CONTACT_ID_COLUMN_INDEX); 733 String displayName = cursor.getString(CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX); 734 String lookupKey = cursor.getString(CONTACT_LOOKUP_KEY_COLUMN_INDEX); 735 String photoUri = cursor.getString(CONTACT_PHOTO_URI_COLUMN_INDEX); 736 getLoaderManager().destroyLoader(LOADER_NEW_GROUP_MEMBER); 737 member = new Member(mRawContactId, lookupKey, contactId, displayName, photoUri); 738 } finally { 739 cursor.close(); 740 } 741 742 if (member == null) { 743 return; 744 } 745 746 // Otherwise continue adding the member to list of members 747 addMember(member); 748 } 749 750 @Override 751 public void onLoaderReset(Loader<Cursor> loader) {} 752 }; 753 754 /** 755 * This represents a single member of the current group. 756 */ 757 public static class Member { 758 // TODO: Switch to just dealing with raw contact IDs everywhere if possible 759 private final long mRawContactId; 760 private final long mContactId; 761 private final Uri mLookupUri; 762 private final String mDisplayName; 763 private final Uri mPhotoUri; 764 765 public Member(long rawContactId, String lookupKey, long contactId, String displayName, 766 String photoUri) { 767 mRawContactId = rawContactId; 768 mContactId = contactId; 769 mLookupUri = Contacts.getLookupUri(contactId, lookupKey); 770 mDisplayName = displayName; 771 mPhotoUri = (photoUri != null) ? Uri.parse(photoUri) : null; 772 } 773 774 public long getRawContactId() { 775 return mRawContactId; 776 } 777 778 public long getContactId() { 779 return mContactId; 780 } 781 782 public Uri getLookupUri() { 783 return mLookupUri; 784 } 785 786 public String getDisplayName() { 787 return mDisplayName; 788 } 789 790 public Uri getPhotoUri() { 791 return mPhotoUri; 792 } 793 794 @Override 795 public boolean equals(Object object) { 796 if (object instanceof Member) { 797 Member otherMember = (Member) object; 798 return otherMember != null && Objects.equal(mLookupUri, otherMember.getLookupUri()); 799 } 800 return false; 801 } 802 } 803 804 /** 805 * This adapter displays a list of members for the current group being edited. 806 */ 807 private final class MemberListAdapter extends BaseAdapter { 808 809 private boolean mIsGroupMembershipEditable = true; 810 811 @Override 812 public View getView(int position, View convertView, ViewGroup parent) { 813 View result; 814 if (convertView == null) { 815 result = mLayoutInflater.inflate(mIsGroupMembershipEditable ? 816 R.layout.group_member_item : R.layout.external_group_member_item, 817 parent, false); 818 } else { 819 result = convertView; 820 } 821 final Member member = getItem(position); 822 823 QuickContactBadge badge = (QuickContactBadge) result.findViewById(R.id.badge); 824 badge.assignContactUri(member.getLookupUri()); 825 826 TextView name = (TextView) result.findViewById(R.id.name); 827 name.setText(member.getDisplayName()); 828 829 View deleteButton = result.findViewById(R.id.delete_button_container); 830 if (deleteButton != null) { 831 deleteButton.setOnClickListener(new OnClickListener() { 832 @Override 833 public void onClick(View v) { 834 removeMember(member); 835 } 836 }); 837 } 838 839 mPhotoManager.loadPhoto(badge, member.getPhotoUri()); 840 return result; 841 } 842 843 @Override 844 public int getCount() { 845 return mListToDisplay.size(); 846 } 847 848 @Override 849 public Member getItem(int position) { 850 return mListToDisplay.get(position); 851 } 852 853 @Override 854 public long getItemId(int position) { 855 return position; 856 } 857 858 public void setIsGroupMembershipEditable(boolean editable) { 859 mIsGroupMembershipEditable = editable; 860 } 861 } 862} 863