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