GroupEditorFragment.java revision c6b8afe730255537978f2c938cca6986cae63c34
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.ContactLoader; 20import com.android.contacts.ContactPhotoManager; 21import com.android.contacts.ContactSaveService; 22import com.android.contacts.GroupMemberLoader; 23import com.android.contacts.GroupMetaDataLoader; 24import com.android.contacts.R; 25import com.android.contacts.activities.GroupEditorActivity; 26import com.android.contacts.editor.ContactEditorFragment.SaveMode; 27import com.android.contacts.group.SuggestedMemberListAdapter.SuggestedMember; 28import com.android.contacts.model.AccountType; 29import com.android.contacts.model.AccountTypeManager; 30import com.android.contacts.model.DataKind; 31import com.android.contacts.model.EntityDelta; 32import com.android.contacts.model.EntityDelta.ValuesDelta; 33import com.android.contacts.model.EntityDeltaList; 34import com.android.contacts.model.EntityModifier; 35import com.android.internal.util.Objects; 36 37import android.accounts.Account; 38import android.app.Activity; 39import android.app.AlertDialog; 40import android.app.Dialog; 41import android.app.DialogFragment; 42import android.app.Fragment; 43import android.app.LoaderManager; 44import android.app.LoaderManager.LoaderCallbacks; 45import android.content.ContentResolver; 46import android.content.ContentUris; 47import android.content.Context; 48import android.content.CursorLoader; 49import android.content.DialogInterface; 50import android.content.Intent; 51import android.content.Loader; 52import android.database.Cursor; 53import android.net.Uri; 54import android.os.Bundle; 55import android.provider.ContactsContract.Contacts; 56import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 57import android.provider.ContactsContract.RawContacts; 58import android.text.TextUtils; 59import android.util.Log; 60import android.view.LayoutInflater; 61import android.view.Menu; 62import android.view.MenuInflater; 63import android.view.MenuItem; 64import android.view.View; 65import android.view.View.OnClickListener; 66import android.view.ViewGroup; 67import android.widget.AdapterView; 68import android.widget.AdapterView.OnItemClickListener; 69import android.widget.AutoCompleteTextView; 70import android.widget.BaseAdapter; 71import android.widget.EditText; 72import android.widget.ImageView; 73import android.widget.ListView; 74import android.widget.QuickContactBadge; 75import android.widget.TextView; 76import android.widget.Toast; 77 78import java.util.ArrayList; 79import java.util.List; 80 81// TODO: Use savedInstanceState 82public class GroupEditorFragment extends Fragment { 83 84 private static final String TAG = "GroupEditorFragment"; 85 86 private static final String LEGACY_CONTACTS_AUTHORITY = "contacts"; 87 88 public static interface Listener { 89 /** 90 * Group metadata was not found, close the fragment now. 91 */ 92 public void onGroupNotFound(); 93 94 /** 95 * User has tapped Revert, close the fragment now. 96 */ 97 void onReverted(); 98 99 /** 100 * Title has been determined. 101 */ 102 void onTitleLoaded(int resourceId); 103 104 /** 105 * Contact was saved and the Fragment can now be closed safely. 106 */ 107 void onSaveFinished(int resultCode, Intent resultIntent, boolean navigateHome); 108 } 109 110 private static final int LOADER_GROUP_METADATA = 1; 111 private static final int LOADER_EXISTING_MEMBERS = 2; 112 private static final int LOADER_NEW_GROUP_MEMBER = 3; 113 private static final int FULL_LOADER_NEW_GROUP_MEMBER = 4; 114 115 public static final String SAVE_MODE_EXTRA_KEY = "saveMode"; 116 117 private static final String MEMBER_LOOKUP_URI_KEY = "memberLookupUri"; 118 private static final String MEMBER_ACTION_KEY = "memberAction"; 119 120 private static final int ADD_MEMBER = 0; 121 private static final int REMOVE_MEMBER = 1; 122 123 protected static final String[] PROJECTION_CONTACT = new String[] { 124 Contacts._ID, // 0 125 Contacts.DISPLAY_NAME_PRIMARY, // 1 126 Contacts.DISPLAY_NAME_ALTERNATIVE, // 2 127 Contacts.SORT_KEY_PRIMARY, // 3 128 Contacts.STARRED, // 4 129 Contacts.CONTACT_PRESENCE, // 5 130 Contacts.CONTACT_CHAT_CAPABILITY, // 6 131 Contacts.PHOTO_ID, // 7 132 Contacts.PHOTO_THUMBNAIL_URI, // 8 133 Contacts.LOOKUP_KEY, // 9 134 Contacts.PHONETIC_NAME, // 10 135 Contacts.HAS_PHONE_NUMBER, // 11 136 Contacts.IS_USER_PROFILE, // 12 137 }; 138 139 protected static final int CONTACT_ID_COLUMN_INDEX = 0; 140 protected static final int CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX = 1; 141 protected static final int CONTACT_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX = 2; 142 protected static final int CONTACT_SORT_KEY_PRIMARY_COLUMN_INDEX = 3; 143 protected static final int CONTACT_STARRED_COLUMN_INDEX = 4; 144 protected static final int CONTACT_PRESENCE_STATUS_COLUMN_INDEX = 5; 145 protected static final int CONTACT_CHAT_CAPABILITY_COLUMN_INDEX = 6; 146 protected static final int CONTACT_PHOTO_ID_COLUMN_INDEX = 7; 147 protected static final int CONTACT_PHOTO_URI_COLUMN_INDEX = 8; 148 protected static final int CONTACT_LOOKUP_KEY_COLUMN_INDEX = 9; 149 protected static final int CONTACT_PHONETIC_NAME_COLUMN_INDEX = 10; 150 protected static final int CONTACT_HAS_PHONE_COLUMN_INDEX = 11; 151 protected static final int CONTACT_IS_USER_PROFILE = 12; 152 153 /** 154 * Modes that specify the status of the editor 155 */ 156 public enum Status { 157 LOADING, // Loader is fetching the data 158 EDITING, // Not currently busy. We are waiting forthe user to enter data. 159 SAVING, // Data is currently being saved 160 CLOSING // Prevents any more saves 161 } 162 163 private Context mContext; 164 private String mAction; 165 private Uri mGroupUri; 166 private long mGroupId; 167 private Listener mListener; 168 169 private Status mStatus; 170 171 private View mRootView; 172 private ListView mListView; 173 private LayoutInflater mLayoutInflater; 174 175 private EditText mGroupNameView; 176 private ImageView mAccountIcon; 177 private TextView mAccountTypeTextView; 178 private TextView mAccountNameTextView; 179 private AutoCompleteTextView mAutoCompleteTextView; 180 181 private boolean mGroupNameIsReadOnly; 182 private String mAccountName; 183 private String mAccountType; 184 private String mOriginalGroupName = ""; 185 186 private MemberListAdapter mMemberListAdapter; 187 private ContactPhotoManager mPhotoManager; 188 189 private Member mMemberToRemove; 190 191 private ContentResolver mContentResolver; 192 private SuggestedMemberListAdapter mAutoCompleteAdapter; 193 194 public GroupEditorFragment() { 195 } 196 197 @Override 198 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { 199 setHasOptionsMenu(true); 200 201 mLayoutInflater = inflater; 202 mRootView = inflater.inflate(R.layout.group_editor_fragment, container, false); 203 204 mGroupNameView = (EditText) mRootView.findViewById(R.id.group_name); 205 mAccountIcon = (ImageView) mRootView.findViewById(R.id.account_icon); 206 mAccountTypeTextView = (TextView) mRootView.findViewById(R.id.account_type); 207 mAccountNameTextView = (TextView) mRootView.findViewById(R.id.account_name); 208 mAutoCompleteTextView = (AutoCompleteTextView) mRootView.findViewById( 209 R.id.add_member_field); 210 211 mListView = (ListView) mRootView.findViewById(android.R.id.list); 212 mListView.setAdapter(mMemberListAdapter); 213 214 return mRootView; 215 } 216 217 @Override 218 public void onAttach(Activity activity) { 219 super.onAttach(activity); 220 mContext = activity; 221 mPhotoManager = ContactPhotoManager.getInstance(mContext); 222 mMemberListAdapter = new MemberListAdapter(); 223 } 224 225 @Override 226 public void onActivityCreated(Bundle savedInstanceState) { 227 super.onActivityCreated(savedInstanceState); 228 229 // Edit an existing group 230 if (Intent.ACTION_EDIT.equals(mAction)) { 231 if (mListener != null) { 232 mListener.onTitleLoaded(R.string.editGroup_title_edit); 233 } 234 getLoaderManager().initLoader(LOADER_GROUP_METADATA, null, 235 mGroupMetaDataLoaderListener); 236 getLoaderManager().initLoader(LOADER_EXISTING_MEMBERS, null, 237 mGroupMemberListLoaderListener); 238 } else if (Intent.ACTION_INSERT.equals(mAction)) { 239 if (mListener != null) { 240 mListener.onTitleLoaded(R.string.editGroup_title_insert); 241 } 242 setupAccountSwitcher(); 243 mStatus = Status.EDITING; 244 // The user wants to create a new group, temporarily hide the "add members" text view 245 // TODO: Need to allow users to add members if it's a new group. Under the current 246 // approach, we can't add members because it needs a group ID in order to save, 247 // and we don't have a group ID for a new group until the whole group is saved. 248 mAutoCompleteTextView.setVisibility(View.GONE); 249 } else { 250 throw new IllegalArgumentException("Unknown Action String " + mAction + 251 ". Only support " + Intent.ACTION_EDIT + " or " + Intent.ACTION_INSERT); 252 } 253 } 254 255 public void setContentResolver(ContentResolver resolver) { 256 mContentResolver = resolver; 257 if (mAutoCompleteAdapter != null) { 258 mAutoCompleteAdapter.setContentResolver(mContentResolver); 259 } 260 } 261 262 /** 263 * Sets up the account header for a new group by taking the first account. 264 */ 265 private void setupAccountSwitcher() { 266 // TODO: Allow switching between valid accounts 267 final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(mContext); 268 final ArrayList<Account> accountsList = accountTypeManager.getAccounts(true); 269 if (accountsList.isEmpty()) { 270 return; 271 } 272 Account account = accountsList.get(0); 273 274 // Store account info for later 275 mAccountName = account.name; 276 mAccountType = account.type; 277 278 // Display account name 279 if (!TextUtils.isEmpty(mAccountName)) { 280 mAccountNameTextView.setText( 281 mContext.getString(R.string.from_account_format, mAccountName)); 282 } 283 // Display account type 284 final AccountType type = accountTypeManager.getAccountType(mAccountType); 285 mAccountTypeTextView.setText(type.getDisplayLabel(mContext)); 286 287 // Display account icon 288 mAccountIcon.setImageDrawable(type.getDisplayIcon(mContext)); 289 } 290 291 /** 292 * Sets up the account header for an existing group. 293 */ 294 private void setupAccountHeader() { 295 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 296 final AccountType accountType = accountTypes.getAccountType(mAccountType); 297 CharSequence accountTypeDisplayLabel = accountType.getDisplayLabel(mContext); 298 if (!TextUtils.isEmpty(mAccountName)) { 299 mAccountNameTextView.setText( 300 mContext.getString(R.string.from_account_format, mAccountName)); 301 } 302 mAccountTypeTextView.setText(accountTypeDisplayLabel); 303 mAccountIcon.setImageDrawable(accountType.getDisplayIcon(mContext)); 304 } 305 306 public void load(String action, Uri groupUri) { 307 mAction = action; 308 mGroupUri = groupUri; 309 mGroupId = (groupUri != null) ? ContentUris.parseId(mGroupUri) : 0; 310 } 311 312 private void bindGroupMetaData(Cursor cursor) { 313 if (cursor.getCount() == 0) { 314 if (mListener != null) { 315 mListener.onGroupNotFound(); 316 } 317 } 318 try { 319 cursor.moveToFirst(); 320 mOriginalGroupName = cursor.getString(GroupMetaDataLoader.TITLE); 321 mAccountName = cursor.getString(GroupMetaDataLoader.ACCOUNT_NAME); 322 mAccountType = cursor.getString(GroupMetaDataLoader.ACCOUNT_TYPE); 323 mGroupNameIsReadOnly = (cursor.getInt(GroupMetaDataLoader.IS_READ_ONLY) == 1); 324 } catch (Exception e) { 325 Log.i(TAG, "Group not found with URI: " + mGroupUri + " Closing activity now."); 326 if (mListener != null) { 327 mListener.onGroupNotFound(); 328 } 329 } finally { 330 cursor.close(); 331 } 332 // Setup the group metadata display (If the group name is ready only, don't let the user 333 // focus on the field). 334 mGroupNameView.setText(mOriginalGroupName); 335 mGroupNameView.setFocusable(!mGroupNameIsReadOnly); 336 setupAccountHeader(); 337 338 // Setup the group member suggestion adapter 339 mAutoCompleteAdapter = new SuggestedMemberListAdapter(getActivity(), 340 android.R.layout.simple_dropdown_item_1line); 341 mAutoCompleteAdapter.setContentResolver(mContentResolver); 342 mAutoCompleteAdapter.setAccountType(mAccountType); 343 mAutoCompleteAdapter.setAccountName(mAccountName); 344 mAutoCompleteTextView.setAdapter(mAutoCompleteAdapter); 345 mAutoCompleteTextView.setOnItemClickListener(new OnItemClickListener() { 346 @Override 347 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 348 SuggestedMember member = mAutoCompleteAdapter.getItem(position); 349 loadMemberToAddToGroup(String.valueOf(member.getContactId())); 350 351 // Update the autocomplete adapter so the contact doesn't get suggested again 352 mAutoCompleteAdapter.addNewMember(member.getContactId()); 353 354 // Clear out the text field 355 mAutoCompleteTextView.setText(""); 356 } 357 }); 358 } 359 360 public void loadMemberToAddToGroup(String contactId) { 361 Bundle args = new Bundle(); 362 args.putString(MEMBER_LOOKUP_URI_KEY, contactId); 363 args.putInt(MEMBER_ACTION_KEY, ADD_MEMBER); 364 getLoaderManager().restartLoader(LOADER_NEW_GROUP_MEMBER, args, mContactLoaderListener); 365 } 366 367 private void loadMemberToRemoveFromGroup(String lookupUri) { 368 Bundle args = new Bundle(); 369 args.putString(MEMBER_LOOKUP_URI_KEY, lookupUri); 370 args.putInt(MEMBER_ACTION_KEY, REMOVE_MEMBER); 371 getLoaderManager().restartLoader(FULL_LOADER_NEW_GROUP_MEMBER, args, 372 mDataLoaderListener); 373 } 374 375 public void finishAddMember(Uri lookupUri) { 376 Toast.makeText(mContext, mContext.getString(R.string.groupMembershipChangeSavedToast), 377 Toast.LENGTH_SHORT).show(); 378 getLoaderManager().destroyLoader(FULL_LOADER_NEW_GROUP_MEMBER); 379 } 380 381 public void finishRemoveMember(Uri lookupUri) { 382 Toast.makeText(mContext, mContext.getString(R.string.groupMembershipChangeSavedToast), 383 Toast.LENGTH_SHORT).show(); 384 getLoaderManager().destroyLoader(FULL_LOADER_NEW_GROUP_MEMBER); 385 mMemberListAdapter.removeMember(mMemberToRemove); 386 } 387 388 public void setListener(Listener value) { 389 mListener = value; 390 } 391 392 @Override 393 public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) { 394 inflater.inflate(R.menu.edit_group, menu); 395 } 396 397 @Override 398 public boolean onOptionsItemSelected(MenuItem item) { 399 switch (item.getItemId()) { 400 case R.id.menu_done: 401 return save(SaveMode.CLOSE); 402 case R.id.menu_discard: 403 return revert(); 404 } 405 return false; 406 } 407 408 private boolean revert() { 409 if (mGroupNameView.getText() != null && 410 mGroupNameView.getText().toString().equals(mOriginalGroupName)) { 411 doRevertAction(); 412 } else { 413 CancelEditDialogFragment.show(this); 414 } 415 return true; 416 } 417 418 private void doRevertAction() { 419 // When this Fragment is closed we don't want it to auto-save 420 mStatus = Status.CLOSING; 421 if (mListener != null) mListener.onReverted(); 422 } 423 424 public static class CancelEditDialogFragment extends DialogFragment { 425 426 public static void show(GroupEditorFragment fragment) { 427 CancelEditDialogFragment dialog = new CancelEditDialogFragment(); 428 dialog.setTargetFragment(fragment, 0); 429 dialog.show(fragment.getFragmentManager(), "cancelEditor"); 430 } 431 432 @Override 433 public Dialog onCreateDialog(Bundle savedInstanceState) { 434 AlertDialog dialog = new AlertDialog.Builder(getActivity()) 435 .setIconAttribute(android.R.attr.alertDialogIcon) 436 .setTitle(R.string.cancel_confirmation_dialog_title) 437 .setMessage(R.string.cancel_confirmation_dialog_message) 438 .setPositiveButton(R.string.discard, 439 new DialogInterface.OnClickListener() { 440 @Override 441 public void onClick(DialogInterface dialog, int whichButton) { 442 ((GroupEditorFragment) getTargetFragment()).doRevertAction(); 443 } 444 } 445 ) 446 .setNegativeButton(android.R.string.cancel, null) 447 .create(); 448 return dialog; 449 } 450 } 451 452 /** 453 * Saves or creates the group based on the mode, and if successful 454 * finishes the activity. This actually only handles saving the group name. 455 * @return true when successful 456 */ 457 public boolean save(int saveMode) { 458 if (!hasValidGroupName() || mStatus != Status.EDITING) { 459 return false; 460 } 461 462 // If we are about to close the editor - there is no need to refresh the data 463 if (saveMode == SaveMode.CLOSE) { 464 getLoaderManager().destroyLoader(LOADER_EXISTING_MEMBERS); 465 } 466 467 mStatus = Status.SAVING; 468 469 if (!hasChanges()) { 470 onSaveCompleted(false, saveMode, mGroupUri); 471 return true; 472 } 473 474 Activity activity = getActivity(); 475 // If the activity is not there anymore, then we can't continue with the save process. 476 if (activity == null) { 477 return false; 478 } 479 Intent saveIntent = null; 480 if (mAction == Intent.ACTION_INSERT) { 481 saveIntent = ContactSaveService.createNewGroupIntent(activity, 482 new Account(mAccountName, mAccountType), mGroupNameView.getText().toString(), 483 activity.getClass(), GroupEditorActivity.ACTION_SAVE_COMPLETED); 484 } else if (mAction == Intent.ACTION_EDIT) { 485 saveIntent = ContactSaveService.createGroupRenameIntent(activity, mGroupId, 486 mGroupNameView.getText().toString(), activity.getClass(), 487 GroupEditorActivity.ACTION_SAVE_COMPLETED); 488 } else { 489 throw new IllegalStateException("Invalid intent action type " + mAction); 490 } 491 activity.startService(saveIntent); 492 return true; 493 } 494 495 public void onSaveCompleted(boolean hadChanges, int saveMode, Uri groupUri) { 496 boolean success = groupUri != null; 497 Log.d(TAG, "onSaveCompleted(" + saveMode + ", " + groupUri + ")"); 498 if (hadChanges) { 499 Toast.makeText(mContext, success ? R.string.groupSavedToast : 500 R.string.groupSavedErrorToast, Toast.LENGTH_SHORT).show(); 501 } 502 switch (saveMode) { 503 case SaveMode.CLOSE: 504 case SaveMode.HOME: 505 final Intent resultIntent; 506 final int resultCode; 507 if (success && groupUri != null) { 508 final String requestAuthority = 509 groupUri == null ? null : groupUri.getAuthority(); 510 511 resultIntent = new Intent(); 512 if (LEGACY_CONTACTS_AUTHORITY.equals(requestAuthority)) { 513 // Build legacy Uri when requested by caller 514 final long groupId = ContentUris.parseId(groupUri); 515 final Uri legacyContentUri = Uri.parse("content://contacts/groups"); 516 final Uri legacyUri = ContentUris.withAppendedId( 517 legacyContentUri, groupId); 518 resultIntent.setData(legacyUri); 519 } else { 520 // Otherwise pass back the given Uri 521 resultIntent.setData(groupUri); 522 } 523 524 resultCode = Activity.RESULT_OK; 525 } else { 526 resultCode = Activity.RESULT_CANCELED; 527 resultIntent = null; 528 } 529 // It is already saved, so prevent that it is saved again 530 mStatus = Status.CLOSING; 531 if (mListener != null) { 532 mListener.onSaveFinished(resultCode, resultIntent, saveMode == SaveMode.HOME); 533 } 534 break; 535 case SaveMode.RELOAD: 536 // TODO: Handle reloading the group list 537 default: 538 throw new IllegalStateException("Unsupported save mode " + saveMode); 539 } 540 } 541 542 private boolean hasValidGroupName() { 543 return !TextUtils.isEmpty(mGroupNameView.getText()); 544 } 545 546 private boolean hasChanges() { 547 return mGroupNameView.getText() != null && 548 !mGroupNameView.getText().toString().equals(mOriginalGroupName); 549 } 550 551 /** 552 * The listener for the group metadata (i.e. group name, account type, and account name) loader. 553 */ 554 private final LoaderManager.LoaderCallbacks<Cursor> mGroupMetaDataLoaderListener = 555 new LoaderCallbacks<Cursor>() { 556 557 @Override 558 public CursorLoader onCreateLoader(int id, Bundle args) { 559 return new GroupMetaDataLoader(mContext, mGroupUri); 560 } 561 562 @Override 563 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 564 mStatus = Status.EDITING; 565 bindGroupMetaData(data); 566 } 567 568 @Override 569 public void onLoaderReset(Loader<Cursor> loader) {} 570 }; 571 572 /** 573 * The loader listener for the list of existing group members. 574 */ 575 private final LoaderManager.LoaderCallbacks<Cursor> mGroupMemberListLoaderListener = 576 new LoaderCallbacks<Cursor>() { 577 578 @Override 579 public CursorLoader onCreateLoader(int id, Bundle args) { 580 return new GroupMemberLoader(mContext, mGroupId); 581 } 582 583 @Override 584 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 585 List<Member> listMembers = new ArrayList<Member>(); 586 List<Long> listContactIds = new ArrayList<Long>(); 587 try { 588 data.moveToPosition(-1); 589 while (data.moveToNext()) { 590 long contactId = data.getLong(GroupMemberLoader.CONTACT_ID_COLUMN_INDEX); 591 String lookupKey = data.getString( 592 GroupMemberLoader.CONTACT_LOOKUP_KEY_COLUMN_INDEX); 593 String displayName = data.getString( 594 GroupMemberLoader.CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX); 595 String photoUri = data.getString( 596 GroupMemberLoader.CONTACT_PHOTO_URI_COLUMN_INDEX); 597 listMembers.add(new Member(lookupKey, contactId, displayName, photoUri)); 598 listContactIds.add(contactId); 599 } 600 } finally { 601 data.close(); 602 } 603 // Update the list of displayed existing members 604 mMemberListAdapter.updateExistingMembersList(listMembers); 605 // Update the autocomplete adapter 606 mAutoCompleteAdapter.updateExistingMembersList(listContactIds); 607 } 608 609 @Override 610 public void onLoaderReset(Loader<Cursor> loader) {} 611 }; 612 613 /** 614 * The listener to load a summary of details for a contact. 615 */ 616 private final LoaderManager.LoaderCallbacks<Cursor> mContactLoaderListener = 617 new LoaderCallbacks<Cursor>() { 618 619 private int mMemberAction; 620 621 @Override 622 public CursorLoader onCreateLoader(int id, Bundle args) { 623 String memberId = args.getString(MEMBER_LOOKUP_URI_KEY); 624 mMemberAction = args.getInt(MEMBER_ACTION_KEY); 625 return new CursorLoader(mContext, Uri.withAppendedPath(Contacts.CONTENT_URI, memberId), 626 PROJECTION_CONTACT, null, null, null); 627 } 628 629 @Override 630 public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { 631 // Retrieve the contact data fields that will be sufficient to update the adapter with 632 // a new entry for this contact 633 Member member = null; 634 try { 635 cursor.moveToFirst(); 636 long contactId = cursor.getLong(CONTACT_ID_COLUMN_INDEX); 637 String displayName = cursor.getString(CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX); 638 String lookupKey = cursor.getString(CONTACT_LOOKUP_KEY_COLUMN_INDEX); 639 String photoUri = cursor.getString(CONTACT_PHOTO_URI_COLUMN_INDEX); 640 getLoaderManager().destroyLoader(LOADER_NEW_GROUP_MEMBER); 641 member = new Member(lookupKey, contactId, displayName, photoUri); 642 } finally { 643 cursor.close(); 644 } 645 646 if (member == null) { 647 return; 648 } 649 650 // Don't do anything if the adapter already contains this member 651 // TODO: Come up with a better way to check membership using a DB query 652 if (mMemberListAdapter.contains(member)) { 653 Toast.makeText(getActivity(), getActivity().getString( 654 R.string.contactAlreadyInGroup), Toast.LENGTH_SHORT).show(); 655 return; 656 } 657 658 // Otherwise continue adding the member to list of members 659 mMemberListAdapter.addMember(member); 660 661 // Then start loading the full contact so that the change can be saved 662 // TODO: Combine these two loader steps into one. Either we get rid of the first loader 663 // (retrieving summary details) and just use the full contact loader, or find a way 664 // to save changes without loading the full contact 665 Bundle args = new Bundle(); 666 args.putString(MEMBER_LOOKUP_URI_KEY, member.getLookupUri().toString()); 667 args.putInt(MEMBER_ACTION_KEY, mMemberAction); 668 getLoaderManager().restartLoader(FULL_LOADER_NEW_GROUP_MEMBER, args, 669 mDataLoaderListener); 670 } 671 672 @Override 673 public void onLoaderReset(Loader<Cursor> loader) {} 674 }; 675 676 /** 677 * The listener for the loader that loads the full details of a contact so that when the data 678 * has arrived, the contact can be added or removed from the group. 679 */ 680 private final LoaderManager.LoaderCallbacks<ContactLoader.Result> mDataLoaderListener = 681 new LoaderCallbacks<ContactLoader.Result>() { 682 683 private int mMemberAction; 684 685 @Override 686 public Loader<ContactLoader.Result> onCreateLoader(int id, Bundle args) { 687 mMemberAction = args.getInt(MEMBER_ACTION_KEY); 688 String memberLookupUri = args.getString(MEMBER_LOOKUP_URI_KEY); 689 return new ContactLoader(mContext, Uri.parse(memberLookupUri)); 690 } 691 692 @Override 693 public void onLoadFinished(Loader<ContactLoader.Result> loader, ContactLoader.Result data) { 694 if (data == ContactLoader.Result.NOT_FOUND || data == ContactLoader.Result.ERROR) { 695 Log.i(TAG, "Contact was not found"); 696 return; 697 } 698 saveChange(data, mMemberAction); 699 } 700 701 public void onLoaderReset(Loader<ContactLoader.Result> loader) { 702 } 703 }; 704 705 private void saveChange(ContactLoader.Result data, int action) { 706 EntityDeltaList state = EntityDeltaList.fromIterator(data.getEntities().iterator()); 707 708 // We need a raw contact to save this group membership change to, so find the first valid 709 // {@link EntityDelta}. 710 // TODO: Find a better way to do this. This will not work if the group is associated with 711 // the other 712 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 713 AccountType type = null; 714 EntityDelta entity = null; 715 int size = state.size(); 716 for (int i = 0; i < size; i++) { 717 entity = state.get(i); 718 final ValuesDelta values = entity.getValues(); 719 if (!values.isVisible()) continue; 720 721 final String accountName = values.getAsString(RawContacts.ACCOUNT_NAME); 722 final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE); 723 type = accountTypes.getAccountType(accountType); 724 // If the account name and type match this group's properties and the account type is 725 // not an external type, then use this raw contact 726 if (mAccountName.equals(accountName) && mAccountType.equals(accountType) && 727 !type.isExternal()) { 728 break; 729 } 730 } 731 732 Intent intent = null; 733 switch (action) { 734 case ADD_MEMBER: 735 DataKind groupMembershipKind = type.getKindForMimetype( 736 GroupMembership.CONTENT_ITEM_TYPE); 737 ValuesDelta entry = EntityModifier.insertChild(entity, groupMembershipKind); 738 entry.put(GroupMembership.GROUP_ROW_ID, mGroupId); 739 // Form intent 740 intent = ContactSaveService.createSaveContactIntent(getActivity(), state, 741 SAVE_MODE_EXTRA_KEY, SaveMode.CLOSE, getActivity().getClass(), 742 GroupEditorActivity.ACTION_ADD_MEMBER_COMPLETED); 743 break; 744 case REMOVE_MEMBER: 745 // TODO: Check that the contact was in the group in the first place 746 ArrayList<ValuesDelta> entries = entity.getMimeEntries( 747 GroupMembership.CONTENT_ITEM_TYPE); 748 if (entries != null) { 749 for (ValuesDelta valuesDeltaEntry : entries) { 750 if (!valuesDeltaEntry.isDelete()) { 751 Long groupId = valuesDeltaEntry.getAsLong(GroupMembership.GROUP_ROW_ID); 752 if (groupId == mGroupId) { 753 valuesDeltaEntry.markDeleted(); 754 } 755 } 756 } 757 } 758 intent = ContactSaveService.createSaveContactIntent(getActivity(), state, 759 SAVE_MODE_EXTRA_KEY, SaveMode.CLOSE, getActivity().getClass(), 760 GroupEditorActivity.ACTION_REMOVE_MEMBER_COMPLETED); 761 break; 762 default: 763 throw new IllegalStateException("Invalid action for a group member " + action); 764 } 765 getActivity().startService(intent); 766 } 767 768 /** 769 * This represents a single member of the current group. 770 */ 771 public static class Member { 772 private final Uri mLookupUri; 773 private final String mDisplayName; 774 private final Uri mPhotoUri; 775 776 public Member(String lookupKey, long contactId, String displayName, String photoUri) { 777 mLookupUri = Contacts.getLookupUri(contactId, lookupKey); 778 mDisplayName = displayName; 779 mPhotoUri = (photoUri != null) ? Uri.parse(photoUri) : null; 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 List<Member> mNewMembersList = new ArrayList<Member>(); 810 private List<Member> mTotalList = new ArrayList<Member>(); 811 812 public boolean contains(Member member) { 813 return mTotalList.contains(member); 814 } 815 816 public void addMember(Member member) { 817 mNewMembersList.add(member); 818 mTotalList.add(member); 819 notifyDataSetChanged(); 820 } 821 822 public void removeMember(Member member) { 823 if (mNewMembersList.contains(member)) { 824 mNewMembersList.remove(member); 825 } 826 mTotalList.remove(member); 827 notifyDataSetChanged(); 828 } 829 830 public void updateExistingMembersList(List<Member> existingMembers) { 831 mTotalList.clear(); 832 mTotalList.addAll(mNewMembersList); 833 mTotalList.addAll(existingMembers); 834 notifyDataSetChanged(); 835 } 836 837 @Override 838 public View getView(int position, View convertView, ViewGroup parent) { 839 View result; 840 if (convertView == null) { 841 result = mLayoutInflater.inflate(R.layout.group_member_item, parent, false); 842 } else { 843 result = convertView; 844 } 845 final Member member = getItem(position); 846 847 QuickContactBadge badge = (QuickContactBadge) result.findViewById(R.id.badge); 848 badge.assignContactUri(member.getLookupUri()); 849 850 TextView name = (TextView) result.findViewById(R.id.name); 851 name.setText(member.getDisplayName()); 852 853 View deleteButton = result.findViewById(R.id.delete_button_container); 854 deleteButton.setOnClickListener(new OnClickListener() { 855 @Override 856 public void onClick(View v) { 857 loadMemberToRemoveFromGroup(member.getLookupUri().toString()); 858 // TODO: This is a hack to save the reference to the member that should be 859 // removed. This won't work if the user tries to remove multiple times in a row 860 // and reference is outdated. We actually need a hash map of member URIs to the 861 // actual Member object. Before dealing with hash map though, hopefully we can 862 // figure out how to batch save membership changes, which would eliminate the 863 // need for this variable. 864 mMemberToRemove = member; 865 } 866 }); 867 868 mPhotoManager.loadPhoto(badge, member.getPhotoUri()); 869 return result; 870 } 871 872 @Override 873 public int getCount() { 874 return mTotalList.size(); 875 } 876 877 @Override 878 public Member getItem(int position) { 879 return mTotalList.get(position); 880 } 881 882 @Override 883 public int getItemViewType(int position) { 884 return 0; 885 } 886 887 @Override 888 public int getViewTypeCount() { 889 return 1; 890 } 891 892 @Override 893 public long getItemId(int position) { 894 return -1; 895 } 896 897 @Override 898 public boolean areAllItemsEnabled() { 899 return false; 900 } 901 902 @Override 903 public boolean isEnabled(int position) { 904 return false; 905 } 906 } 907} 908