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