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