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