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