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