ContactEditorFragment.java revision 36184f630cdb97d0a642de25f69962ef2abfedb1
1/* 2 * Copyright (C) 2010 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.editor; 18 19import android.accounts.Account; 20import android.app.Activity; 21import android.app.AlertDialog; 22import android.app.Dialog; 23import android.app.DialogFragment; 24import android.app.Fragment; 25import android.app.LoaderManager; 26import android.app.LoaderManager.LoaderCallbacks; 27import android.content.ContentUris; 28import android.content.ContentValues; 29import android.content.Context; 30import android.content.CursorLoader; 31import android.content.DialogInterface; 32import android.content.Intent; 33import android.content.Loader; 34import android.database.Cursor; 35import android.graphics.Bitmap; 36import android.graphics.BitmapFactory; 37import android.net.Uri; 38import android.os.Bundle; 39import android.os.SystemClock; 40import android.provider.ContactsContract.CommonDataKinds.Email; 41import android.provider.ContactsContract.CommonDataKinds.Event; 42import android.provider.ContactsContract.CommonDataKinds.Organization; 43import android.provider.ContactsContract.CommonDataKinds.Phone; 44import android.provider.ContactsContract.CommonDataKinds.Photo; 45import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 46import android.provider.ContactsContract.Contacts; 47import android.provider.ContactsContract.Groups; 48import android.provider.ContactsContract.Intents; 49import android.provider.ContactsContract.RawContacts; 50import android.util.Log; 51import android.view.LayoutInflater; 52import android.view.Menu; 53import android.view.MenuInflater; 54import android.view.MenuItem; 55import android.view.View; 56import android.view.ViewGroup; 57import android.widget.AdapterView; 58import android.widget.AdapterView.OnItemClickListener; 59import android.widget.BaseAdapter; 60import android.widget.LinearLayout; 61import android.widget.ListPopupWindow; 62import android.widget.Toast; 63 64import com.android.contacts.ContactSaveService; 65import com.android.contacts.GroupMetaDataLoader; 66import com.android.contacts.R; 67import com.android.contacts.activities.ContactEditorAccountsChangedActivity; 68import com.android.contacts.activities.ContactEditorActivity; 69import com.android.contacts.activities.JoinContactActivity; 70import com.android.contacts.detail.PhotoSelectionHandler; 71import com.android.contacts.editor.AggregationSuggestionEngine.Suggestion; 72import com.android.contacts.editor.Editor.EditorListener; 73import com.android.contacts.common.model.AccountTypeManager; 74import com.android.contacts.model.Contact; 75import com.android.contacts.model.ContactLoader; 76import com.android.contacts.model.RawContact; 77import com.android.contacts.model.RawContactDelta; 78import com.android.contacts.common.model.ValuesDelta; 79import com.android.contacts.model.RawContactDeltaList; 80import com.android.contacts.model.RawContactModifier; 81import com.android.contacts.common.model.account.AccountType; 82import com.android.contacts.common.model.account.AccountWithDataSet; 83import com.android.contacts.common.model.account.GoogleAccountType; 84import com.android.contacts.common.util.AccountsListAdapter; 85import com.android.contacts.common.util.AccountsListAdapter.AccountListFilter; 86import com.android.contacts.util.ContactPhotoUtils; 87import com.android.contacts.util.HelpUtils; 88import com.google.common.collect.ImmutableList; 89 90import java.io.File; 91import java.util.ArrayList; 92import java.util.Collections; 93import java.util.Comparator; 94import java.util.List; 95 96public class ContactEditorFragment extends Fragment implements 97 SplitContactConfirmationDialogFragment.Listener, 98 AggregationSuggestionEngine.Listener, AggregationSuggestionView.Listener, 99 RawContactReadOnlyEditorView.Listener { 100 101 private static final String TAG = ContactEditorFragment.class.getSimpleName(); 102 103 private static final int LOADER_DATA = 1; 104 private static final int LOADER_GROUPS = 2; 105 106 private static final String KEY_URI = "uri"; 107 private static final String KEY_ACTION = "action"; 108 private static final String KEY_EDIT_STATE = "state"; 109 private static final String KEY_RAW_CONTACT_ID_REQUESTING_PHOTO = "photorequester"; 110 private static final String KEY_VIEW_ID_GENERATOR = "viewidgenerator"; 111 private static final String KEY_CURRENT_PHOTO_FILE = "currentphotofile"; 112 private static final String KEY_CONTACT_ID_FOR_JOIN = "contactidforjoin"; 113 private static final String KEY_CONTACT_WRITABLE_FOR_JOIN = "contactwritableforjoin"; 114 private static final String KEY_SHOW_JOIN_SUGGESTIONS = "showJoinSuggestions"; 115 private static final String KEY_ENABLED = "enabled"; 116 private static final String KEY_STATUS = "status"; 117 private static final String KEY_NEW_LOCAL_PROFILE = "newLocalProfile"; 118 private static final String KEY_IS_USER_PROFILE = "isUserProfile"; 119 private static final String KEY_UPDATED_PHOTOS = "updatedPhotos"; 120 121 public static final String SAVE_MODE_EXTRA_KEY = "saveMode"; 122 123 124 /** 125 * An intent extra that forces the editor to add the edited contact 126 * to the default group (e.g. "My Contacts"). 127 */ 128 public static final String INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY = "addToDefaultDirectory"; 129 130 public static final String INTENT_EXTRA_NEW_LOCAL_PROFILE = "newLocalProfile"; 131 132 /** 133 * Modes that specify what the AsyncTask has to perform after saving 134 */ 135 public interface SaveMode { 136 /** 137 * Close the editor after saving 138 */ 139 public static final int CLOSE = 0; 140 141 /** 142 * Reload the data so that the user can continue editing 143 */ 144 public static final int RELOAD = 1; 145 146 /** 147 * Split the contact after saving 148 */ 149 public static final int SPLIT = 2; 150 151 /** 152 * Join another contact after saving 153 */ 154 public static final int JOIN = 3; 155 156 /** 157 * Navigate to Contacts Home activity after saving. 158 */ 159 public static final int HOME = 4; 160 } 161 162 private interface Status { 163 /** 164 * The loader is fetching data 165 */ 166 public static final int LOADING = 0; 167 168 /** 169 * Not currently busy. We are waiting for the user to enter data 170 */ 171 public static final int EDITING = 1; 172 173 /** 174 * The data is currently being saved. This is used to prevent more 175 * auto-saves (they shouldn't overlap) 176 */ 177 public static final int SAVING = 2; 178 179 /** 180 * Prevents any more saves. This is used if in the following cases: 181 * - After Save/Close 182 * - After Revert 183 * - After the user has accepted an edit suggestion 184 */ 185 public static final int CLOSING = 3; 186 187 /** 188 * Prevents saving while running a child activity. 189 */ 190 public static final int SUB_ACTIVITY = 4; 191 } 192 193 private static final int REQUEST_CODE_JOIN = 0; 194 private static final int REQUEST_CODE_ACCOUNTS_CHANGED = 1; 195 196 /** 197 * The raw contact for which we started "take photo" or "choose photo from gallery" most 198 * recently. Used to restore {@link #mCurrentPhotoHandler} after orientation change. 199 */ 200 private long mRawContactIdRequestingPhoto; 201 /** 202 * The {@link PhotoHandler} for the photo editor for the {@link #mRawContactIdRequestingPhoto} 203 * raw contact. 204 * 205 * A {@link PhotoHandler} is created for each photo editor in {@link #bindPhotoHandler}, but 206 * the only "active" one should get the activity result. This member represents the active 207 * one. 208 */ 209 private PhotoHandler mCurrentPhotoHandler; 210 211 private final EntityDeltaComparator mComparator = new EntityDeltaComparator(); 212 213 private Cursor mGroupMetaData; 214 215 private String mCurrentPhotoFile; 216 private Bundle mUpdatedPhotos = new Bundle(); 217 218 private Context mContext; 219 private String mAction; 220 private Uri mLookupUri; 221 private Bundle mIntentExtras; 222 private Listener mListener; 223 224 private long mContactIdForJoin; 225 private boolean mContactWritableForJoin; 226 227 private ContactEditorUtils mEditorUtils; 228 229 private LinearLayout mContent; 230 private RawContactDeltaList mState; 231 232 private ViewIdGenerator mViewIdGenerator; 233 234 private long mLoaderStartTime; 235 236 private int mStatus; 237 238 private AggregationSuggestionEngine mAggregationSuggestionEngine; 239 private long mAggregationSuggestionsRawContactId; 240 private View mAggregationSuggestionView; 241 242 private ListPopupWindow mAggregationSuggestionPopup; 243 244 private static final class AggregationSuggestionAdapter extends BaseAdapter { 245 private final Activity mActivity; 246 private final boolean mSetNewContact; 247 private final AggregationSuggestionView.Listener mListener; 248 private final List<Suggestion> mSuggestions; 249 250 public AggregationSuggestionAdapter(Activity activity, boolean setNewContact, 251 AggregationSuggestionView.Listener listener, List<Suggestion> suggestions) { 252 mActivity = activity; 253 mSetNewContact = setNewContact; 254 mListener = listener; 255 mSuggestions = suggestions; 256 } 257 258 @Override 259 public View getView(int position, View convertView, ViewGroup parent) { 260 Suggestion suggestion = (Suggestion) getItem(position); 261 LayoutInflater inflater = mActivity.getLayoutInflater(); 262 AggregationSuggestionView suggestionView = 263 (AggregationSuggestionView) inflater.inflate( 264 R.layout.aggregation_suggestions_item, null); 265 suggestionView.setNewContact(mSetNewContact); 266 suggestionView.setListener(mListener); 267 suggestionView.bindSuggestion(suggestion); 268 return suggestionView; 269 } 270 271 @Override 272 public long getItemId(int position) { 273 return position; 274 } 275 276 @Override 277 public Object getItem(int position) { 278 return mSuggestions.get(position); 279 } 280 281 @Override 282 public int getCount() { 283 return mSuggestions.size(); 284 } 285 } 286 287 private OnItemClickListener mAggregationSuggestionItemClickListener = 288 new OnItemClickListener() { 289 @Override 290 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 291 final AggregationSuggestionView suggestionView = (AggregationSuggestionView) view; 292 suggestionView.handleItemClickEvent(); 293 mAggregationSuggestionPopup.dismiss(); 294 mAggregationSuggestionPopup = null; 295 } 296 }; 297 298 private boolean mAutoAddToDefaultGroup; 299 300 private boolean mEnabled = true; 301 private boolean mRequestFocus; 302 private boolean mNewLocalProfile = false; 303 private boolean mIsUserProfile = false; 304 305 public ContactEditorFragment() { 306 } 307 308 public void setEnabled(boolean enabled) { 309 if (mEnabled != enabled) { 310 mEnabled = enabled; 311 if (mContent != null) { 312 int count = mContent.getChildCount(); 313 for (int i = 0; i < count; i++) { 314 mContent.getChildAt(i).setEnabled(enabled); 315 } 316 } 317 setAggregationSuggestionViewEnabled(enabled); 318 final Activity activity = getActivity(); 319 if (activity != null) activity.invalidateOptionsMenu(); 320 } 321 } 322 323 @Override 324 public void onAttach(Activity activity) { 325 super.onAttach(activity); 326 mContext = activity; 327 mEditorUtils = ContactEditorUtils.getInstance(mContext); 328 } 329 330 @Override 331 public void onStop() { 332 super.onStop(); 333 // If anything was left unsaved, save it now but keep the editor open. 334 if (!getActivity().isChangingConfigurations() && mStatus == Status.EDITING) { 335 save(SaveMode.RELOAD); 336 } 337 } 338 339 @Override 340 public void onDestroy() { 341 super.onDestroy(); 342 if (mAggregationSuggestionEngine != null) { 343 mAggregationSuggestionEngine.quit(); 344 } 345 } 346 347 @Override 348 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { 349 final View view = inflater.inflate(R.layout.contact_editor_fragment, container, false); 350 351 mContent = (LinearLayout) view.findViewById(R.id.editors); 352 353 setHasOptionsMenu(true); 354 355 return view; 356 } 357 358 @Override 359 public void onActivityCreated(Bundle savedInstanceState) { 360 super.onActivityCreated(savedInstanceState); 361 362 validateAction(mAction); 363 364 // Handle initial actions only when existing state missing 365 final boolean hasIncomingState = savedInstanceState != null; 366 367 if (mState == null) { 368 // The delta list may not have finished loading before orientation change happens. 369 // In this case, there will be a saved state but deltas will be missing. Reload from 370 // database. 371 if (Intent.ACTION_EDIT.equals(mAction)) { 372 // Either... 373 // 1) orientation change but load never finished. 374 // or 375 // 2) not an orientation change. data needs to be loaded for first time. 376 getLoaderManager().initLoader(LOADER_DATA, null, mDataLoaderListener); 377 } 378 } else { 379 // Orientation change, we already have mState, it was loaded by onCreate 380 bindEditors(); 381 } 382 383 if (!hasIncomingState) { 384 if (Intent.ACTION_INSERT.equals(mAction)) { 385 final Account account = mIntentExtras == null ? null : 386 (Account) mIntentExtras.getParcelable(Intents.Insert.ACCOUNT); 387 final String dataSet = mIntentExtras == null ? null : 388 mIntentExtras.getString(Intents.Insert.DATA_SET); 389 390 if (account != null) { 391 // Account specified in Intent 392 createContact(new AccountWithDataSet(account.name, account.type, dataSet)); 393 } else { 394 // No Account specified. Let the user choose 395 // Load Accounts async so that we can present them 396 selectAccountAndCreateContact(); 397 } 398 } 399 } 400 } 401 402 /** 403 * Checks if the requested action is valid. 404 * 405 * @param action The action to test. 406 * @throws IllegalArgumentException when the action is invalid. 407 */ 408 private void validateAction(String action) { 409 if (Intent.ACTION_EDIT.equals(action) || Intent.ACTION_INSERT.equals(action) || 410 ContactEditorActivity.ACTION_SAVE_COMPLETED.equals(action)) { 411 return; 412 } 413 throw new IllegalArgumentException("Unknown Action String " + mAction + 414 ". Only support " + Intent.ACTION_EDIT + " or " + Intent.ACTION_INSERT + " or " + 415 ContactEditorActivity.ACTION_SAVE_COMPLETED); 416 } 417 418 @Override 419 public void onStart() { 420 getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupLoaderListener); 421 super.onStart(); 422 } 423 424 public void load(String action, Uri lookupUri, Bundle intentExtras) { 425 mAction = action; 426 mLookupUri = lookupUri; 427 mIntentExtras = intentExtras; 428 mAutoAddToDefaultGroup = mIntentExtras != null 429 && mIntentExtras.containsKey(INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY); 430 mNewLocalProfile = mIntentExtras != null 431 && mIntentExtras.getBoolean(INTENT_EXTRA_NEW_LOCAL_PROFILE); 432 } 433 434 public void setListener(Listener value) { 435 mListener = value; 436 } 437 438 @Override 439 public void onCreate(Bundle savedState) { 440 if (savedState != null) { 441 // Restore mUri before calling super.onCreate so that onInitializeLoaders 442 // would already have a uri and an action to work with 443 mLookupUri = savedState.getParcelable(KEY_URI); 444 mAction = savedState.getString(KEY_ACTION); 445 } 446 447 super.onCreate(savedState); 448 449 if (savedState == null) { 450 // If savedState is non-null, onRestoreInstanceState() will restore the generator. 451 mViewIdGenerator = new ViewIdGenerator(); 452 } else { 453 // Read state from savedState. No loading involved here 454 mState = savedState.<RawContactDeltaList> getParcelable(KEY_EDIT_STATE); 455 mRawContactIdRequestingPhoto = savedState.getLong( 456 KEY_RAW_CONTACT_ID_REQUESTING_PHOTO); 457 mViewIdGenerator = savedState.getParcelable(KEY_VIEW_ID_GENERATOR); 458 mCurrentPhotoFile = savedState.getString(KEY_CURRENT_PHOTO_FILE); 459 mContactIdForJoin = savedState.getLong(KEY_CONTACT_ID_FOR_JOIN); 460 mContactWritableForJoin = savedState.getBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN); 461 mAggregationSuggestionsRawContactId = savedState.getLong(KEY_SHOW_JOIN_SUGGESTIONS); 462 mEnabled = savedState.getBoolean(KEY_ENABLED); 463 mStatus = savedState.getInt(KEY_STATUS); 464 mNewLocalProfile = savedState.getBoolean(KEY_NEW_LOCAL_PROFILE); 465 mIsUserProfile = savedState.getBoolean(KEY_IS_USER_PROFILE); 466 mUpdatedPhotos = savedState.getParcelable(KEY_UPDATED_PHOTOS); 467 } 468 } 469 470 public void setData(Contact data) { 471 // If we have already loaded data, we do not want to change it here to not confuse the user 472 if (mState != null) { 473 Log.v(TAG, "Ignoring background change. This will have to be rebased later"); 474 return; 475 } 476 477 // See if this edit operation needs to be redirected to a custom editor 478 ImmutableList<RawContact> rawContacts = data.getRawContacts(); 479 if (rawContacts.size() == 1) { 480 RawContact rawContact = rawContacts.get(0); 481 String type = rawContact.getAccountTypeString(); 482 String dataSet = rawContact.getDataSet(); 483 AccountType accountType = rawContact.getAccountType(mContext); 484 if (accountType.getEditContactActivityClassName() != null && 485 !accountType.areContactsWritable()) { 486 if (mListener != null) { 487 String name = rawContact.getAccountName(); 488 long rawContactId = rawContact.getId(); 489 mListener.onCustomEditContactActivityRequested( 490 new AccountWithDataSet(name, type, dataSet), 491 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId), 492 mIntentExtras, true); 493 } 494 return; 495 } 496 } 497 498 bindEditorsForExistingContact(data); 499 } 500 501 @Override 502 public void onExternalEditorRequest(AccountWithDataSet account, Uri uri) { 503 mListener.onCustomEditContactActivityRequested(account, uri, null, false); 504 } 505 506 private void bindEditorsForExistingContact(Contact contact) { 507 setEnabled(true); 508 509 mState = contact.createRawContactDeltaList(); 510 setIntentExtras(mIntentExtras); 511 mIntentExtras = null; 512 513 // For user profile, change the contacts query URI 514 mIsUserProfile = contact.isUserProfile(); 515 boolean localProfileExists = false; 516 517 if (mIsUserProfile) { 518 for (RawContactDelta state : mState) { 519 // For profile contacts, we need a different query URI 520 state.setProfileQueryUri(); 521 // Try to find a local profile contact 522 if (state.getValues().getAsString(RawContacts.ACCOUNT_TYPE) == null) { 523 localProfileExists = true; 524 } 525 } 526 // Editor should always present a local profile for editing 527 if (!localProfileExists) { 528 final RawContact rawContact = new RawContact(); 529 rawContact.setAccountToLocal(); 530 531 RawContactDelta insert = new RawContactDelta(ValuesDelta.fromAfter( 532 rawContact.getValues())); 533 insert.setProfileQueryUri(); 534 mState.add(insert); 535 } 536 } 537 mRequestFocus = true; 538 539 bindEditors(); 540 } 541 542 /** 543 * Merges extras from the intent. 544 */ 545 public void setIntentExtras(Bundle extras) { 546 if (extras == null || extras.size() == 0) { 547 return; 548 } 549 550 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 551 for (RawContactDelta state : mState) { 552 final AccountType type = state.getAccountType(accountTypes); 553 if (type.areContactsWritable()) { 554 // Apply extras to the first writable raw contact only 555 RawContactModifier.parseExtras(mContext, type, state, extras); 556 break; 557 } 558 } 559 } 560 561 private void selectAccountAndCreateContact() { 562 // If this is a local profile, then skip the logic about showing the accounts changed 563 // activity and create a phone-local contact. 564 if (mNewLocalProfile) { 565 createContact(null); 566 return; 567 } 568 569 // If there is no default account or the accounts have changed such that we need to 570 // prompt the user again, then launch the account prompt. 571 if (mEditorUtils.shouldShowAccountChangedNotification()) { 572 Intent intent = new Intent(mContext, ContactEditorAccountsChangedActivity.class); 573 mStatus = Status.SUB_ACTIVITY; 574 startActivityForResult(intent, REQUEST_CODE_ACCOUNTS_CHANGED); 575 } else { 576 // Otherwise, there should be a default account. Then either create a local contact 577 // (if default account is null) or create a contact with the specified account. 578 AccountWithDataSet defaultAccount = mEditorUtils.getDefaultAccount(); 579 if (defaultAccount == null) { 580 createContact(null); 581 } else { 582 createContact(defaultAccount); 583 } 584 } 585 } 586 587 /** 588 * Create a contact by automatically selecting the first account. If there's no available 589 * account, a device-local contact should be created. 590 */ 591 private void createContact() { 592 final List<AccountWithDataSet> accounts = 593 AccountTypeManager.getInstance(mContext).getAccounts(true); 594 // No Accounts available. Create a phone-local contact. 595 if (accounts.isEmpty()) { 596 createContact(null); 597 return; 598 } 599 600 // We have an account switcher in "create-account" screen, so don't need to ask a user to 601 // select an account here. 602 createContact(accounts.get(0)); 603 } 604 605 /** 606 * Shows account creation screen associated with a given account. 607 * 608 * @param account may be null to signal a device-local contact should be created. 609 */ 610 private void createContact(AccountWithDataSet account) { 611 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 612 final AccountType accountType = 613 accountTypes.getAccountType(account != null ? account.type : null, 614 account != null ? account.dataSet : null); 615 616 if (accountType.getCreateContactActivityClassName() != null) { 617 if (mListener != null) { 618 mListener.onCustomCreateContactActivityRequested(account, mIntentExtras); 619 } 620 } else { 621 bindEditorsForNewContact(account, accountType); 622 } 623 } 624 625 /** 626 * Removes a current editor ({@link #mState}) and rebinds new editor for a new account. 627 * Some of old data are reused with new restriction enforced by the new account. 628 * 629 * @param oldState Old data being edited. 630 * @param oldAccount Old account associated with oldState. 631 * @param newAccount New account to be used. 632 */ 633 private void rebindEditorsForNewContact( 634 RawContactDelta oldState, AccountWithDataSet oldAccount, 635 AccountWithDataSet newAccount) { 636 AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 637 AccountType oldAccountType = accountTypes.getAccountType( 638 oldAccount.type, oldAccount.dataSet); 639 AccountType newAccountType = accountTypes.getAccountType( 640 newAccount.type, newAccount.dataSet); 641 642 if (newAccountType.getCreateContactActivityClassName() != null) { 643 Log.w(TAG, "external activity called in rebind situation"); 644 if (mListener != null) { 645 mListener.onCustomCreateContactActivityRequested(newAccount, mIntentExtras); 646 } 647 } else { 648 mState = null; 649 bindEditorsForNewContact(newAccount, newAccountType, oldState, oldAccountType); 650 } 651 } 652 653 private void bindEditorsForNewContact(AccountWithDataSet account, 654 final AccountType accountType) { 655 bindEditorsForNewContact(account, accountType, null, null); 656 } 657 658 private void bindEditorsForNewContact(AccountWithDataSet newAccount, 659 final AccountType newAccountType, RawContactDelta oldState, 660 AccountType oldAccountType) { 661 mStatus = Status.EDITING; 662 663 final RawContact rawContact = new RawContact(); 664 if (newAccount != null) { 665 rawContact.setAccount(newAccount); 666 } else { 667 rawContact.setAccountToLocal(); 668 } 669 670 RawContactDelta insert = new RawContactDelta(ValuesDelta.fromAfter(rawContact.getValues())); 671 if (oldState == null) { 672 // Parse any values from incoming intent 673 RawContactModifier.parseExtras(mContext, newAccountType, insert, mIntentExtras); 674 } else { 675 RawContactModifier.migrateStateForNewContact(mContext, oldState, insert, 676 oldAccountType, newAccountType); 677 } 678 679 // Ensure we have some default fields (if the account type does not support a field, 680 // ensureKind will not add it, so it is safe to add e.g. Event) 681 RawContactModifier.ensureKindExists(insert, newAccountType, Phone.CONTENT_ITEM_TYPE); 682 RawContactModifier.ensureKindExists(insert, newAccountType, Email.CONTENT_ITEM_TYPE); 683 RawContactModifier.ensureKindExists(insert, newAccountType, Organization.CONTENT_ITEM_TYPE); 684 RawContactModifier.ensureKindExists(insert, newAccountType, Event.CONTENT_ITEM_TYPE); 685 RawContactModifier.ensureKindExists(insert, newAccountType, 686 StructuredPostal.CONTENT_ITEM_TYPE); 687 688 // Set the correct URI for saving the contact as a profile 689 if (mNewLocalProfile) { 690 insert.setProfileQueryUri(); 691 } 692 693 if (mState == null) { 694 // Create state if none exists yet 695 mState = RawContactDeltaList.fromSingle(insert); 696 } else { 697 // Add contact onto end of existing state 698 mState.add(insert); 699 } 700 701 mRequestFocus = true; 702 703 bindEditors(); 704 } 705 706 private void bindEditors() { 707 // bindEditors() can only bind views if there is data in mState, so immediately return 708 // if mState is null 709 if (mState == null) { 710 return; 711 } 712 713 // Sort the editors 714 Collections.sort(mState, mComparator); 715 716 // Remove any existing editors and rebuild any visible 717 mContent.removeAllViews(); 718 719 final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( 720 Context.LAYOUT_INFLATER_SERVICE); 721 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 722 int numRawContacts = mState.size(); 723 for (int i = 0; i < numRawContacts; i++) { 724 // TODO ensure proper ordering of entities in the list 725 final RawContactDelta rawContactDelta = mState.get(i); 726 if (!rawContactDelta.isVisible()) continue; 727 728 final AccountType type = rawContactDelta.getAccountType(accountTypes); 729 final long rawContactId = rawContactDelta.getRawContactId(); 730 731 final BaseRawContactEditorView editor; 732 if (!type.areContactsWritable()) { 733 editor = (BaseRawContactEditorView) inflater.inflate( 734 R.layout.raw_contact_readonly_editor_view, mContent, false); 735 ((RawContactReadOnlyEditorView) editor).setListener(this); 736 } else { 737 editor = (RawContactEditorView) inflater.inflate(R.layout.raw_contact_editor_view, 738 mContent, false); 739 } 740 if (Intent.ACTION_INSERT.equals(mAction) && numRawContacts == 1) { 741 final List<AccountWithDataSet> accounts = 742 AccountTypeManager.getInstance(mContext).getAccounts(true); 743 if (accounts.size() > 1 && !mNewLocalProfile) { 744 addAccountSwitcher(mState.get(0), editor); 745 } else { 746 disableAccountSwitcher(editor); 747 } 748 } else { 749 disableAccountSwitcher(editor); 750 } 751 752 editor.setEnabled(mEnabled); 753 754 mContent.addView(editor); 755 756 editor.setState(rawContactDelta, type, mViewIdGenerator, isEditingUserProfile()); 757 758 // Set up the photo handler. 759 bindPhotoHandler(editor, type, mState); 760 761 // If a new photo was chosen but not yet saved, we need to 762 // update the thumbnail to reflect this. 763 Bitmap bitmap = updatedBitmapForRawContact(rawContactId); 764 if (bitmap != null) editor.setPhotoBitmap(bitmap); 765 766 if (editor instanceof RawContactEditorView) { 767 final Activity activity = getActivity(); 768 final RawContactEditorView rawContactEditor = (RawContactEditorView) editor; 769 EditorListener listener = new EditorListener() { 770 771 @Override 772 public void onRequest(int request) { 773 if (activity.isFinishing()) { // Make sure activity is still running. 774 return; 775 } 776 if (request == EditorListener.FIELD_CHANGED && !isEditingUserProfile()) { 777 acquireAggregationSuggestions(activity, rawContactEditor); 778 } 779 } 780 781 @Override 782 public void onDeleteRequested(Editor removedEditor) { 783 } 784 }; 785 786 final TextFieldsEditorView nameEditor = rawContactEditor.getNameEditor(); 787 if (mRequestFocus) { 788 nameEditor.requestFocus(); 789 mRequestFocus = false; 790 } 791 nameEditor.setEditorListener(listener); 792 793 final TextFieldsEditorView phoneticNameEditor = 794 rawContactEditor.getPhoneticNameEditor(); 795 phoneticNameEditor.setEditorListener(listener); 796 rawContactEditor.setAutoAddToDefaultGroup(mAutoAddToDefaultGroup); 797 798 if (rawContactId == mAggregationSuggestionsRawContactId) { 799 acquireAggregationSuggestions(activity, rawContactEditor); 800 } 801 } 802 } 803 804 mRequestFocus = false; 805 806 bindGroupMetaData(); 807 808 // Show editor now that we've loaded state 809 mContent.setVisibility(View.VISIBLE); 810 811 // Refresh Action Bar as the visibility of the join command 812 // Activity can be null if we have been detached from the Activity 813 final Activity activity = getActivity(); 814 if (activity != null) activity.invalidateOptionsMenu(); 815 } 816 817 /** 818 * If we've stashed a temporary file containing a contact's new photo, 819 * decode it and return the bitmap. 820 * @param rawContactId identifies the raw-contact whose Bitmap we'll try to return. 821 * @return Bitmap of photo for specified raw-contact, or null 822 */ 823 private Bitmap updatedBitmapForRawContact(long rawContactId) { 824 String path = mUpdatedPhotos.getString(String.valueOf(rawContactId)); 825 return BitmapFactory.decodeFile(path); 826 } 827 828 private void bindPhotoHandler(BaseRawContactEditorView editor, AccountType type, 829 RawContactDeltaList state) { 830 final int mode; 831 if (type.areContactsWritable()) { 832 if (editor.hasSetPhoto()) { 833 if (hasMoreThanOnePhoto()) { 834 mode = PhotoActionPopup.Modes.PHOTO_ALLOW_PRIMARY; 835 } else { 836 mode = PhotoActionPopup.Modes.PHOTO_DISALLOW_PRIMARY; 837 } 838 } else { 839 mode = PhotoActionPopup.Modes.NO_PHOTO; 840 } 841 } else { 842 if (editor.hasSetPhoto() && hasMoreThanOnePhoto()) { 843 mode = PhotoActionPopup.Modes.READ_ONLY_ALLOW_PRIMARY; 844 } else { 845 // Read-only and either no photo or the only photo ==> no options 846 editor.getPhotoEditor().setEditorListener(null); 847 return; 848 } 849 } 850 final PhotoHandler photoHandler = new PhotoHandler(mContext, editor, mode, state); 851 editor.getPhotoEditor().setEditorListener( 852 (PhotoHandler.PhotoEditorListener) photoHandler.getListener()); 853 854 // Note a newly created raw contact gets some random negative ID, so any value is valid 855 // here. (i.e. don't check against -1 or anything.) 856 if (mRawContactIdRequestingPhoto == editor.getRawContactId()) { 857 mCurrentPhotoHandler = photoHandler; 858 } 859 } 860 861 private void bindGroupMetaData() { 862 if (mGroupMetaData == null) { 863 return; 864 } 865 866 int editorCount = mContent.getChildCount(); 867 for (int i = 0; i < editorCount; i++) { 868 BaseRawContactEditorView editor = (BaseRawContactEditorView) mContent.getChildAt(i); 869 editor.setGroupMetaData(mGroupMetaData); 870 } 871 } 872 873 private void saveDefaultAccountIfNecessary() { 874 // Verify that this is a newly created contact, that the contact is composed of only 875 // 1 raw contact, and that the contact is not a user profile. 876 if (!Intent.ACTION_INSERT.equals(mAction) && mState.size() == 1 && 877 !isEditingUserProfile()) { 878 return; 879 } 880 881 // Find the associated account for this contact (retrieve it here because there are 882 // multiple paths to creating a contact and this ensures we always have the correct 883 // account). 884 final RawContactDelta rawContactDelta = mState.get(0); 885 String name = rawContactDelta.getAccountName(); 886 String type = rawContactDelta.getAccountType(); 887 String dataSet = rawContactDelta.getDataSet(); 888 889 AccountWithDataSet account = (name == null || type == null) ? null : 890 new AccountWithDataSet(name, type, dataSet); 891 mEditorUtils.saveDefaultAndAllAccounts(account); 892 } 893 894 private void addAccountSwitcher( 895 final RawContactDelta currentState, BaseRawContactEditorView editor) { 896 final AccountWithDataSet currentAccount = new AccountWithDataSet( 897 currentState.getAccountName(), 898 currentState.getAccountType(), 899 currentState.getDataSet()); 900 final View accountView = editor.findViewById(R.id.account); 901 final View anchorView = editor.findViewById(R.id.account_container); 902 accountView.setOnClickListener(new View.OnClickListener() { 903 @Override 904 public void onClick(View v) { 905 final ListPopupWindow popup = new ListPopupWindow(mContext, null); 906 final AccountsListAdapter adapter = 907 new AccountsListAdapter(mContext, 908 AccountListFilter.ACCOUNTS_CONTACT_WRITABLE, currentAccount); 909 popup.setWidth(anchorView.getWidth()); 910 popup.setAnchorView(anchorView); 911 popup.setAdapter(adapter); 912 popup.setModal(true); 913 popup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); 914 popup.setOnItemClickListener(new AdapterView.OnItemClickListener() { 915 @Override 916 public void onItemClick(AdapterView<?> parent, View view, int position, 917 long id) { 918 popup.dismiss(); 919 AccountWithDataSet newAccount = adapter.getItem(position); 920 if (!newAccount.equals(currentAccount)) { 921 rebindEditorsForNewContact(currentState, currentAccount, newAccount); 922 } 923 } 924 }); 925 popup.show(); 926 } 927 }); 928 } 929 930 private void disableAccountSwitcher(BaseRawContactEditorView editor) { 931 // Remove the pressed state from the account header because the user cannot switch accounts 932 // on an existing contact 933 final View accountView = editor.findViewById(R.id.account); 934 accountView.setBackground(null); 935 accountView.setEnabled(false); 936 } 937 938 @Override 939 public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) { 940 inflater.inflate(R.menu.edit_contact, menu); 941 } 942 943 @Override 944 public void onPrepareOptionsMenu(Menu menu) { 945 // This supports the keyboard shortcut to save changes to a contact but shouldn't be visible 946 // because the custom action bar contains the "save" button now (not the overflow menu). 947 // TODO: Find a better way to handle shortcuts, i.e. onKeyDown()? 948 final MenuItem doneMenu = menu.findItem(R.id.menu_done); 949 final MenuItem splitMenu = menu.findItem(R.id.menu_split); 950 final MenuItem joinMenu = menu.findItem(R.id.menu_join); 951 final MenuItem helpMenu = menu.findItem(R.id.menu_help); 952 953 // Set visibility of menus 954 doneMenu.setVisible(false); 955 956 // Split only if more than one raw profile and not a user profile 957 splitMenu.setVisible(mState != null && mState.size() > 1 && !isEditingUserProfile()); 958 959 // Cannot join a user profile 960 joinMenu.setVisible(!isEditingUserProfile()); 961 962 // help menu depending on whether this is inserting or editing 963 if (Intent.ACTION_INSERT.equals(mAction)) { 964 // inserting 965 HelpUtils.prepareHelpMenuItem(mContext, helpMenu, R.string.help_url_people_add); 966 } else if (Intent.ACTION_EDIT.equals(mAction)) { 967 // editing 968 HelpUtils.prepareHelpMenuItem(mContext, helpMenu, R.string.help_url_people_edit); 969 } else { 970 // something else, so don't show the help menu 971 helpMenu.setVisible(false); 972 } 973 974 int size = menu.size(); 975 for (int i = 0; i < size; i++) { 976 menu.getItem(i).setEnabled(mEnabled); 977 } 978 } 979 980 @Override 981 public boolean onOptionsItemSelected(MenuItem item) { 982 switch (item.getItemId()) { 983 case R.id.menu_done: 984 return save(SaveMode.CLOSE); 985 case R.id.menu_discard: 986 return revert(); 987 case R.id.menu_split: 988 return doSplitContactAction(); 989 case R.id.menu_join: 990 return doJoinContactAction(); 991 } 992 return false; 993 } 994 995 private boolean doSplitContactAction() { 996 if (!hasValidState()) return false; 997 998 final SplitContactConfirmationDialogFragment dialog = 999 new SplitContactConfirmationDialogFragment(); 1000 dialog.setTargetFragment(this, 0); 1001 dialog.show(getFragmentManager(), SplitContactConfirmationDialogFragment.TAG); 1002 return true; 1003 } 1004 1005 private boolean doJoinContactAction() { 1006 if (!hasValidState()) { 1007 return false; 1008 } 1009 1010 // If we just started creating a new contact and haven't added any data, it's too 1011 // early to do a join 1012 if (mState.size() == 1 && mState.get(0).isContactInsert() && !hasPendingChanges()) { 1013 Toast.makeText(mContext, R.string.toast_join_with_empty_contact, 1014 Toast.LENGTH_LONG).show(); 1015 return true; 1016 } 1017 1018 return save(SaveMode.JOIN); 1019 } 1020 1021 /** 1022 * Check if our internal {@link #mState} is valid, usually checked before 1023 * performing user actions. 1024 */ 1025 private boolean hasValidState() { 1026 return mState != null && mState.size() > 0; 1027 } 1028 1029 /** 1030 * Return true if there are any edits to the current contact which need to 1031 * be saved. 1032 */ 1033 private boolean hasPendingChanges() { 1034 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 1035 return RawContactModifier.hasChanges(mState, accountTypes); 1036 } 1037 1038 /** 1039 * Saves or creates the contact based on the mode, and if successful 1040 * finishes the activity. 1041 */ 1042 public boolean save(int saveMode) { 1043 if (!hasValidState() || mStatus != Status.EDITING) { 1044 return false; 1045 } 1046 1047 // If we are about to close the editor - there is no need to refresh the data 1048 if (saveMode == SaveMode.CLOSE || saveMode == SaveMode.SPLIT) { 1049 getLoaderManager().destroyLoader(LOADER_DATA); 1050 } 1051 1052 mStatus = Status.SAVING; 1053 1054 if (!hasPendingChanges()) { 1055 if (mLookupUri == null && saveMode == SaveMode.RELOAD) { 1056 // We don't have anything to save and there isn't even an existing contact yet. 1057 // Nothing to do, simply go back to editing mode 1058 mStatus = Status.EDITING; 1059 return true; 1060 } 1061 onSaveCompleted(false, saveMode, mLookupUri != null, mLookupUri); 1062 return true; 1063 } 1064 1065 setEnabled(false); 1066 1067 // Store account as default account, only if this is a new contact 1068 saveDefaultAccountIfNecessary(); 1069 1070 // Save contact 1071 Intent intent = ContactSaveService.createSaveContactIntent(mContext, mState, 1072 SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(), 1073 ((Activity)mContext).getClass(), ContactEditorActivity.ACTION_SAVE_COMPLETED, 1074 mUpdatedPhotos); 1075 mContext.startService(intent); 1076 1077 // Don't try to save the same photos twice. 1078 mUpdatedPhotos = new Bundle(); 1079 1080 return true; 1081 } 1082 1083 public static class CancelEditDialogFragment extends DialogFragment { 1084 1085 public static void show(ContactEditorFragment fragment) { 1086 CancelEditDialogFragment dialog = new CancelEditDialogFragment(); 1087 dialog.setTargetFragment(fragment, 0); 1088 dialog.show(fragment.getFragmentManager(), "cancelEditor"); 1089 } 1090 1091 @Override 1092 public Dialog onCreateDialog(Bundle savedInstanceState) { 1093 AlertDialog dialog = new AlertDialog.Builder(getActivity()) 1094 .setIconAttribute(android.R.attr.alertDialogIcon) 1095 .setMessage(R.string.cancel_confirmation_dialog_message) 1096 .setPositiveButton(android.R.string.ok, 1097 new DialogInterface.OnClickListener() { 1098 @Override 1099 public void onClick(DialogInterface dialogInterface, int whichButton) { 1100 ((ContactEditorFragment)getTargetFragment()).doRevertAction(); 1101 } 1102 } 1103 ) 1104 .setNegativeButton(android.R.string.cancel, null) 1105 .create(); 1106 return dialog; 1107 } 1108 } 1109 1110 private boolean revert() { 1111 if (mState == null || !hasPendingChanges()) { 1112 doRevertAction(); 1113 } else { 1114 CancelEditDialogFragment.show(this); 1115 } 1116 return true; 1117 } 1118 1119 private void doRevertAction() { 1120 // When this Fragment is closed we don't want it to auto-save 1121 mStatus = Status.CLOSING; 1122 if (mListener != null) mListener.onReverted(); 1123 } 1124 1125 public void doSaveAction() { 1126 save(SaveMode.CLOSE); 1127 } 1128 1129 public void onJoinCompleted(Uri uri) { 1130 onSaveCompleted(false, SaveMode.RELOAD, uri != null, uri); 1131 } 1132 1133 public void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded, 1134 Uri contactLookupUri) { 1135 if (hadChanges) { 1136 if (saveSucceeded) { 1137 if (saveMode != SaveMode.JOIN) { 1138 Toast.makeText(mContext, R.string.contactSavedToast, Toast.LENGTH_SHORT).show(); 1139 } 1140 } else { 1141 Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show(); 1142 } 1143 } 1144 switch (saveMode) { 1145 case SaveMode.CLOSE: 1146 case SaveMode.HOME: 1147 final Intent resultIntent; 1148 if (saveSucceeded && contactLookupUri != null) { 1149 final String requestAuthority = 1150 mLookupUri == null ? null : mLookupUri.getAuthority(); 1151 1152 final String legacyAuthority = "contacts"; 1153 1154 resultIntent = new Intent(); 1155 resultIntent.setAction(Intent.ACTION_VIEW); 1156 if (legacyAuthority.equals(requestAuthority)) { 1157 // Build legacy Uri when requested by caller 1158 final long contactId = ContentUris.parseId(Contacts.lookupContact( 1159 mContext.getContentResolver(), contactLookupUri)); 1160 final Uri legacyContentUri = Uri.parse("content://contacts/people"); 1161 final Uri legacyUri = ContentUris.withAppendedId( 1162 legacyContentUri, contactId); 1163 resultIntent.setData(legacyUri); 1164 } else { 1165 // Otherwise pass back a lookup-style Uri 1166 resultIntent.setData(contactLookupUri); 1167 } 1168 1169 } else { 1170 resultIntent = null; 1171 } 1172 // It is already saved, so prevent that it is saved again 1173 mStatus = Status.CLOSING; 1174 if (mListener != null) mListener.onSaveFinished(resultIntent); 1175 break; 1176 1177 case SaveMode.RELOAD: 1178 case SaveMode.JOIN: 1179 if (saveSucceeded && contactLookupUri != null) { 1180 // If it was a JOIN, we are now ready to bring up the join activity. 1181 if (saveMode == SaveMode.JOIN && hasValidState()) { 1182 showJoinAggregateActivity(contactLookupUri); 1183 } 1184 1185 // If this was in INSERT, we are changing into an EDIT now. 1186 // If it already was an EDIT, we are changing to the new Uri now 1187 mState = null; 1188 load(Intent.ACTION_EDIT, contactLookupUri, null); 1189 mStatus = Status.LOADING; 1190 getLoaderManager().restartLoader(LOADER_DATA, null, mDataLoaderListener); 1191 } 1192 break; 1193 1194 case SaveMode.SPLIT: 1195 mStatus = Status.CLOSING; 1196 if (mListener != null) { 1197 mListener.onContactSplit(contactLookupUri); 1198 } else { 1199 Log.d(TAG, "No listener registered, can not call onSplitFinished"); 1200 } 1201 break; 1202 } 1203 } 1204 1205 /** 1206 * Shows a list of aggregates that can be joined into the currently viewed aggregate. 1207 * 1208 * @param contactLookupUri the fresh URI for the currently edited contact (after saving it) 1209 */ 1210 private void showJoinAggregateActivity(Uri contactLookupUri) { 1211 if (contactLookupUri == null || !isAdded()) { 1212 return; 1213 } 1214 1215 mContactIdForJoin = ContentUris.parseId(contactLookupUri); 1216 mContactWritableForJoin = isContactWritable(); 1217 final Intent intent = new Intent(JoinContactActivity.JOIN_CONTACT); 1218 intent.putExtra(JoinContactActivity.EXTRA_TARGET_CONTACT_ID, mContactIdForJoin); 1219 startActivityForResult(intent, REQUEST_CODE_JOIN); 1220 } 1221 1222 /** 1223 * Performs aggregation with the contact selected by the user from suggestions or A-Z list. 1224 */ 1225 private void joinAggregate(final long contactId) { 1226 Intent intent = ContactSaveService.createJoinContactsIntent(mContext, mContactIdForJoin, 1227 contactId, mContactWritableForJoin, 1228 ContactEditorActivity.class, ContactEditorActivity.ACTION_JOIN_COMPLETED); 1229 mContext.startService(intent); 1230 } 1231 1232 /** 1233 * Returns true if there is at least one writable raw contact in the current contact. 1234 */ 1235 private boolean isContactWritable() { 1236 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 1237 int size = mState.size(); 1238 for (int i = 0; i < size; i++) { 1239 RawContactDelta entity = mState.get(i); 1240 final AccountType type = entity.getAccountType(accountTypes); 1241 if (type.areContactsWritable()) { 1242 return true; 1243 } 1244 } 1245 return false; 1246 } 1247 1248 private boolean isEditingUserProfile() { 1249 return mNewLocalProfile || mIsUserProfile; 1250 } 1251 1252 public static interface Listener { 1253 /** 1254 * Contact was not found, so somehow close this fragment. This is raised after a contact 1255 * is removed via Menu/Delete (unless it was a new contact) 1256 */ 1257 void onContactNotFound(); 1258 1259 /** 1260 * Contact was split, so we can close now. 1261 * @param newLookupUri The lookup uri of the new contact that should be shown to the user. 1262 * The editor tries best to chose the most natural contact here. 1263 */ 1264 void onContactSplit(Uri newLookupUri); 1265 1266 /** 1267 * User has tapped Revert, close the fragment now. 1268 */ 1269 void onReverted(); 1270 1271 /** 1272 * Contact was saved and the Fragment can now be closed safely. 1273 */ 1274 void onSaveFinished(Intent resultIntent); 1275 1276 /** 1277 * User switched to editing a different contact (a suggestion from the 1278 * aggregation engine). 1279 */ 1280 void onEditOtherContactRequested( 1281 Uri contactLookupUri, ArrayList<ContentValues> contentValues); 1282 1283 /** 1284 * Contact is being created for an external account that provides its own 1285 * new contact activity. 1286 */ 1287 void onCustomCreateContactActivityRequested(AccountWithDataSet account, 1288 Bundle intentExtras); 1289 1290 /** 1291 * The edited raw contact belongs to an external account that provides 1292 * its own edit activity. 1293 * 1294 * @param redirect indicates that the current editor should be closed 1295 * before the custom editor is shown. 1296 */ 1297 void onCustomEditContactActivityRequested(AccountWithDataSet account, Uri rawContactUri, 1298 Bundle intentExtras, boolean redirect); 1299 } 1300 1301 private class EntityDeltaComparator implements Comparator<RawContactDelta> { 1302 /** 1303 * Compare EntityDeltas for sorting the stack of editors. 1304 */ 1305 @Override 1306 public int compare(RawContactDelta one, RawContactDelta two) { 1307 // Check direct equality 1308 if (one.equals(two)) { 1309 return 0; 1310 } 1311 1312 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 1313 String accountType1 = one.getValues().getAsString(RawContacts.ACCOUNT_TYPE); 1314 String dataSet1 = one.getValues().getAsString(RawContacts.DATA_SET); 1315 final AccountType type1 = accountTypes.getAccountType(accountType1, dataSet1); 1316 String accountType2 = two.getValues().getAsString(RawContacts.ACCOUNT_TYPE); 1317 String dataSet2 = two.getValues().getAsString(RawContacts.DATA_SET); 1318 final AccountType type2 = accountTypes.getAccountType(accountType2, dataSet2); 1319 1320 // Check read-only 1321 if (!type1.areContactsWritable() && type2.areContactsWritable()) { 1322 return 1; 1323 } else if (type1.areContactsWritable() && !type2.areContactsWritable()) { 1324 return -1; 1325 } 1326 1327 // Check account type 1328 boolean skipAccountTypeCheck = false; 1329 boolean isGoogleAccount1 = type1 instanceof GoogleAccountType; 1330 boolean isGoogleAccount2 = type2 instanceof GoogleAccountType; 1331 if (isGoogleAccount1 && !isGoogleAccount2) { 1332 return -1; 1333 } else if (!isGoogleAccount1 && isGoogleAccount2) { 1334 return 1; 1335 } else if (isGoogleAccount1 && isGoogleAccount2){ 1336 skipAccountTypeCheck = true; 1337 } 1338 1339 int value; 1340 if (!skipAccountTypeCheck) { 1341 if (type1.accountType == null) { 1342 return 1; 1343 } 1344 value = type1.accountType.compareTo(type2.accountType); 1345 if (value != 0) { 1346 return value; 1347 } else { 1348 // Fall back to data set. 1349 if (type1.dataSet != null) { 1350 value = type1.dataSet.compareTo(type2.dataSet); 1351 if (value != 0) { 1352 return value; 1353 } 1354 } else if (type2.dataSet != null) { 1355 return 1; 1356 } 1357 } 1358 } 1359 1360 // Check account name 1361 String oneAccount = one.getAccountName(); 1362 if (oneAccount == null) oneAccount = ""; 1363 String twoAccount = two.getAccountName(); 1364 if (twoAccount == null) twoAccount = ""; 1365 value = oneAccount.compareTo(twoAccount); 1366 if (value != 0) { 1367 return value; 1368 } 1369 1370 // Both are in the same account, fall back to contact ID 1371 Long oneId = one.getRawContactId(); 1372 Long twoId = two.getRawContactId(); 1373 if (oneId == null) { 1374 return -1; 1375 } else if (twoId == null) { 1376 return 1; 1377 } 1378 1379 return (int)(oneId - twoId); 1380 } 1381 } 1382 1383 /** 1384 * Returns the contact ID for the currently edited contact or 0 if the contact is new. 1385 */ 1386 protected long getContactId() { 1387 if (mState != null) { 1388 for (RawContactDelta rawContact : mState) { 1389 Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID); 1390 if (contactId != null) { 1391 return contactId; 1392 } 1393 } 1394 } 1395 return 0; 1396 } 1397 1398 /** 1399 * Triggers an asynchronous search for aggregation suggestions. 1400 */ 1401 private void acquireAggregationSuggestions(Context context, 1402 RawContactEditorView rawContactEditor) { 1403 long rawContactId = rawContactEditor.getRawContactId(); 1404 if (mAggregationSuggestionsRawContactId != rawContactId 1405 && mAggregationSuggestionView != null) { 1406 mAggregationSuggestionView.setVisibility(View.GONE); 1407 mAggregationSuggestionView = null; 1408 mAggregationSuggestionEngine.reset(); 1409 } 1410 1411 mAggregationSuggestionsRawContactId = rawContactId; 1412 1413 if (mAggregationSuggestionEngine == null) { 1414 mAggregationSuggestionEngine = new AggregationSuggestionEngine(context); 1415 mAggregationSuggestionEngine.setListener(this); 1416 mAggregationSuggestionEngine.start(); 1417 } 1418 1419 mAggregationSuggestionEngine.setContactId(getContactId()); 1420 1421 LabeledEditorView nameEditor = rawContactEditor.getNameEditor(); 1422 mAggregationSuggestionEngine.onNameChange(nameEditor.getValues()); 1423 } 1424 1425 @Override 1426 public void onAggregationSuggestionChange() { 1427 Activity activity = getActivity(); 1428 if ((activity != null && activity.isFinishing()) 1429 || !isVisible() || mState == null || mStatus != Status.EDITING) { 1430 return; 1431 } 1432 1433 if (mAggregationSuggestionPopup != null && mAggregationSuggestionPopup.isShowing()) { 1434 mAggregationSuggestionPopup.dismiss(); 1435 } 1436 1437 if (mAggregationSuggestionEngine.getSuggestedContactCount() == 0) { 1438 return; 1439 } 1440 1441 final RawContactEditorView rawContactView = 1442 (RawContactEditorView)getRawContactEditorView(mAggregationSuggestionsRawContactId); 1443 if (rawContactView == null) { 1444 return; // Raw contact deleted? 1445 } 1446 final View anchorView = rawContactView.findViewById(R.id.anchor_view); 1447 mAggregationSuggestionPopup = new ListPopupWindow(mContext, null); 1448 mAggregationSuggestionPopup.setAnchorView(anchorView); 1449 mAggregationSuggestionPopup.setWidth(anchorView.getWidth()); 1450 mAggregationSuggestionPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); 1451 mAggregationSuggestionPopup.setAdapter( 1452 new AggregationSuggestionAdapter(getActivity(), 1453 mState.size() == 1 && mState.get(0).isContactInsert(), 1454 this, mAggregationSuggestionEngine.getSuggestions())); 1455 mAggregationSuggestionPopup.setOnItemClickListener(mAggregationSuggestionItemClickListener); 1456 mAggregationSuggestionPopup.show(); 1457 } 1458 1459 @Override 1460 public void onJoinAction(long contactId, List<Long> rawContactIdList) { 1461 long rawContactIds[] = new long[rawContactIdList.size()]; 1462 for (int i = 0; i < rawContactIds.length; i++) { 1463 rawContactIds[i] = rawContactIdList.get(i); 1464 } 1465 JoinSuggestedContactDialogFragment dialog = 1466 new JoinSuggestedContactDialogFragment(); 1467 Bundle args = new Bundle(); 1468 args.putLongArray("rawContactIds", rawContactIds); 1469 dialog.setArguments(args); 1470 dialog.setTargetFragment(this, 0); 1471 try { 1472 dialog.show(getFragmentManager(), "join"); 1473 } catch (Exception ex) { 1474 // No problem - the activity is no longer available to display the dialog 1475 } 1476 } 1477 1478 public static class JoinSuggestedContactDialogFragment extends DialogFragment { 1479 1480 @Override 1481 public Dialog onCreateDialog(Bundle savedInstanceState) { 1482 return new AlertDialog.Builder(getActivity()) 1483 .setIconAttribute(android.R.attr.alertDialogIcon) 1484 .setMessage(R.string.aggregation_suggestion_join_dialog_message) 1485 .setPositiveButton(android.R.string.yes, 1486 new DialogInterface.OnClickListener() { 1487 @Override 1488 public void onClick(DialogInterface dialog, int whichButton) { 1489 ContactEditorFragment targetFragment = 1490 (ContactEditorFragment) getTargetFragment(); 1491 long rawContactIds[] = 1492 getArguments().getLongArray("rawContactIds"); 1493 targetFragment.doJoinSuggestedContact(rawContactIds); 1494 } 1495 } 1496 ) 1497 .setNegativeButton(android.R.string.no, null) 1498 .create(); 1499 } 1500 } 1501 1502 /** 1503 * Joins the suggested contact (specified by the id's of constituent raw 1504 * contacts), save all changes, and stay in the editor. 1505 */ 1506 protected void doJoinSuggestedContact(long[] rawContactIds) { 1507 if (!hasValidState() || mStatus != Status.EDITING) { 1508 return; 1509 } 1510 1511 mState.setJoinWithRawContacts(rawContactIds); 1512 save(SaveMode.RELOAD); 1513 } 1514 1515 @Override 1516 public void onEditAction(Uri contactLookupUri) { 1517 SuggestionEditConfirmationDialogFragment dialog = 1518 new SuggestionEditConfirmationDialogFragment(); 1519 Bundle args = new Bundle(); 1520 args.putParcelable("contactUri", contactLookupUri); 1521 dialog.setArguments(args); 1522 dialog.setTargetFragment(this, 0); 1523 dialog.show(getFragmentManager(), "edit"); 1524 } 1525 1526 public static class SuggestionEditConfirmationDialogFragment extends DialogFragment { 1527 1528 @Override 1529 public Dialog onCreateDialog(Bundle savedInstanceState) { 1530 return new AlertDialog.Builder(getActivity()) 1531 .setIconAttribute(android.R.attr.alertDialogIcon) 1532 .setMessage(R.string.aggregation_suggestion_edit_dialog_message) 1533 .setPositiveButton(android.R.string.yes, 1534 new DialogInterface.OnClickListener() { 1535 @Override 1536 public void onClick(DialogInterface dialog, int whichButton) { 1537 ContactEditorFragment targetFragment = 1538 (ContactEditorFragment) getTargetFragment(); 1539 Uri contactUri = 1540 getArguments().getParcelable("contactUri"); 1541 targetFragment.doEditSuggestedContact(contactUri); 1542 } 1543 } 1544 ) 1545 .setNegativeButton(android.R.string.no, null) 1546 .create(); 1547 } 1548 } 1549 1550 /** 1551 * Abandons the currently edited contact and switches to editing the suggested 1552 * one, transferring all the data there 1553 */ 1554 protected void doEditSuggestedContact(Uri contactUri) { 1555 if (mListener != null) { 1556 // make sure we don't save this contact when closing down 1557 mStatus = Status.CLOSING; 1558 mListener.onEditOtherContactRequested( 1559 contactUri, mState.get(0).getContentValues()); 1560 } 1561 } 1562 1563 public void setAggregationSuggestionViewEnabled(boolean enabled) { 1564 if (mAggregationSuggestionView == null) { 1565 return; 1566 } 1567 1568 LinearLayout itemList = (LinearLayout) mAggregationSuggestionView.findViewById( 1569 R.id.aggregation_suggestions); 1570 int count = itemList.getChildCount(); 1571 for (int i = 0; i < count; i++) { 1572 itemList.getChildAt(i).setEnabled(enabled); 1573 } 1574 } 1575 1576 @Override 1577 public void onSaveInstanceState(Bundle outState) { 1578 outState.putParcelable(KEY_URI, mLookupUri); 1579 outState.putString(KEY_ACTION, mAction); 1580 1581 if (hasValidState()) { 1582 // Store entities with modifications 1583 outState.putParcelable(KEY_EDIT_STATE, mState); 1584 } 1585 outState.putLong(KEY_RAW_CONTACT_ID_REQUESTING_PHOTO, mRawContactIdRequestingPhoto); 1586 outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator); 1587 outState.putString(KEY_CURRENT_PHOTO_FILE, mCurrentPhotoFile); 1588 outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin); 1589 outState.putBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN, mContactWritableForJoin); 1590 outState.putLong(KEY_SHOW_JOIN_SUGGESTIONS, mAggregationSuggestionsRawContactId); 1591 outState.putBoolean(KEY_ENABLED, mEnabled); 1592 outState.putBoolean(KEY_NEW_LOCAL_PROFILE, mNewLocalProfile); 1593 outState.putBoolean(KEY_IS_USER_PROFILE, mIsUserProfile); 1594 outState.putInt(KEY_STATUS, mStatus); 1595 outState.putParcelable(KEY_UPDATED_PHOTOS, mUpdatedPhotos); 1596 1597 super.onSaveInstanceState(outState); 1598 } 1599 1600 @Override 1601 public void onActivityResult(int requestCode, int resultCode, Intent data) { 1602 if (mStatus == Status.SUB_ACTIVITY) { 1603 mStatus = Status.EDITING; 1604 } 1605 1606 // See if the photo selection handler handles this result. 1607 if (mCurrentPhotoHandler != null && mCurrentPhotoHandler.handlePhotoActivityResult( 1608 requestCode, resultCode, data)) { 1609 return; 1610 } 1611 1612 switch (requestCode) { 1613 case REQUEST_CODE_JOIN: { 1614 // Ignore failed requests 1615 if (resultCode != Activity.RESULT_OK) return; 1616 if (data != null) { 1617 final long contactId = ContentUris.parseId(data.getData()); 1618 joinAggregate(contactId); 1619 } 1620 break; 1621 } 1622 case REQUEST_CODE_ACCOUNTS_CHANGED: { 1623 // Bail if the account selector was not successful. 1624 if (resultCode != Activity.RESULT_OK) { 1625 mListener.onReverted(); 1626 return; 1627 } 1628 // If there's an account specified, use it. 1629 if (data != null) { 1630 AccountWithDataSet account = data.getParcelableExtra(Intents.Insert.ACCOUNT); 1631 if (account != null) { 1632 createContact(account); 1633 return; 1634 } 1635 } 1636 // If there isn't an account specified, then this is likely a phone-local 1637 // contact, so we should continue setting up the editor by automatically selecting 1638 // the most appropriate account. 1639 createContact(); 1640 break; 1641 } 1642 } 1643 } 1644 1645 /** 1646 * Sets the photo stored in mPhoto and writes it to the RawContact with the given id 1647 */ 1648 private void setPhoto(long rawContact, Bitmap photo, String photoFile) { 1649 BaseRawContactEditorView requestingEditor = getRawContactEditorView(rawContact); 1650 1651 if (photo == null || photo.getHeight() < 0 || photo.getWidth() < 0) { 1652 // This is unexpected. 1653 Log.w(TAG, "Invalid bitmap passed to setPhoto()"); 1654 } 1655 1656 if (requestingEditor != null) { 1657 requestingEditor.setPhotoBitmap(photo); 1658 } else { 1659 Log.w(TAG, "The contact that requested the photo is no longer present."); 1660 } 1661 1662 final String croppedPhotoPath = 1663 ContactPhotoUtils.pathForCroppedPhoto(mContext, mCurrentPhotoFile); 1664 mUpdatedPhotos.putString(String.valueOf(rawContact), croppedPhotoPath); 1665 } 1666 1667 /** 1668 * Finds raw contact editor view for the given rawContactId. 1669 */ 1670 public BaseRawContactEditorView getRawContactEditorView(long rawContactId) { 1671 for (int i = 0; i < mContent.getChildCount(); i++) { 1672 final View childView = mContent.getChildAt(i); 1673 if (childView instanceof BaseRawContactEditorView) { 1674 final BaseRawContactEditorView editor = (BaseRawContactEditorView) childView; 1675 if (editor.getRawContactId() == rawContactId) { 1676 return editor; 1677 } 1678 } 1679 } 1680 return null; 1681 } 1682 1683 /** 1684 * Returns true if there is currently more than one photo on screen. 1685 */ 1686 private boolean hasMoreThanOnePhoto() { 1687 int countWithPicture = 0; 1688 final int numEntities = mState.size(); 1689 for (int i = 0; i < numEntities; i++) { 1690 final RawContactDelta entity = mState.get(i); 1691 if (entity.isVisible()) { 1692 final ValuesDelta primary = entity.getPrimaryEntry(Photo.CONTENT_ITEM_TYPE); 1693 if (primary != null && primary.getPhoto() != null) { 1694 countWithPicture++; 1695 } else { 1696 final long rawContactId = entity.getRawContactId(); 1697 final String path = mUpdatedPhotos.getString(String.valueOf(rawContactId)); 1698 if (path != null) { 1699 final File file = new File(path); 1700 if (file.exists()) { 1701 countWithPicture++; 1702 } 1703 } 1704 } 1705 1706 if (countWithPicture > 1) { 1707 return true; 1708 } 1709 } 1710 } 1711 return false; 1712 } 1713 1714 /** 1715 * The listener for the data loader 1716 */ 1717 private final LoaderManager.LoaderCallbacks<Contact> mDataLoaderListener = 1718 new LoaderCallbacks<Contact>() { 1719 @Override 1720 public Loader<Contact> onCreateLoader(int id, Bundle args) { 1721 mLoaderStartTime = SystemClock.elapsedRealtime(); 1722 return new ContactLoader(mContext, mLookupUri, true); 1723 } 1724 1725 @Override 1726 public void onLoadFinished(Loader<Contact> loader, Contact data) { 1727 final long loaderCurrentTime = SystemClock.elapsedRealtime(); 1728 Log.v(TAG, "Time needed for loading: " + (loaderCurrentTime-mLoaderStartTime)); 1729 if (!data.isLoaded()) { 1730 // Item has been deleted 1731 Log.i(TAG, "No contact found. Closing activity"); 1732 if (mListener != null) mListener.onContactNotFound(); 1733 return; 1734 } 1735 1736 mStatus = Status.EDITING; 1737 mLookupUri = data.getLookupUri(); 1738 final long setDataStartTime = SystemClock.elapsedRealtime(); 1739 setData(data); 1740 final long setDataEndTime = SystemClock.elapsedRealtime(); 1741 1742 Log.v(TAG, "Time needed for setting UI: " + (setDataEndTime-setDataStartTime)); 1743 } 1744 1745 @Override 1746 public void onLoaderReset(Loader<Contact> loader) { 1747 } 1748 }; 1749 1750 /** 1751 * The listener for the group meta data loader for all groups. 1752 */ 1753 private final LoaderManager.LoaderCallbacks<Cursor> mGroupLoaderListener = 1754 new LoaderCallbacks<Cursor>() { 1755 1756 @Override 1757 public CursorLoader onCreateLoader(int id, Bundle args) { 1758 return new GroupMetaDataLoader(mContext, Groups.CONTENT_URI); 1759 } 1760 1761 @Override 1762 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 1763 mGroupMetaData = data; 1764 bindGroupMetaData(); 1765 } 1766 1767 @Override 1768 public void onLoaderReset(Loader<Cursor> loader) { 1769 } 1770 }; 1771 1772 @Override 1773 public void onSplitContactConfirmed() { 1774 if (mState == null) { 1775 // This may happen when this Fragment is recreated by the system during users 1776 // confirming the split action (and thus this method is called just before onCreate()), 1777 // for example. 1778 Log.e(TAG, "mState became null during the user's confirming split action. " + 1779 "Cannot perform the save action."); 1780 return; 1781 } 1782 1783 mState.markRawContactsForSplitting(); 1784 save(SaveMode.SPLIT); 1785 } 1786 1787 /** 1788 * Custom photo handler for the editor. The inner listener that this creates also has a 1789 * reference to the editor and acts as an {@link EditorListener}, and uses that editor to hold 1790 * state information in several of the listener methods. 1791 */ 1792 private final class PhotoHandler extends PhotoSelectionHandler { 1793 1794 final long mRawContactId; 1795 private final BaseRawContactEditorView mEditor; 1796 private final PhotoActionListener mPhotoEditorListener; 1797 1798 public PhotoHandler(Context context, BaseRawContactEditorView editor, int photoMode, 1799 RawContactDeltaList state) { 1800 super(context, editor.getPhotoEditor(), photoMode, false, state); 1801 mEditor = editor; 1802 mRawContactId = editor.getRawContactId(); 1803 mPhotoEditorListener = new PhotoEditorListener(); 1804 } 1805 1806 @Override 1807 public PhotoActionListener getListener() { 1808 return mPhotoEditorListener; 1809 } 1810 1811 @Override 1812 public void startPhotoActivity(Intent intent, int requestCode, String photoFile) { 1813 mRawContactIdRequestingPhoto = mEditor.getRawContactId(); 1814 mCurrentPhotoHandler = this; 1815 mStatus = Status.SUB_ACTIVITY; 1816 mCurrentPhotoFile = photoFile; 1817 ContactEditorFragment.this.startActivityForResult(intent, requestCode); 1818 } 1819 1820 private final class PhotoEditorListener extends PhotoSelectionHandler.PhotoActionListener 1821 implements EditorListener { 1822 1823 @Override 1824 public void onRequest(int request) { 1825 if (!hasValidState()) return; 1826 1827 if (request == EditorListener.REQUEST_PICK_PHOTO) { 1828 onClick(mEditor.getPhotoEditor()); 1829 } 1830 } 1831 1832 @Override 1833 public void onDeleteRequested(Editor removedEditor) { 1834 // The picture cannot be deleted, it can only be removed, which is handled by 1835 // onRemovePictureChosen() 1836 } 1837 1838 /** 1839 * User has chosen to set the selected photo as the (super) primary photo 1840 */ 1841 @Override 1842 public void onUseAsPrimaryChosen() { 1843 // Set the IsSuperPrimary for each editor 1844 int count = mContent.getChildCount(); 1845 for (int i = 0; i < count; i++) { 1846 final View childView = mContent.getChildAt(i); 1847 if (childView instanceof BaseRawContactEditorView) { 1848 final BaseRawContactEditorView editor = 1849 (BaseRawContactEditorView) childView; 1850 final PhotoEditorView photoEditor = editor.getPhotoEditor(); 1851 photoEditor.setSuperPrimary(editor == mEditor); 1852 } 1853 } 1854 bindEditors(); 1855 } 1856 1857 /** 1858 * User has chosen to remove a picture 1859 */ 1860 @Override 1861 public void onRemovePictureChosen() { 1862 mEditor.setPhotoBitmap(null); 1863 1864 // Prevent bitmap from being restored if rotate the device. 1865 // (only if we first chose a new photo before removing it) 1866 mUpdatedPhotos.remove(String.valueOf(mRawContactId)); 1867 bindEditors(); 1868 } 1869 1870 @Override 1871 public void onPhotoSelected(Bitmap bitmap) { 1872 setPhoto(mRawContactId, bitmap, mCurrentPhotoFile); 1873 mCurrentPhotoHandler = null; 1874 bindEditors(); 1875 } 1876 1877 @Override 1878 public String getCurrentPhotoFile() { 1879 return mCurrentPhotoFile; 1880 } 1881 1882 @Override 1883 public void onPhotoSelectionDismissed() { 1884 // Nothing to do. 1885 } 1886 } 1887 } 1888} 1889