GroupEditorFragment.java revision ad8e14065996cf8c0f5e0d6bc90c7fa334fc227f
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 public static interface Listener { 96 /** 97 * Group metadata was not found, close the fragment now. 98 */ 99 public void onGroupNotFound(); 100 101 /** 102 * User has tapped Revert, close the fragment now. 103 */ 104 void onReverted(); 105 106 /** 107 * Title has been determined. 108 * 109 * TODO Remove this. No longer needed with the latest visual spec. 110 */ 111 void onTitleLoaded(int resourceId); 112 113 /** 114 * Contact was saved and the Fragment can now be closed safely. 115 */ 116 void onSaveFinished(int resultCode, Intent resultIntent); 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 195 private MemberListAdapter mMemberListAdapter; 196 private ContactPhotoManager mPhotoManager; 197 198 private ContentResolver mContentResolver; 199 private SuggestedMemberListAdapter mAutoCompleteAdapter; 200 201 private final List<Member> mListMembersToAdd = new ArrayList<Member>(); 202 private final List<Member> mListMembersToRemove = new ArrayList<Member>(); 203 private final List<Member> mListToDisplay = new ArrayList<Member>(); 204 205 public GroupEditorFragment() { 206 } 207 208 @Override 209 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { 210 setHasOptionsMenu(true); 211 mLayoutInflater = inflater; 212 mRootView = (ViewGroup) inflater.inflate(R.layout.group_editor_fragment, container, false); 213 return mRootView; 214 } 215 216 @Override 217 public void onAttach(Activity activity) { 218 super.onAttach(activity); 219 mContext = activity; 220 mPhotoManager = ContactPhotoManager.getInstance(mContext); 221 mMemberListAdapter = new MemberListAdapter(); 222 } 223 224 @Override 225 public void onActivityCreated(Bundle savedInstanceState) { 226 super.onActivityCreated(savedInstanceState); 227 228 if (savedInstanceState != null) { 229 // Just restore from the saved state. No loading. 230 onRestoreInstanceState(savedInstanceState); 231 if (mStatus == Status.SELECTING_ACCOUNT) { 232 // Account select dialog is showing. Don't setup the editor yet. 233 } else if (mStatus == Status.LOADING) { 234 startGroupMetaDataLoader(); 235 } else { 236 setupEditorForAccount(); 237 } 238 } else if (Intent.ACTION_EDIT.equals(mAction)) { 239 startGroupMetaDataLoader(); 240 } else if (Intent.ACTION_INSERT.equals(mAction)) { 241 final Account account = mIntentExtras == null ? null : 242 (Account) mIntentExtras.getParcelable(Intents.Insert.ACCOUNT); 243 final String dataSet = mIntentExtras == null ? null : 244 mIntentExtras.getString(Intents.Insert.DATA_SET); 245 246 if (account != null) { 247 // Account specified in Intent - no data set can be specified in this manner. 248 mAccountName = account.name; 249 mAccountType = account.type; 250 mDataSet = dataSet; 251 setupEditorForAccount(); 252 } else { 253 // No Account specified. Let the user choose from a disambiguation dialog. 254 selectAccountAndCreateGroup(); 255 } 256 } else { 257 throw new IllegalArgumentException("Unknown Action String " + mAction + 258 ". Only support " + Intent.ACTION_EDIT + " or " + Intent.ACTION_INSERT); 259 } 260 261 // Let the activity update the title. 262 if (mListener != null) { 263 mListener.onTitleLoaded(Intent.ACTION_EDIT.equals(mAction) 264 ? R.string.editGroup_title_edit 265 : R.string.editGroup_title_insert); 266 } 267 } 268 269 private void startGroupMetaDataLoader() { 270 mStatus = Status.LOADING; 271 getLoaderManager().initLoader(LOADER_GROUP_METADATA, null, 272 mGroupMetaDataLoaderListener); 273 } 274 275 @Override 276 public void onSaveInstanceState(Bundle outState) { 277 super.onSaveInstanceState(outState); 278 outState.putString(KEY_ACTION, mAction); 279 outState.putParcelable(KEY_GROUP_URI, mGroupUri); 280 outState.putLong(KEY_GROUP_ID, mGroupId); 281 282 outState.putSerializable(KEY_STATUS, mStatus); 283 outState.putString(KEY_ACCOUNT_NAME, mAccountName); 284 outState.putString(KEY_ACCOUNT_TYPE, mAccountType); 285 outState.putString(KEY_DATA_SET, mDataSet); 286 287 outState.putBoolean(KEY_GROUP_NAME_IS_READ_ONLY, mGroupNameIsReadOnly); 288 outState.putString(KEY_ORIGINAL_GROUP_NAME, mOriginalGroupName); 289 290 outState.putParcelableArray(KEY_MEMBERS_TO_ADD, Member.toArray(mListMembersToAdd)); 291 outState.putParcelableArray(KEY_MEMBERS_TO_REMOVE, Member.toArray(mListMembersToRemove)); 292 outState.putParcelableArray(KEY_MEMBERS_TO_DISPLAY, Member.toArray(mListToDisplay)); 293 } 294 295 private void onRestoreInstanceState(Bundle state) { 296 mAction = state.getString(KEY_ACTION); 297 mGroupUri = state.getParcelable(KEY_GROUP_URI); 298 mGroupId = state.getLong(KEY_GROUP_ID); 299 300 mStatus = (Status) state.getSerializable(KEY_STATUS); 301 mAccountName = state.getString(KEY_ACCOUNT_NAME); 302 mAccountType = state.getString(KEY_ACCOUNT_TYPE); 303 mDataSet = state.getString(KEY_DATA_SET); 304 305 mGroupNameIsReadOnly = state.getBoolean(KEY_GROUP_NAME_IS_READ_ONLY); 306 mOriginalGroupName = state.getString(KEY_ORIGINAL_GROUP_NAME); 307 308 Member.toList((Member[]) state.getParcelableArray(KEY_MEMBERS_TO_ADD), mListMembersToAdd); 309 Member.toList((Member[]) state.getParcelableArray(KEY_MEMBERS_TO_REMOVE), 310 mListMembersToRemove); 311 Member.toList((Member[]) state.getParcelableArray(KEY_MEMBERS_TO_DISPLAY), mListToDisplay); 312 } 313 314 public void setContentResolver(ContentResolver resolver) { 315 mContentResolver = resolver; 316 if (mAutoCompleteAdapter != null) { 317 mAutoCompleteAdapter.setContentResolver(mContentResolver); 318 } 319 } 320 321 private void selectAccountAndCreateGroup() { 322 final List<AccountWithDataSet> accounts = 323 AccountTypeManager.getInstance(mContext).getAccounts(true /* writeable */); 324 // No Accounts available 325 if (accounts.isEmpty()) { 326 throw new IllegalStateException("No accounts were found."); 327 } 328 329 // In the common case of a single account being writable, auto-select 330 // it without showing a dialog. 331 if (accounts.size() == 1) { 332 mAccountName = accounts.get(0).name; 333 mAccountType = accounts.get(0).type; 334 mDataSet = accounts.get(0).dataSet; 335 setupEditorForAccount(); 336 return; // Don't show a dialog. 337 } 338 339 mStatus = Status.SELECTING_ACCOUNT; 340 final SelectAccountDialogFragment dialog = new SelectAccountDialogFragment( 341 R.string.dialog_new_group_account); 342 dialog.setTargetFragment(this, 0); 343 dialog.show(getFragmentManager(), SelectAccountDialogFragment.TAG); 344 } 345 346 @Override 347 public void onAccountChosen(int requestCode, AccountWithDataSet account) { 348 mAccountName = account.name; 349 mAccountType = account.type; 350 mDataSet = account.dataSet; 351 setupEditorForAccount(); 352 } 353 354 @Override 355 public void onAccountSelectorCancelled() { 356 if (mListener != null) { 357 // Exit the fragment because we cannot continue without selecting an account 358 mListener.onGroupNotFound(); 359 } 360 } 361 362 private AccountType getAccountType() { 363 return AccountTypeManager.getInstance(mContext).getAccountType(mAccountType, mDataSet); 364 } 365 366 /** 367 * @return true if the group membership is editable on this account type. false otherwise, 368 * or account is not set yet. 369 */ 370 private boolean isGroupMembershipEditable() { 371 if (mAccountType == null) { 372 return false; 373 } 374 return getAccountType().isGroupMembershipEditable(); 375 } 376 377 /** 378 * Sets up the editor based on the group's account name and type. 379 */ 380 private void setupEditorForAccount() { 381 final AccountType accountType = getAccountType(); 382 final boolean editable = isGroupMembershipEditable(); 383 mMemberListAdapter.setIsGroupMembershipEditable(editable); 384 385 View editorView = mLayoutInflater.inflate(editable ? 386 R.layout.group_editor_view : R.layout.external_group_editor_view, mRootView, false); 387 388 mGroupNameView = (TextView) editorView.findViewById(R.id.group_name); 389 mAccountIcon = (ImageView) editorView.findViewById(R.id.account_icon); 390 mAccountTypeTextView = (TextView) editorView.findViewById(R.id.account_type); 391 mAccountNameTextView = (TextView) editorView.findViewById(R.id.account_name); 392 mAutoCompleteTextView = (AutoCompleteTextView) editorView.findViewById( 393 R.id.add_member_field); 394 395 mListView = (ListView) editorView.findViewById(android.R.id.list); 396 mListView.setAdapter(mMemberListAdapter); 397 398 // Setup the account header 399 CharSequence accountTypeDisplayLabel = accountType.getDisplayLabel(mContext); 400 if (!TextUtils.isEmpty(mAccountName)) { 401 mAccountNameTextView.setText( 402 mContext.getString(R.string.from_account_format, mAccountName)); 403 } 404 mAccountTypeTextView.setText(accountTypeDisplayLabel); 405 mAccountIcon.setImageDrawable(accountType.getDisplayIcon(mContext)); 406 407 // Setup the autocomplete adapter (for contacts to suggest to add to the group) based on the 408 // account name and type. For groups that cannot have membership edited, there will be no 409 // autocomplete text view. 410 if (mAutoCompleteTextView != null) { 411 mAutoCompleteAdapter = new SuggestedMemberListAdapter(mContext, 412 android.R.layout.simple_dropdown_item_1line); 413 mAutoCompleteAdapter.setContentResolver(mContentResolver); 414 mAutoCompleteAdapter.setAccountType(mAccountType); 415 mAutoCompleteAdapter.setAccountName(mAccountName); 416 mAutoCompleteAdapter.setDataSet(mDataSet); 417 mAutoCompleteTextView.setAdapter(mAutoCompleteAdapter); 418 mAutoCompleteTextView.setOnItemClickListener(new OnItemClickListener() { 419 @Override 420 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 421 SuggestedMember member = mAutoCompleteAdapter.getItem(position); 422 loadMemberToAddToGroup(member.getRawContactId(), 423 String.valueOf(member.getContactId())); 424 425 // Update the autocomplete adapter so the contact doesn't get suggested again 426 mAutoCompleteAdapter.addNewMember(member.getContactId()); 427 428 // Clear out the text field 429 mAutoCompleteTextView.setText(""); 430 } 431 }); 432 // Update the exempt list. (mListToDisplay might have been restored from the saved 433 // state.) 434 mAutoCompleteAdapter.updateExistingMembersList(mListToDisplay); 435 } 436 437 // If the group name is ready only, don't let the user focus on the field. 438 mGroupNameView.setFocusable(!mGroupNameIsReadOnly); 439 440 mRootView.addView(editorView); 441 mStatus = Status.EDITING; 442 } 443 444 public void load(String action, Uri groupUri, Bundle intentExtras) { 445 mAction = action; 446 mGroupUri = groupUri; 447 mGroupId = (groupUri != null) ? ContentUris.parseId(mGroupUri) : 0; 448 mIntentExtras = intentExtras; 449 } 450 451 private void bindGroupMetaData(Cursor cursor) { 452 if (!cursor.moveToFirst()) { 453 Log.i(TAG, "Group not found with URI: " + mGroupUri + " Closing activity now."); 454 if (mListener != null) { 455 mListener.onGroupNotFound(); 456 } 457 return; 458 } 459 mOriginalGroupName = cursor.getString(GroupMetaDataLoader.TITLE); 460 mAccountName = cursor.getString(GroupMetaDataLoader.ACCOUNT_NAME); 461 mAccountType = cursor.getString(GroupMetaDataLoader.ACCOUNT_TYPE); 462 mGroupNameIsReadOnly = (cursor.getInt(GroupMetaDataLoader.IS_READ_ONLY) == 1); 463 setupEditorForAccount(); 464 465 // Setup the group metadata display 466 mGroupNameView.setText(mOriginalGroupName); 467 } 468 469 public void loadMemberToAddToGroup(long rawContactId, String contactId) { 470 Bundle args = new Bundle(); 471 args.putLong(MEMBER_RAW_CONTACT_ID_KEY, rawContactId); 472 args.putString(MEMBER_LOOKUP_URI_KEY, contactId); 473 getLoaderManager().restartLoader(LOADER_NEW_GROUP_MEMBER, args, mContactLoaderListener); 474 } 475 476 public void setListener(Listener value) { 477 mListener = value; 478 } 479 480 public void onDoneClicked() { 481 if (isGroupMembershipEditable()) { 482 save(SaveMode.CLOSE); 483 } else { 484 // Just revert it. 485 doRevertAction(); 486 } 487 } 488 489 @Override 490 public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) { 491 inflater.inflate(R.menu.edit_group, menu); 492 } 493 494 @Override 495 public boolean onOptionsItemSelected(MenuItem item) { 496 switch (item.getItemId()) { 497 case R.id.menu_discard: 498 return revert(); 499 } 500 return false; 501 } 502 503 private boolean revert() { 504 if (!hasNameChange() && !hasMembershipChange()) { 505 doRevertAction(); 506 } else { 507 CancelEditDialogFragment.show(this); 508 } 509 return true; 510 } 511 512 private void doRevertAction() { 513 // When this Fragment is closed we don't want it to auto-save 514 mStatus = Status.CLOSING; 515 if (mListener != null) mListener.onReverted(); 516 } 517 518 public static class CancelEditDialogFragment extends DialogFragment { 519 520 public static void show(GroupEditorFragment fragment) { 521 CancelEditDialogFragment dialog = new CancelEditDialogFragment(); 522 dialog.setTargetFragment(fragment, 0); 523 dialog.show(fragment.getFragmentManager(), "cancelEditor"); 524 } 525 526 @Override 527 public Dialog onCreateDialog(Bundle savedInstanceState) { 528 AlertDialog dialog = new AlertDialog.Builder(getActivity()) 529 .setIconAttribute(android.R.attr.alertDialogIcon) 530 .setTitle(R.string.cancel_confirmation_dialog_title) 531 .setMessage(R.string.cancel_confirmation_dialog_message) 532 .setPositiveButton(R.string.discard, 533 new DialogInterface.OnClickListener() { 534 @Override 535 public void onClick(DialogInterface dialog, int whichButton) { 536 ((GroupEditorFragment) getTargetFragment()).doRevertAction(); 537 } 538 } 539 ) 540 .setNegativeButton(android.R.string.cancel, null) 541 .create(); 542 return dialog; 543 } 544 } 545 546 /** 547 * Saves or creates the group based on the mode, and if successful 548 * finishes the activity. This actually only handles saving the group name. 549 * @return true when successful 550 */ 551 public boolean save(int saveMode) { 552 if (!hasValidGroupName() || mStatus != Status.EDITING) { 553 return false; 554 } 555 556 // If we are about to close the editor - there is no need to refresh the data 557 if (saveMode == SaveMode.CLOSE) { 558 getLoaderManager().destroyLoader(LOADER_EXISTING_MEMBERS); 559 } 560 561 // If there are no changes, then go straight to onSaveCompleted() 562 if (!hasNameChange() && !hasMembershipChange()) { 563 onSaveCompleted(false, SaveMode.CLOSE, mGroupUri); 564 return true; 565 } 566 567 mStatus = Status.SAVING; 568 569 Activity activity = getActivity(); 570 // If the activity is not there anymore, then we can't continue with the save process. 571 if (activity == null) { 572 return false; 573 } 574 Intent saveIntent = null; 575 if (Intent.ACTION_INSERT.equals(mAction)) { 576 // Create array of raw contact IDs for contacts to add to the group 577 long[] membersToAddArray = convertToArray(mListMembersToAdd); 578 579 // Create the save intent to create the group and add members at the same time 580 saveIntent = ContactSaveService.createNewGroupIntent(activity, 581 new AccountWithDataSet(mAccountName, mAccountType, mDataSet), 582 mGroupNameView.getText().toString(), 583 membersToAddArray, activity.getClass(), 584 GroupEditorActivity.ACTION_SAVE_COMPLETED); 585 } else if (Intent.ACTION_EDIT.equals(mAction)) { 586 // Create array of raw contact IDs for contacts to add to the group 587 long[] membersToAddArray = convertToArray(mListMembersToAdd); 588 589 // Create array of raw contact IDs for contacts to add to the group 590 long[] membersToRemoveArray = convertToArray(mListMembersToRemove); 591 592 // Create the update intent (which includes the updated group name if necessary) 593 saveIntent = ContactSaveService.createGroupUpdateIntent(activity, mGroupId, 594 getUpdatedName(), membersToAddArray, membersToRemoveArray, 595 activity.getClass(), GroupEditorActivity.ACTION_SAVE_COMPLETED); 596 } else { 597 throw new IllegalStateException("Invalid intent action type " + mAction); 598 } 599 activity.startService(saveIntent); 600 return true; 601 } 602 603 public void onSaveCompleted(boolean hadChanges, int saveMode, Uri groupUri) { 604 boolean success = groupUri != null; 605 Log.d(TAG, "onSaveCompleted(" + saveMode + ", " + groupUri + ")"); 606 if (hadChanges) { 607 Toast.makeText(mContext, success ? R.string.groupSavedToast : 608 R.string.groupSavedErrorToast, Toast.LENGTH_SHORT).show(); 609 } 610 switch (saveMode) { 611 case SaveMode.CLOSE: 612 case SaveMode.HOME: 613 final Intent resultIntent; 614 final int resultCode; 615 if (success && groupUri != null) { 616 final String requestAuthority = 617 groupUri == null ? null : groupUri.getAuthority(); 618 619 resultIntent = new Intent(); 620 if (LEGACY_CONTACTS_AUTHORITY.equals(requestAuthority)) { 621 // Build legacy Uri when requested by caller 622 final long groupId = ContentUris.parseId(groupUri); 623 final Uri legacyContentUri = Uri.parse("content://contacts/groups"); 624 final Uri legacyUri = ContentUris.withAppendedId( 625 legacyContentUri, groupId); 626 resultIntent.setData(legacyUri); 627 } else { 628 // Otherwise pass back the given Uri 629 resultIntent.setData(groupUri); 630 } 631 632 resultCode = Activity.RESULT_OK; 633 } else { 634 resultCode = Activity.RESULT_CANCELED; 635 resultIntent = null; 636 } 637 // It is already saved, so prevent that it is saved again 638 mStatus = Status.CLOSING; 639 if (mListener != null) { 640 mListener.onSaveFinished(resultCode, resultIntent); 641 } 642 break; 643 case SaveMode.RELOAD: 644 // TODO: Handle reloading the group list 645 default: 646 throw new IllegalStateException("Unsupported save mode " + saveMode); 647 } 648 } 649 650 private boolean hasValidGroupName() { 651 return mGroupNameView != null && !TextUtils.isEmpty(mGroupNameView.getText()); 652 } 653 654 private boolean hasNameChange() { 655 return mGroupNameView != null && 656 !mGroupNameView.getText().toString().equals(mOriginalGroupName); 657 } 658 659 private boolean hasMembershipChange() { 660 return mListMembersToAdd.size() > 0 || mListMembersToRemove.size() > 0; 661 } 662 663 /** 664 * Returns the group's new name or null if there is no change from the 665 * original name that was loaded for the group. 666 */ 667 private String getUpdatedName() { 668 String groupNameFromTextView = mGroupNameView.getText().toString(); 669 if (groupNameFromTextView.equals(mOriginalGroupName)) { 670 // No name change, so return null 671 return null; 672 } 673 return groupNameFromTextView; 674 } 675 676 private static long[] convertToArray(List<Member> listMembers) { 677 int size = listMembers.size(); 678 long[] membersArray = new long[size]; 679 for (int i = 0; i < size; i++) { 680 membersArray[i] = listMembers.get(i).getRawContactId(); 681 } 682 return membersArray; 683 } 684 685 private void addExistingMembers(List<Member> members) { 686 mListToDisplay.addAll(members); 687 mMemberListAdapter.notifyDataSetChanged(); 688 689 // Update the autocomplete adapter (if there is one) so these contacts don't get suggested 690 if (mAutoCompleteAdapter != null) { 691 mAutoCompleteAdapter.updateExistingMembersList(members); 692 } 693 } 694 695 private void addMember(Member member) { 696 // Update the display list 697 mListMembersToAdd.add(member); 698 mListToDisplay.add(member); 699 mMemberListAdapter.notifyDataSetChanged(); 700 701 // Update the autocomplete adapter so the contact doesn't get suggested again 702 mAutoCompleteAdapter.addNewMember(member.getContactId()); 703 } 704 705 private void removeMember(Member member) { 706 // If the contact was just added during this session, remove it from the list of 707 // members to add 708 if (mListMembersToAdd.contains(member)) { 709 mListMembersToAdd.remove(member); 710 } else { 711 // Otherwise this contact was already part of the existing list of contacts, 712 // so we need to do a content provider deletion operation 713 mListMembersToRemove.add(member); 714 } 715 // In either case, update the UI so the contact is no longer in the list of 716 // members 717 mListToDisplay.remove(member); 718 mMemberListAdapter.notifyDataSetChanged(); 719 720 // Update the autocomplete adapter so the contact can get suggested again 721 mAutoCompleteAdapter.removeMember(member.getContactId()); 722 } 723 724 /** 725 * The listener for the group metadata (i.e. group name, account type, and account name) loader. 726 */ 727 private final LoaderManager.LoaderCallbacks<Cursor> mGroupMetaDataLoaderListener = 728 new LoaderCallbacks<Cursor>() { 729 730 @Override 731 public CursorLoader onCreateLoader(int id, Bundle args) { 732 return new GroupMetaDataLoader(mContext, mGroupUri); 733 } 734 735 @Override 736 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 737 bindGroupMetaData(data); 738 739 // Load existing members 740 getLoaderManager().initLoader(LOADER_EXISTING_MEMBERS, null, 741 mGroupMemberListLoaderListener); 742 } 743 744 @Override 745 public void onLoaderReset(Loader<Cursor> loader) {} 746 }; 747 748 /** 749 * The loader listener for the list of existing group members. 750 */ 751 private final LoaderManager.LoaderCallbacks<Cursor> mGroupMemberListLoaderListener = 752 new LoaderCallbacks<Cursor>() { 753 754 @Override 755 public CursorLoader onCreateLoader(int id, Bundle args) { 756 return new GroupMemberLoader(mContext, mGroupId); 757 } 758 759 @Override 760 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 761 List<Member> listExistingMembers = new ArrayList<Member>(); 762 data.moveToPosition(-1); 763 while (data.moveToNext()) { 764 long contactId = data.getLong(GroupMemberLoader.CONTACT_ID_COLUMN_INDEX); 765 long rawContactId = data.getLong(GroupMemberLoader.RAW_CONTACT_ID_COLUMN_INDEX); 766 String lookupKey = data.getString( 767 GroupMemberLoader.CONTACT_LOOKUP_KEY_COLUMN_INDEX); 768 String displayName = data.getString( 769 GroupMemberLoader.CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX); 770 String photoUri = data.getString( 771 GroupMemberLoader.CONTACT_PHOTO_URI_COLUMN_INDEX); 772 listExistingMembers.add(new Member(rawContactId, lookupKey, contactId, 773 displayName, photoUri)); 774 } 775 776 // Update the display list 777 addExistingMembers(listExistingMembers); 778 779 // No more updates 780 // TODO: move to a runnable 781 getLoaderManager().destroyLoader(LOADER_EXISTING_MEMBERS); 782 } 783 784 @Override 785 public void onLoaderReset(Loader<Cursor> loader) {} 786 }; 787 788 /** 789 * The listener to load a summary of details for a contact. 790 */ 791 // TODO: Remove this step because showing the aggregate contact can be confusing when the user 792 // just selected a raw contact 793 private final LoaderManager.LoaderCallbacks<Cursor> mContactLoaderListener = 794 new LoaderCallbacks<Cursor>() { 795 796 private long mRawContactId; 797 798 @Override 799 public CursorLoader onCreateLoader(int id, Bundle args) { 800 String memberId = args.getString(MEMBER_LOOKUP_URI_KEY); 801 mRawContactId = args.getLong(MEMBER_RAW_CONTACT_ID_KEY); 802 return new CursorLoader(mContext, Uri.withAppendedPath(Contacts.CONTENT_URI, memberId), 803 PROJECTION_CONTACT, null, null, null); 804 } 805 806 @Override 807 public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { 808 if (!cursor.moveToFirst()) { 809 return; 810 } 811 // Retrieve the contact data fields that will be sufficient to update the adapter with 812 // a new entry for this contact 813 long contactId = cursor.getLong(CONTACT_ID_COLUMN_INDEX); 814 String displayName = cursor.getString(CONTACT_DISPLAY_NAME_PRIMARY_COLUMN_INDEX); 815 String lookupKey = cursor.getString(CONTACT_LOOKUP_KEY_COLUMN_INDEX); 816 String photoUri = cursor.getString(CONTACT_PHOTO_URI_COLUMN_INDEX); 817 getLoaderManager().destroyLoader(LOADER_NEW_GROUP_MEMBER); 818 Member member = new Member(mRawContactId, lookupKey, contactId, displayName, photoUri); 819 addMember(member); 820 } 821 822 @Override 823 public void onLoaderReset(Loader<Cursor> loader) {} 824 }; 825 826 /** 827 * This represents a single member of the current group. 828 */ 829 public static class Member implements Parcelable { 830 private static final Member[] EMPTY_ARRAY = new Member[0]; 831 832 // TODO: Switch to just dealing with raw contact IDs everywhere if possible 833 private final long mRawContactId; 834 private final long mContactId; 835 private final Uri mLookupUri; 836 private final String mDisplayName; 837 private final Uri mPhotoUri; 838 839 public Member(long rawContactId, String lookupKey, long contactId, String displayName, 840 String photoUri) { 841 mRawContactId = rawContactId; 842 mContactId = contactId; 843 mLookupUri = Contacts.getLookupUri(contactId, lookupKey); 844 mDisplayName = displayName; 845 mPhotoUri = (photoUri != null) ? Uri.parse(photoUri) : null; 846 } 847 848 public long getRawContactId() { 849 return mRawContactId; 850 } 851 852 public long getContactId() { 853 return mContactId; 854 } 855 856 public Uri getLookupUri() { 857 return mLookupUri; 858 } 859 860 public String getDisplayName() { 861 return mDisplayName; 862 } 863 864 public Uri getPhotoUri() { 865 return mPhotoUri; 866 } 867 868 @Override 869 public boolean equals(Object object) { 870 if (object instanceof Member) { 871 Member otherMember = (Member) object; 872 return otherMember != null && Objects.equal(mLookupUri, otherMember.getLookupUri()); 873 } 874 return false; 875 } 876 877 // Parcelable 878 @Override 879 public int describeContents() { 880 return 0; 881 } 882 883 @Override 884 public void writeToParcel(Parcel dest, int flags) { 885 dest.writeLong(mRawContactId); 886 dest.writeLong(mContactId); 887 dest.writeParcelable(mLookupUri, flags); 888 dest.writeString(mDisplayName); 889 dest.writeParcelable(mPhotoUri, flags); 890 } 891 892 private Member(Parcel in) { 893 mRawContactId = in.readLong(); 894 mContactId = in.readLong(); 895 mLookupUri = in.readParcelable(getClass().getClassLoader()); 896 mDisplayName = in.readString(); 897 mPhotoUri = in.readParcelable(getClass().getClassLoader()); 898 } 899 900 public static final Parcelable.Creator<Member> CREATOR = new Parcelable.Creator<Member>() { 901 public Member createFromParcel(Parcel in) { 902 return new Member(in); 903 } 904 905 public Member[] newArray(int size) { 906 return new Member[size]; 907 } 908 }; 909 910 /** Convert to an array */ 911 public static Member[] toArray(List<Member> list) { 912 return list.toArray(EMPTY_ARRAY); 913 } 914 915 /** 916 * Convert to a list. Instead of creating a new one, this method clears the passed list 917 * and adds elements to it. 918 */ 919 public static void toList(Member[] array, List<Member> list) { 920 list.clear(); 921 for (Member member : array) { 922 list.add(member); 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()); 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