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