ContactEditorFragment.java revision 860698b8d4542a55da5ec40032a361433b525aad
1/* 2 * Copyright (C) 2015 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.Fragment; 22import android.app.LoaderManager; 23import android.content.ContentResolver; 24import android.content.ContentUris; 25import android.content.ContentValues; 26import android.content.Context; 27import android.content.CursorLoader; 28import android.content.Intent; 29import android.content.Loader; 30import android.database.Cursor; 31import android.graphics.Bitmap; 32import android.net.Uri; 33import android.os.Bundle; 34import android.os.SystemClock; 35import android.provider.ContactsContract; 36import android.provider.ContactsContract.CommonDataKinds.Email; 37import android.provider.ContactsContract.CommonDataKinds.Event; 38import android.provider.ContactsContract.CommonDataKinds.Organization; 39import android.provider.ContactsContract.CommonDataKinds.Phone; 40import android.provider.ContactsContract.CommonDataKinds.StructuredName; 41import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 42import android.provider.ContactsContract.Intents; 43import android.provider.ContactsContract.RawContacts; 44import android.text.TextUtils; 45import android.util.Log; 46import android.view.LayoutInflater; 47import android.view.Menu; 48import android.view.MenuInflater; 49import android.view.MenuItem; 50import android.view.View; 51import android.view.ViewGroup; 52import android.widget.AdapterView; 53import android.widget.BaseAdapter; 54import android.widget.LinearLayout; 55import android.widget.ListPopupWindow; 56import android.widget.Toast; 57import android.widget.Toolbar; 58 59import com.android.contacts.ContactSaveService; 60import com.android.contacts.GroupMetaDataLoader; 61import com.android.contacts.R; 62import com.android.contacts.activities.ContactEditorAccountsChangedActivity; 63import com.android.contacts.activities.ContactEditorActivity; 64import com.android.contacts.activities.ContactEditorActivity.ContactEditor; 65import com.android.contacts.activities.ContactSelectionActivity; 66import com.android.contacts.common.Experiments; 67import com.android.contacts.common.logging.ScreenEvent.ScreenType; 68import com.android.contacts.common.model.AccountTypeManager; 69import com.android.contacts.common.model.Contact; 70import com.android.contacts.common.model.ContactLoader; 71import com.android.contacts.common.model.RawContact; 72import com.android.contacts.common.model.RawContactDelta; 73import com.android.contacts.common.model.RawContactDeltaList; 74import com.android.contacts.common.model.RawContactModifier; 75import com.android.contacts.common.model.ValuesDelta; 76import com.android.contacts.common.model.account.AccountType; 77import com.android.contacts.common.model.account.AccountWithDataSet; 78import com.android.contacts.common.preference.ContactsPreferences; 79import com.android.contacts.common.util.ContactDisplayUtils; 80import com.android.contacts.common.util.ImplicitIntentsUtil; 81import com.android.contacts.common.util.MaterialColorMapUtils; 82import com.android.contacts.editor.AggregationSuggestionEngine.Suggestion; 83import com.android.contacts.group.GroupUtil; 84import com.android.contacts.list.UiIntentActions; 85import com.android.contacts.quickcontact.InvisibleContactUtil; 86import com.android.contacts.quickcontact.QuickContactActivity; 87import com.android.contacts.util.ContactPhotoUtils; 88import com.android.contacts.util.UiClosables; 89import com.android.contactsbind.HelpUtils; 90import com.android.contactsbind.ObjectFactory; 91import com.android.contactsbind.experiments.Flags; 92 93import com.google.common.collect.ImmutableList; 94import com.google.common.collect.Lists; 95 96import java.io.FileNotFoundException; 97import java.util.ArrayList; 98import java.util.HashSet; 99import java.util.Iterator; 100import java.util.List; 101import java.util.Set; 102 103/** 104 * Contact editor with only the most important fields displayed initially. 105 */ 106public class ContactEditorFragment extends Fragment implements 107 ContactEditor, SplitContactConfirmationDialogFragment.Listener, 108 JoinContactConfirmationDialogFragment.Listener, 109 AggregationSuggestionEngine.Listener, AggregationSuggestionView.Listener, 110 CancelEditDialogFragment.Listener, 111 RawContactEditorView.Listener, PhotoEditorView.Listener { 112 113 static final String TAG = "ContactEditor"; 114 115 private static final int LOADER_CONTACT = 1; 116 private static final int LOADER_GROUPS = 2; 117 118 private static final String KEY_PHOTO_RAW_CONTACT_ID = "photo_raw_contact_id"; 119 private static final String KEY_UPDATED_PHOTOS = "updated_photos"; 120 121 private static final List<String> VALID_INTENT_ACTIONS = new ArrayList<String>() {{ 122 add(Intent.ACTION_EDIT); 123 add(Intent.ACTION_INSERT); 124 add(ContactEditorActivity.ACTION_SAVE_COMPLETED); 125 }}; 126 127 private static final String KEY_ACTION = "action"; 128 private static final String KEY_URI = "uri"; 129 private static final String KEY_AUTO_ADD_TO_DEFAULT_GROUP = "autoAddToDefaultGroup"; 130 private static final String KEY_DISABLE_DELETE_MENU_OPTION = "disableDeleteMenuOption"; 131 private static final String KEY_NEW_LOCAL_PROFILE = "newLocalProfile"; 132 private static final String KEY_MATERIAL_PALETTE = "materialPalette"; 133 private static final String KEY_ACCOUNT = "saveToAccount"; 134 private static final String KEY_VIEW_ID_GENERATOR = "viewidgenerator"; 135 136 private static final String KEY_RAW_CONTACTS = "rawContacts"; 137 138 private static final String KEY_EDIT_STATE = "state"; 139 private static final String KEY_STATUS = "status"; 140 141 private static final String KEY_HAS_NEW_CONTACT = "hasNewContact"; 142 private static final String KEY_NEW_CONTACT_READY = "newContactDataReady"; 143 144 private static final String KEY_IS_EDIT = "isEdit"; 145 private static final String KEY_EXISTING_CONTACT_READY = "existingContactDataReady"; 146 147 private static final String KEY_IS_USER_PROFILE = "isUserProfile"; 148 149 private static final String KEY_ENABLED = "enabled"; 150 151 // Aggregation PopupWindow 152 private static final String KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID = 153 "aggregationSuggestionsRawContactId"; 154 155 // Join Activity 156 private static final String KEY_CONTACT_ID_FOR_JOIN = "contactidforjoin"; 157 158 private static final String KEY_READ_ONLY_DISPLAY_NAME_ID = "readOnlyDisplayNameId"; 159 private static final String KEY_COPY_READ_ONLY_DISPLAY_NAME = "copyReadOnlyDisplayName"; 160 161 protected static final int REQUEST_CODE_JOIN = 0; 162 protected static final int REQUEST_CODE_ACCOUNTS_CHANGED = 1; 163 164 /** 165 * An intent extra that forces the editor to add the edited contact 166 * to the default group (e.g. "My Contacts"). 167 */ 168 public static final String INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY = "addToDefaultDirectory"; 169 170 public static final String INTENT_EXTRA_NEW_LOCAL_PROFILE = "newLocalProfile"; 171 172 public static final String INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION = 173 "disableDeleteMenuOption"; 174 175 /** 176 * Intent key to pass the photo palette primary color calculated by 177 * {@link com.android.contacts.quickcontact.QuickContactActivity} to the editor. 178 */ 179 public static final String INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR = 180 "material_palette_primary_color"; 181 182 /** 183 * Intent key to pass the photo palette secondary color calculated by 184 * {@link com.android.contacts.quickcontact.QuickContactActivity} to the editor. 185 */ 186 public static final String INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR = 187 "material_palette_secondary_color"; 188 189 /** 190 * Intent key to pass the ID of the photo to display on the editor. 191 */ 192 // TODO: This can be cleaned up if we decide to not pass the photo id through 193 // QuickContactActivity. 194 public static final String INTENT_EXTRA_PHOTO_ID = "photo_id"; 195 196 /** 197 * Intent key to pass the ID of the raw contact id that should be displayed in the full editor 198 * by itself. 199 */ 200 public static final String INTENT_EXTRA_RAW_CONTACT_ID_TO_DISPLAY_ALONE = 201 "raw_contact_id_to_display_alone"; 202 203 /** 204 * Intent extra to specify a {@link ContactEditor.SaveMode}. 205 */ 206 public static final String SAVE_MODE_EXTRA_KEY = "saveMode"; 207 208 /** 209 * Intent extra key for the contact ID to join the current contact to after saving. 210 */ 211 public static final String JOIN_CONTACT_ID_EXTRA_KEY = "joinContactId"; 212 213 /** 214 * Callbacks for Activities that host contact editors Fragments. 215 */ 216 public interface Listener { 217 218 /** 219 * Contact was not found, so somehow close this fragment. This is raised after a contact 220 * is removed via Menu/Delete 221 */ 222 void onContactNotFound(); 223 224 /** 225 * Contact was split, so we can close now. 226 * 227 * @param newLookupUri The lookup uri of the new contact that should be shown to the user. 228 * The editor tries best to chose the most natural contact here. 229 */ 230 void onContactSplit(Uri newLookupUri); 231 232 /** 233 * User has tapped Revert, close the fragment now. 234 */ 235 void onReverted(); 236 237 /** 238 * Contact was saved and the Fragment can now be closed safely. 239 */ 240 void onSaveFinished(Intent resultIntent); 241 242 /** 243 * User switched to editing a different raw contact (a suggestion from the 244 * aggregation engine). 245 */ 246 void onEditOtherRawContactRequested(Uri contactLookupUri, long rawContactId, 247 ArrayList<ContentValues> contentValues); 248 249 /** 250 * User has requested that contact be deleted. 251 */ 252 void onDeleteRequested(Uri contactUri); 253 } 254 255 /** 256 * Adapter for aggregation suggestions displayed in a PopupWindow when 257 * editor fields change. 258 */ 259 private static final class AggregationSuggestionAdapter extends BaseAdapter { 260 private final LayoutInflater mLayoutInflater; 261 private final AggregationSuggestionView.Listener mListener; 262 private final List<AggregationSuggestionEngine.Suggestion> mSuggestions; 263 264 public AggregationSuggestionAdapter(Activity activity, 265 AggregationSuggestionView.Listener listener, List<Suggestion> suggestions) { 266 mLayoutInflater = activity.getLayoutInflater(); 267 mListener = listener; 268 mSuggestions = suggestions; 269 } 270 271 @Override 272 public View getView(int position, View convertView, ViewGroup parent) { 273 final Suggestion suggestion = (Suggestion) getItem(position); 274 final AggregationSuggestionView suggestionView = 275 (AggregationSuggestionView) mLayoutInflater.inflate( 276 R.layout.aggregation_suggestions_item, null); 277 suggestionView.setListener(mListener); 278 suggestionView.bindSuggestion(suggestion); 279 return suggestionView; 280 } 281 282 @Override 283 public long getItemId(int position) { 284 return position; 285 } 286 287 @Override 288 public Object getItem(int position) { 289 return mSuggestions.get(position); 290 } 291 292 @Override 293 public int getCount() { 294 return mSuggestions.size(); 295 } 296 } 297 298 protected Context mContext; 299 protected Listener mListener; 300 301 // 302 // Views 303 // 304 protected LinearLayout mContent; 305 protected View mAggregationSuggestionView; 306 protected ListPopupWindow mAggregationSuggestionPopup; 307 308 // 309 // Parameters passed in on {@link #load} 310 // 311 protected String mAction; 312 protected Uri mLookupUri; 313 protected Bundle mIntentExtras; 314 protected boolean mAutoAddToDefaultGroup; 315 protected boolean mDisableDeleteMenuOption; 316 protected boolean mNewLocalProfile; 317 protected MaterialColorMapUtils.MaterialPalette mMaterialPalette; 318 319 // 320 // Helpers 321 // 322 protected ContactEditorUtils mEditorUtils; 323 protected RawContactDeltaComparator mComparator; 324 protected ViewIdGenerator mViewIdGenerator; 325 private AggregationSuggestionEngine mAggregationSuggestionEngine; 326 327 // 328 // Loaded data 329 // 330 // Used to store existing contact data so it can be re-applied during a rebind call, 331 // i.e. account switch. 332 protected Contact mContact; 333 protected ImmutableList<RawContact> mRawContacts; 334 protected Cursor mGroupMetaData; 335 336 // 337 // Editor state 338 // 339 protected RawContactDeltaList mState; 340 protected int mStatus; 341 protected long mRawContactIdToDisplayAlone = -1; 342 343 // Whether to show the new contact blank form and if it's corresponding delta is ready. 344 protected boolean mHasNewContact; 345 protected AccountWithDataSet mAccountWithDataSet; 346 protected boolean mNewContactDataReady; 347 protected boolean mNewContactAccountChanged; 348 349 // Whether it's an edit of existing contact and if it's corresponding delta is ready. 350 protected boolean mIsEdit; 351 protected boolean mExistingContactDataReady; 352 353 // Whether we are editing the "me" profile 354 protected boolean mIsUserProfile; 355 356 // Whether editor views and options menu items should be enabled 357 private boolean mEnabled = true; 358 359 // Aggregation PopupWindow 360 private long mAggregationSuggestionsRawContactId; 361 362 // Join Activity 363 protected long mContactIdForJoin; 364 365 // Used to pre-populate the editor with a display name when a user edits a read-only contact. 366 protected long mReadOnlyDisplayNameId; 367 protected boolean mCopyReadOnlyName; 368 369 /** 370 * The contact data loader listener. 371 */ 372 protected final LoaderManager.LoaderCallbacks<Contact> mContactLoaderListener = 373 new LoaderManager.LoaderCallbacks<Contact>() { 374 375 protected long mLoaderStartTime; 376 377 @Override 378 public Loader<Contact> onCreateLoader(int id, Bundle args) { 379 mLoaderStartTime = SystemClock.elapsedRealtime(); 380 return new ContactLoader(mContext, mLookupUri, 381 /* postViewNotification */ true, 382 /* loadGroupMetaData */ true); 383 } 384 385 @Override 386 public void onLoadFinished(Loader<Contact> loader, Contact contact) { 387 final long loaderCurrentTime = SystemClock.elapsedRealtime(); 388 Log.v(TAG, "Time needed for loading: " + (loaderCurrentTime-mLoaderStartTime)); 389 if (!contact.isLoaded()) { 390 // Item has been deleted. Close activity without saving again. 391 Log.i(TAG, "No contact found. Closing activity"); 392 mStatus = Status.CLOSING; 393 if (mListener != null) mListener.onContactNotFound(); 394 return; 395 } 396 397 mStatus = Status.EDITING; 398 mLookupUri = contact.getLookupUri(); 399 final long setDataStartTime = SystemClock.elapsedRealtime(); 400 setState(contact); 401 final long setDataEndTime = SystemClock.elapsedRealtime(); 402 403 Log.v(TAG, "Time needed for setting UI: " + (setDataEndTime - setDataStartTime)); 404 } 405 406 @Override 407 public void onLoaderReset(Loader<Contact> loader) { 408 } 409 }; 410 411 /** 412 * The groups meta data loader listener. 413 */ 414 protected final LoaderManager.LoaderCallbacks<Cursor> mGroupsLoaderListener = 415 new LoaderManager.LoaderCallbacks<Cursor>() { 416 417 @Override 418 public CursorLoader onCreateLoader(int id, Bundle args) { 419 return new GroupMetaDataLoader(mContext, ContactsContract.Groups.CONTENT_URI, 420 GroupUtil.ALL_GROUPS_SELECTION); 421 } 422 423 @Override 424 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 425 mGroupMetaData = data; 426 setGroupMetaData(); 427 } 428 429 @Override 430 public void onLoaderReset(Loader<Cursor> loader) { 431 } 432 }; 433 434 private long mPhotoRawContactId; 435 private Bundle mUpdatedPhotos = new Bundle(); 436 437 @Override 438 public Context getContext() { 439 return getActivity(); 440 } 441 442 @Override 443 public void onAttach(Activity activity) { 444 super.onAttach(activity); 445 mContext = activity; 446 mEditorUtils = ContactEditorUtils.create(mContext); 447 mComparator = new RawContactDeltaComparator(mContext); 448 } 449 450 @Override 451 public void onCreate(Bundle savedState) { 452 if (savedState != null) { 453 // Restore mUri before calling super.onCreate so that onInitializeLoaders 454 // would already have a uri and an action to work with 455 mAction = savedState.getString(KEY_ACTION); 456 mLookupUri = savedState.getParcelable(KEY_URI); 457 } 458 459 super.onCreate(savedState); 460 461 if (savedState == null) { 462 mViewIdGenerator = new ViewIdGenerator(); 463 464 // mState can still be null because it may not have have finished loading before 465 // onSaveInstanceState was called. 466 mState = new RawContactDeltaList(); 467 } else { 468 mViewIdGenerator = savedState.getParcelable(KEY_VIEW_ID_GENERATOR); 469 470 mAutoAddToDefaultGroup = savedState.getBoolean(KEY_AUTO_ADD_TO_DEFAULT_GROUP); 471 mDisableDeleteMenuOption = savedState.getBoolean(KEY_DISABLE_DELETE_MENU_OPTION); 472 mNewLocalProfile = savedState.getBoolean(KEY_NEW_LOCAL_PROFILE); 473 mMaterialPalette = savedState.getParcelable(KEY_MATERIAL_PALETTE); 474 mAccountWithDataSet = savedState.getParcelable(KEY_ACCOUNT); 475 mRawContacts = ImmutableList.copyOf(savedState.<RawContact>getParcelableArrayList( 476 KEY_RAW_CONTACTS)); 477 // NOTE: mGroupMetaData is not saved/restored 478 479 // Read state from savedState. No loading involved here 480 mState = savedState.<RawContactDeltaList> getParcelable(KEY_EDIT_STATE); 481 mStatus = savedState.getInt(KEY_STATUS); 482 483 mHasNewContact = savedState.getBoolean(KEY_HAS_NEW_CONTACT); 484 mNewContactDataReady = savedState.getBoolean(KEY_NEW_CONTACT_READY); 485 486 mIsEdit = savedState.getBoolean(KEY_IS_EDIT); 487 mExistingContactDataReady = savedState.getBoolean(KEY_EXISTING_CONTACT_READY); 488 489 mIsUserProfile = savedState.getBoolean(KEY_IS_USER_PROFILE); 490 491 mEnabled = savedState.getBoolean(KEY_ENABLED); 492 493 // Aggregation PopupWindow 494 mAggregationSuggestionsRawContactId = savedState.getLong( 495 KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID); 496 497 // Join Activity 498 mContactIdForJoin = savedState.getLong(KEY_CONTACT_ID_FOR_JOIN); 499 500 mReadOnlyDisplayNameId = savedState.getLong(KEY_READ_ONLY_DISPLAY_NAME_ID); 501 mCopyReadOnlyName = savedState.getBoolean(KEY_COPY_READ_ONLY_DISPLAY_NAME, false); 502 503 mPhotoRawContactId = savedState.getLong(KEY_PHOTO_RAW_CONTACT_ID); 504 mUpdatedPhotos = savedState.getParcelable(KEY_UPDATED_PHOTOS); 505 } 506 } 507 508 @Override 509 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { 510 setHasOptionsMenu(true); 511 512 final View view = inflater.inflate( 513 R.layout.contact_editor_fragment, container, false); 514 mContent = (LinearLayout) view.findViewById(R.id.raw_contacts_editor_view); 515 return view; 516 } 517 518 @Override 519 public void onActivityCreated(Bundle savedInstanceState) { 520 super.onActivityCreated(savedInstanceState); 521 522 validateAction(mAction); 523 524 if (mState.isEmpty()) { 525 // The delta list may not have finished loading before orientation change happens. 526 // In this case, there will be a saved state but deltas will be missing. Reload from 527 // database. 528 if (Intent.ACTION_EDIT.equals(mAction)) { 529 // Either 530 // 1) orientation change but load never finished. 531 // 2) not an orientation change so data needs to be loaded for first time. 532 getLoaderManager().initLoader(LOADER_CONTACT, null, mContactLoaderListener); 533 getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupsLoaderListener); 534 } 535 } else { 536 // Orientation change, we already have mState, it was loaded by onCreate 537 bindEditors(); 538 } 539 540 // Handle initial actions only when existing state missing 541 if (savedInstanceState == null) { 542 final Account account = mIntentExtras == null ? null : 543 (Account) mIntentExtras.getParcelable(Intents.Insert.EXTRA_ACCOUNT); 544 final String dataSet = mIntentExtras == null ? null : 545 mIntentExtras.getString(Intents.Insert.EXTRA_DATA_SET); 546 if (account != null) { 547 mAccountWithDataSet = new AccountWithDataSet(account.name, account.type, dataSet); 548 } 549 550 if (Intent.ACTION_EDIT.equals(mAction)) { 551 mIsEdit = true; 552 } else if (Intent.ACTION_INSERT.equals(mAction)) { 553 mHasNewContact = true; 554 if (mAccountWithDataSet != null) { 555 createContact(mAccountWithDataSet); 556 } else if (mIntentExtras != null && mIntentExtras.getBoolean( 557 ContactEditorActivity.EXTRA_SAVE_TO_DEVICE_FLAG, false)) { 558 createContact(null); 559 } else { 560 // No Account specified. Let the user choose 561 // Load Accounts async so that we can present them 562 selectAccountAndCreateContact(); 563 } 564 } 565 } 566 } 567 568 /** 569 * Checks if the requested action is valid. 570 * 571 * @param action The action to test. 572 * @throws IllegalArgumentException when the action is invalid. 573 */ 574 private static void validateAction(String action) { 575 if (VALID_INTENT_ACTIONS.contains(action)) { 576 return; 577 } 578 throw new IllegalArgumentException( 579 "Unknown action " + action + "; Supported actions: " + VALID_INTENT_ACTIONS); 580 } 581 582 @Override 583 public void onSaveInstanceState(Bundle outState) { 584 outState.putString(KEY_ACTION, mAction); 585 outState.putParcelable(KEY_URI, mLookupUri); 586 outState.putBoolean(KEY_AUTO_ADD_TO_DEFAULT_GROUP, mAutoAddToDefaultGroup); 587 outState.putBoolean(KEY_DISABLE_DELETE_MENU_OPTION, mDisableDeleteMenuOption); 588 outState.putBoolean(KEY_NEW_LOCAL_PROFILE, mNewLocalProfile); 589 if (mMaterialPalette != null) { 590 outState.putParcelable(KEY_MATERIAL_PALETTE, mMaterialPalette); 591 } 592 outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator); 593 594 outState.putParcelableArrayList(KEY_RAW_CONTACTS, mRawContacts == null ? 595 Lists.<RawContact>newArrayList() : Lists.newArrayList(mRawContacts)); 596 // NOTE: mGroupMetaData is not saved 597 598 outState.putParcelable(KEY_EDIT_STATE, mState); 599 outState.putInt(KEY_STATUS, mStatus); 600 outState.putBoolean(KEY_HAS_NEW_CONTACT, mHasNewContact); 601 outState.putBoolean(KEY_NEW_CONTACT_READY, mNewContactDataReady); 602 outState.putBoolean(KEY_IS_EDIT, mIsEdit); 603 outState.putBoolean(KEY_EXISTING_CONTACT_READY, mExistingContactDataReady); 604 outState.putParcelable(KEY_ACCOUNT, mAccountWithDataSet); 605 outState.putBoolean(KEY_IS_USER_PROFILE, mIsUserProfile); 606 607 outState.putBoolean(KEY_ENABLED, mEnabled); 608 609 // Aggregation PopupWindow 610 outState.putLong(KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID, 611 mAggregationSuggestionsRawContactId); 612 613 // Join Activity 614 outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin); 615 616 outState.putLong(KEY_READ_ONLY_DISPLAY_NAME_ID, mReadOnlyDisplayNameId); 617 outState.putBoolean(KEY_COPY_READ_ONLY_DISPLAY_NAME, mCopyReadOnlyName); 618 619 outState.putLong(KEY_PHOTO_RAW_CONTACT_ID, mPhotoRawContactId); 620 outState.putParcelable(KEY_UPDATED_PHOTOS, mUpdatedPhotos); 621 super.onSaveInstanceState(outState); 622 } 623 624 @Override 625 public void onStop() { 626 super.onStop(); 627 UiClosables.closeQuietly(mAggregationSuggestionPopup); 628 } 629 630 @Override 631 public void onDestroy() { 632 super.onDestroy(); 633 if (mAggregationSuggestionEngine != null) { 634 mAggregationSuggestionEngine.quit(); 635 } 636 } 637 638 @Override 639 public void onActivityResult(int requestCode, int resultCode, Intent data) { 640 switch (requestCode) { 641 case REQUEST_CODE_JOIN: { 642 // Ignore failed requests 643 if (resultCode != Activity.RESULT_OK) return; 644 if (data != null) { 645 final long contactId = ContentUris.parseId(data.getData()); 646 if (hasPendingChanges()) { 647 // Ask the user if they want to save changes before doing the join 648 JoinContactConfirmationDialogFragment.show(this, contactId); 649 } else { 650 // Do the join immediately 651 joinAggregate(contactId); 652 } 653 } 654 break; 655 } 656 case REQUEST_CODE_ACCOUNTS_CHANGED: { 657 // Bail if the account selector was not successful. 658 if (resultCode != Activity.RESULT_OK) { 659 if (mListener != null) { 660 mListener.onReverted(); 661 } 662 return; 663 } 664 // If there's an account specified, use it. 665 if (data != null) { 666 AccountWithDataSet account = data.getParcelableExtra( 667 Intents.Insert.EXTRA_ACCOUNT); 668 if (account != null) { 669 createContact(account); 670 return; 671 } 672 } 673 // If there isn't an account specified, then this is likely a phone-local 674 // contact, so we should continue setting up the editor by automatically selecting 675 // the most appropriate account. 676 createContact(); 677 break; 678 } 679 } 680 } 681 682 // 683 // Options menu 684 // 685 686 @Override 687 public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) { 688 inflater.inflate(R.menu.edit_contact, menu); 689 } 690 691 @Override 692 public void onPrepareOptionsMenu(Menu menu) { 693 // This supports the keyboard shortcut to save changes to a contact but shouldn't be visible 694 // because the custom action bar contains the "save" button now (not the overflow menu). 695 // TODO: Find a better way to handle shortcuts, i.e. onKeyDown()? 696 final MenuItem saveMenu = menu.findItem(R.id.menu_save); 697 final MenuItem splitMenu = menu.findItem(R.id.menu_split); 698 final MenuItem joinMenu = menu.findItem(R.id.menu_join); 699 final MenuItem deleteMenu = menu.findItem(R.id.menu_delete); 700 701 // TODO: b/30771904, b/31827701, temporarily disable these items until we get them to work 702 // on a raw contact level. 703 joinMenu.setVisible(false); 704 splitMenu.setVisible(false); 705 deleteMenu.setVisible(false); 706 // Save menu is invisible when there's only one read only contact in the editor. 707 saveMenu.setVisible(!isEditingReadOnlyRawContact()); 708 if (saveMenu.isVisible()) { 709 // Since we're using a custom action layout we have to manually hook up the handler. 710 saveMenu.getActionView().setOnClickListener(new View.OnClickListener() { 711 @Override 712 public void onClick(View v) { 713 onOptionsItemSelected(saveMenu); 714 } 715 }); 716 } 717 718 int size = menu.size(); 719 for (int i = 0; i < size; i++) { 720 menu.getItem(i).setEnabled(mEnabled); 721 } 722 } 723 724 @Override 725 public boolean onOptionsItemSelected(MenuItem item) { 726 if (item.getItemId() == android.R.id.home) { 727 return revert(); 728 } 729 730 final Activity activity = getActivity(); 731 if (activity == null || activity.isFinishing() || activity.isDestroyed()) { 732 // If we no longer are attached to a running activity want to 733 // drain this event. 734 return true; 735 } 736 737 switch (item.getItemId()) { 738 case R.id.menu_save: 739 return save(SaveMode.CLOSE); 740 case R.id.menu_delete: 741 if (mListener != null) mListener.onDeleteRequested(mLookupUri); 742 return true; 743 case R.id.menu_split: 744 return doSplitContactAction(); 745 case R.id.menu_join: 746 return doJoinContactAction(); 747 case R.id.menu_help: 748 HelpUtils.launchHelpAndFeedbackForContactScreen(getActivity()); 749 return true; 750 } 751 752 return false; 753 } 754 755 @Override 756 public boolean revert() { 757 if (mState.isEmpty() || !hasPendingChanges()) { 758 onCancelEditConfirmed(); 759 } else { 760 CancelEditDialogFragment.show(this); 761 } 762 return true; 763 } 764 765 @Override 766 public void onCancelEditConfirmed() { 767 // When this Fragment is closed we don't want it to auto-save 768 mStatus = Status.CLOSING; 769 if (mListener != null) { 770 mListener.onReverted(); 771 } 772 } 773 774 @Override 775 public void onSplitContactConfirmed(boolean hasPendingChanges) { 776 if (mState.isEmpty()) { 777 // This may happen when this Fragment is recreated by the system during users 778 // confirming the split action (and thus this method is called just before onCreate()), 779 // for example. 780 Log.e(TAG, "mState became null during the user's confirming split action. " + 781 "Cannot perform the save action."); 782 return; 783 } 784 785 if (!hasPendingChanges && mHasNewContact) { 786 // If the user didn't add anything new, we don't want to split out the newly created 787 // raw contact into a name-only contact so remove them. 788 final Iterator<RawContactDelta> iterator = mState.iterator(); 789 while (iterator.hasNext()) { 790 final RawContactDelta rawContactDelta = iterator.next(); 791 if (rawContactDelta.getRawContactId() < 0) { 792 iterator.remove(); 793 } 794 } 795 } 796 mState.markRawContactsForSplitting(); 797 save(SaveMode.SPLIT); 798 } 799 800 @Override 801 public void onSplitContactCanceled() {} 802 803 private boolean doSplitContactAction() { 804 if (!hasValidState()) return false; 805 806 SplitContactConfirmationDialogFragment.show(this, hasPendingChanges()); 807 return true; 808 } 809 810 private boolean doJoinContactAction() { 811 if (!hasValidState() || mLookupUri == null) { 812 return false; 813 } 814 815 // If we just started creating a new contact and haven't added any data, it's too 816 // early to do a join 817 if (mState.size() == 1 && mState.get(0).isContactInsert() 818 && !hasPendingChanges()) { 819 Toast.makeText(mContext, R.string.toast_join_with_empty_contact, 820 Toast.LENGTH_LONG).show(); 821 return true; 822 } 823 824 showJoinAggregateActivity(mLookupUri); 825 return true; 826 } 827 828 @Override 829 public void onJoinContactConfirmed(long joinContactId) { 830 doSaveAction(SaveMode.JOIN, joinContactId); 831 } 832 833 @Override 834 public boolean save(int saveMode) { 835 if (!hasValidState() || mStatus != Status.EDITING) { 836 return false; 837 } 838 839 // If we are about to close the editor - there is no need to refresh the data 840 if (saveMode == SaveMode.CLOSE || saveMode == SaveMode.EDITOR 841 || saveMode == SaveMode.SPLIT) { 842 getLoaderManager().destroyLoader(LOADER_CONTACT); 843 } 844 845 mStatus = Status.SAVING; 846 847 if (!hasPendingChanges()) { 848 if (mLookupUri == null && saveMode == SaveMode.RELOAD) { 849 // We don't have anything to save and there isn't even an existing contact yet. 850 // Nothing to do, simply go back to editing mode 851 mStatus = Status.EDITING; 852 return true; 853 } 854 onSaveCompleted(/* hadChanges =*/ false, saveMode, 855 /* saveSucceeded =*/ mLookupUri != null, mLookupUri, /* joinContactId =*/ null); 856 return true; 857 } 858 859 setEnabled(false); 860 861 return doSaveAction(saveMode, /* joinContactId */ null); 862 } 863 864 // 865 // State accessor methods 866 // 867 868 /** 869 * Check if our internal {@link #mState} is valid, usually checked before 870 * performing user actions. 871 */ 872 private boolean hasValidState() { 873 return mState.size() > 0; 874 } 875 876 private boolean isEditingUserProfile() { 877 return mNewLocalProfile || mIsUserProfile; 878 } 879 880 /** 881 * Whether the contact being edited is composed of read-only raw contacts 882 * aggregated with a newly created writable raw contact. 883 */ 884 private boolean isEditingReadOnlyRawContactWithNewContact() { 885 return mHasNewContact && mState.size() > 1; 886 } 887 888 /** 889 * @return true if the single raw contact we're looking at is read-only. 890 */ 891 private boolean isEditingReadOnlyRawContact() { 892 return hasValidState() && mRawContactIdToDisplayAlone > 0 893 && !mState.getByRawContactId(mRawContactIdToDisplayAlone) 894 .getAccountType(AccountTypeManager.getInstance(mContext)) 895 .areContactsWritable(); 896 } 897 898 /** 899 * Return true if there are any edits to the current contact which need to 900 * be saved. 901 */ 902 private boolean hasPendingRawContactChanges(Set<String> excludedMimeTypes) { 903 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 904 return RawContactModifier.hasChanges(mState, accountTypes, excludedMimeTypes); 905 } 906 907 /** 908 * Determines if changes were made in the editor that need to be saved, while taking into 909 * account that name changes are not real for read-only contacts. 910 * See go/editing-read-only-contacts 911 */ 912 private boolean hasPendingChanges() { 913 if (isEditingReadOnlyRawContactWithNewContact()) { 914 // We created a new raw contact delta with a default display name. 915 // We must test for pending changes while ignoring the default display name. 916 final ValuesDelta beforeDelta = mState.getByRawContactId(mReadOnlyDisplayNameId) 917 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE); 918 final ValuesDelta pendingDelta = mState 919 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE); 920 if (structuredNamesAreEqual(beforeDelta, pendingDelta)) { 921 final Set<String> excludedMimeTypes = new HashSet<>(); 922 excludedMimeTypes.add(StructuredName.CONTENT_ITEM_TYPE); 923 return hasPendingRawContactChanges(excludedMimeTypes); 924 } 925 return true; 926 } 927 return hasPendingRawContactChanges(/* excludedMimeTypes =*/ null); 928 } 929 930 /** 931 * Compares the two {@link ValuesDelta} to see if the structured name is changed. We made a copy 932 * of a read only delta and now we want to check if the copied delta has changes. 933 * 934 * @param before original {@link ValuesDelta} 935 * @param after copied {@link ValuesDelta} 936 * @return true if the copied {@link ValuesDelta} has all the same values in the structured 937 * name fields as the original. 938 */ 939 private boolean structuredNamesAreEqual(ValuesDelta before, ValuesDelta after) { 940 if (before == after) return true; 941 if (before == null || after == null) return false; 942 final ContentValues original = before.getBefore(); 943 final ContentValues pending = after.getAfter(); 944 if (original != null && pending != null) { 945 final String beforeDisplayName = original.getAsString(StructuredName.DISPLAY_NAME); 946 final String afterDisplayName = pending.getAsString(StructuredName.DISPLAY_NAME); 947 if (!TextUtils.equals(beforeDisplayName, afterDisplayName)) return false; 948 949 final String beforePrefix = original.getAsString(StructuredName.PREFIX); 950 final String afterPrefix = pending.getAsString(StructuredName.PREFIX); 951 if (!TextUtils.equals(beforePrefix, afterPrefix)) return false; 952 953 final String beforeFirstName = original.getAsString(StructuredName.GIVEN_NAME); 954 final String afterFirstName = pending.getAsString(StructuredName.GIVEN_NAME); 955 if (!TextUtils.equals(beforeFirstName, afterFirstName)) return false; 956 957 final String beforeMiddleName = original.getAsString(StructuredName.MIDDLE_NAME); 958 final String afterMiddleName = pending.getAsString(StructuredName.MIDDLE_NAME); 959 if (!TextUtils.equals(beforeMiddleName, afterMiddleName)) return false; 960 961 final String beforeLastName = original.getAsString(StructuredName.FAMILY_NAME); 962 final String afterLastName = pending.getAsString(StructuredName.FAMILY_NAME); 963 if (!TextUtils.equals(beforeLastName, afterLastName)) return false; 964 965 final String beforeSuffix = original.getAsString(StructuredName.SUFFIX); 966 final String afterSuffix = pending.getAsString(StructuredName.SUFFIX); 967 return TextUtils.equals(beforeSuffix, afterSuffix); 968 } 969 return false; 970 } 971 972 // 973 // Account creation 974 // 975 976 private void selectAccountAndCreateContact() { 977 // If this is a local profile, then skip the logic about showing the accounts changed 978 // activity and create a phone-local contact. 979 if (mNewLocalProfile) { 980 createContact(null); 981 return; 982 } 983 984 // If there is no default account or the accounts have changed such that we need to 985 // prompt the user again, then launch the account prompt. 986 if (mEditorUtils.shouldShowAccountChangedNotification()) { 987 Intent intent = new Intent(mContext, ContactEditorAccountsChangedActivity.class); 988 // Prevent a second instance from being started on rotates 989 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 990 mStatus = Status.SUB_ACTIVITY; 991 startActivityForResult(intent, REQUEST_CODE_ACCOUNTS_CHANGED); 992 } else { 993 // Make sure the default account is automatically set if there is only one non-device 994 // account. 995 mEditorUtils.maybeUpdateDefaultAccount(); 996 // Otherwise, there should be a default account. Then either create a local contact 997 // (if default account is null) or create a contact with the specified account. 998 AccountWithDataSet defaultAccount = mEditorUtils.getOnlyOrDefaultAccount(); 999 createContact(defaultAccount); 1000 } 1001 } 1002 1003 /** 1004 * Create a contact by automatically selecting the first account. If there's no available 1005 * account, a device-local contact should be created. 1006 */ 1007 private void createContact() { 1008 final List<AccountWithDataSet> accounts = 1009 AccountTypeManager.getInstance(mContext).getAccounts(true); 1010 // No Accounts available. Create a phone-local contact. 1011 if (accounts.isEmpty()) { 1012 createContact(null); 1013 return; 1014 } 1015 1016 // We have an account switcher in "create-account" screen, so don't need to ask a user to 1017 // select an account here. 1018 createContact(accounts.get(0)); 1019 } 1020 1021 /** 1022 * Shows account creation screen associated with a given account. 1023 * 1024 * @param account may be null to signal a device-local contact should be created. 1025 */ 1026 private void createContact(AccountWithDataSet account) { 1027 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 1028 final AccountType accountType = accountTypes.getAccountTypeForAccount(account); 1029 1030 setStateForNewContact(account, accountType, isEditingUserProfile()); 1031 } 1032 1033 // 1034 // Data binding 1035 // 1036 1037 private void setState(Contact contact) { 1038 // If we have already loaded data, we do not want to change it here to not confuse the user 1039 if (!mState.isEmpty()) { 1040 Log.v(TAG, "Ignoring background change. This will have to be rebased later"); 1041 return; 1042 } 1043 mContact = contact; 1044 mRawContacts = contact.getRawContacts(); 1045 1046 // Check for writable raw contacts. If there are none, then we need to create one so user 1047 // can edit. For the user profile case, there is already an editable contact. 1048 if (!contact.isUserProfile() && !contact.isWritableContact(mContext)) { 1049 mHasNewContact = true; 1050 mReadOnlyDisplayNameId = contact.getNameRawContactId(); 1051 mCopyReadOnlyName = true; 1052 // This is potentially an asynchronous call and will add deltas to list. 1053 selectAccountAndCreateContact(); 1054 } else { 1055 mHasNewContact = false; 1056 } 1057 1058 setStateForExistingContact(contact.isUserProfile(), mRawContacts); 1059 if (mAutoAddToDefaultGroup 1060 && InvisibleContactUtil.isInvisibleAndAddable(contact, getContext())) { 1061 InvisibleContactUtil.markAddToDefaultGroup(contact, mState, getContext()); 1062 } 1063 } 1064 1065 /** 1066 * Prepare {@link #mState} for a newly created phone-local contact. 1067 */ 1068 private void setStateForNewContact(AccountWithDataSet account, AccountType accountType, 1069 boolean isUserProfile) { 1070 setStateForNewContact(account, accountType, /* oldState =*/ null, 1071 /* oldAccountType =*/ null, isUserProfile); 1072 } 1073 1074 /** 1075 * Prepare {@link #mState} for a newly created phone-local contact, migrating the state 1076 * specified by oldState and oldAccountType. 1077 */ 1078 private void setStateForNewContact(AccountWithDataSet account, AccountType accountType, 1079 RawContactDelta oldState, AccountType oldAccountType, boolean isUserProfile) { 1080 mStatus = Status.EDITING; 1081 mState.add(createNewRawContactDelta(account, accountType, oldState, oldAccountType)); 1082 mIsUserProfile = isUserProfile; 1083 mNewContactDataReady = true; 1084 bindEditors(); 1085 } 1086 1087 /** 1088 * Returns a {@link RawContactDelta} for a new contact suitable for addition into 1089 * {@link #mState}. 1090 * 1091 * If oldState and oldAccountType are specified, the state specified by those parameters 1092 * is migrated to the result {@link RawContactDelta}. 1093 */ 1094 private RawContactDelta createNewRawContactDelta(AccountWithDataSet account, 1095 AccountType accountType, RawContactDelta oldState, AccountType oldAccountType) { 1096 final RawContact rawContact = new RawContact(); 1097 if (account != null) { 1098 rawContact.setAccount(account); 1099 } else { 1100 rawContact.setAccountToLocal(); 1101 } 1102 1103 final RawContactDelta result = new RawContactDelta( 1104 ValuesDelta.fromAfter(rawContact.getValues())); 1105 if (oldState == null) { 1106 // Parse any values from incoming intent 1107 RawContactModifier.parseExtras(mContext, accountType, result, mIntentExtras); 1108 } else { 1109 RawContactModifier.migrateStateForNewContact( 1110 mContext, oldState, result, oldAccountType, accountType); 1111 } 1112 1113 // Ensure we have some default fields (if the account type does not support a field, 1114 // ensureKind will not add it, so it is safe to add e.g. Event) 1115 RawContactModifier.ensureKindExists(result, accountType, StructuredName.CONTENT_ITEM_TYPE); 1116 RawContactModifier.ensureKindExists(result, accountType, Phone.CONTENT_ITEM_TYPE); 1117 RawContactModifier.ensureKindExists(result, accountType, Email.CONTENT_ITEM_TYPE); 1118 RawContactModifier.ensureKindExists(result, accountType, Organization.CONTENT_ITEM_TYPE); 1119 RawContactModifier.ensureKindExists(result, accountType, Event.CONTENT_ITEM_TYPE); 1120 RawContactModifier.ensureKindExists(result, accountType, 1121 StructuredPostal.CONTENT_ITEM_TYPE); 1122 1123 // Set the correct URI for saving the contact as a profile 1124 if (mNewLocalProfile) { 1125 result.setProfileQueryUri(); 1126 } 1127 1128 return result; 1129 } 1130 1131 /** 1132 * Prepare {@link #mState} for an existing contact. 1133 */ 1134 private void setStateForExistingContact(boolean isUserProfile, 1135 ImmutableList<RawContact> rawContacts) { 1136 setEnabled(true); 1137 1138 mState.addAll(rawContacts.iterator()); 1139 setIntentExtras(mIntentExtras); 1140 mIntentExtras = null; 1141 1142 // For user profile, change the contacts query URI 1143 mIsUserProfile = isUserProfile; 1144 boolean localProfileExists = false; 1145 1146 if (mIsUserProfile) { 1147 for (RawContactDelta rawContactDelta : mState) { 1148 // For profile contacts, we need a different query URI 1149 rawContactDelta.setProfileQueryUri(); 1150 // Try to find a local profile contact 1151 if (rawContactDelta.getValues().getAsString(RawContacts.ACCOUNT_TYPE) == null) { 1152 localProfileExists = true; 1153 } 1154 } 1155 // Editor should always present a local profile for editing 1156 // TODO(wjang): Need to figure out when this case comes up. We can't do this if we're 1157 // going to prune all but the one raw contact that we're trying to display by itself. 1158 if (!localProfileExists && mRawContactIdToDisplayAlone <= 0) { 1159 mState.add(createLocalRawContactDelta()); 1160 } 1161 } 1162 mExistingContactDataReady = true; 1163 bindEditors(); 1164 } 1165 1166 /** 1167 * Set the enabled state of editors. 1168 */ 1169 private void setEnabled(boolean enabled) { 1170 if (mEnabled != enabled) { 1171 mEnabled = enabled; 1172 1173 // Enable/disable editors 1174 if (mContent != null) { 1175 int count = mContent.getChildCount(); 1176 for (int i = 0; i < count; i++) { 1177 mContent.getChildAt(i).setEnabled(enabled); 1178 } 1179 } 1180 1181 // Enable/disable aggregation suggestion vies 1182 if (mAggregationSuggestionView != null) { 1183 LinearLayout itemList = (LinearLayout) mAggregationSuggestionView.findViewById( 1184 R.id.aggregation_suggestions); 1185 int count = itemList.getChildCount(); 1186 for (int i = 0; i < count; i++) { 1187 itemList.getChildAt(i).setEnabled(enabled); 1188 } 1189 } 1190 1191 // Maybe invalidate the options menu 1192 final Activity activity = getActivity(); 1193 if (activity != null) activity.invalidateOptionsMenu(); 1194 } 1195 } 1196 1197 /** 1198 * Returns a {@link RawContactDelta} for a local contact suitable for addition into 1199 * {@link #mState}. 1200 */ 1201 private static RawContactDelta createLocalRawContactDelta() { 1202 final RawContact rawContact = new RawContact(); 1203 rawContact.setAccountToLocal(); 1204 1205 final RawContactDelta result = new RawContactDelta( 1206 ValuesDelta.fromAfter(rawContact.getValues())); 1207 result.setProfileQueryUri(); 1208 1209 return result; 1210 } 1211 1212 private void copyReadOnlyName() { 1213 // We should only ever be doing this if we're creating a new writable contact to attach to 1214 // a read only contact. 1215 if (!isEditingReadOnlyRawContactWithNewContact()) { 1216 return; 1217 } 1218 final int writableIndex = mState.indexOfFirstWritableRawContact(getContext()); 1219 final RawContactDelta writable = mState.get(writableIndex); 1220 final RawContactDelta readOnly = mState.getByRawContactId(mContact.getNameRawContactId()); 1221 final ValuesDelta writeNameDelta = writable 1222 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE); 1223 final ValuesDelta readNameDelta = readOnly 1224 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE); 1225 mCopyReadOnlyName = false; 1226 if (writeNameDelta == null || readNameDelta == null) { 1227 return; 1228 } 1229 writeNameDelta.copyStructuredNameFieldsFrom(readNameDelta); 1230 } 1231 1232 /** 1233 * Bind editors using {@link #mState} and other members initialized from the loaded (or new) 1234 * Contact. 1235 */ 1236 protected void bindEditors() { 1237 if (!isReadyToBindEditors()) { 1238 return; 1239 } 1240 1241 // Add input fields for the loaded Contact 1242 final RawContactEditorView editorView = getContent(); 1243 editorView.setListener(this); 1244 if (mCopyReadOnlyName) { 1245 copyReadOnlyName(); 1246 } 1247 editorView.setState(mState, mMaterialPalette, mViewIdGenerator, 1248 mHasNewContact, mIsUserProfile, mAccountWithDataSet, 1249 mRawContactIdToDisplayAlone); 1250 if (isEditingReadOnlyRawContact()) { 1251 final Toolbar toolbar = getEditorActivity().getToolbar(); 1252 if (toolbar != null) { 1253 toolbar.setTitle(R.string.contact_editor_title_read_only_contact); 1254 // Set activity title for Talkback 1255 getEditorActivity().setTitle(R.string.contact_editor_title_read_only_contact); 1256 toolbar.setNavigationIcon(R.drawable.ic_back_arrow); 1257 toolbar.setNavigationContentDescription(R.string.back_arrow_content_description); 1258 } 1259 } 1260 1261 // Set up the photo widget 1262 editorView.setPhotoListener(this); 1263 mPhotoRawContactId = editorView.getPhotoRawContactId(); 1264 // If there is an updated full resolution photo apply it now, this will be the case if 1265 // the user selects or takes a new photo, then rotates the device. 1266 final Uri uri = (Uri) mUpdatedPhotos.get(String.valueOf(mPhotoRawContactId)); 1267 if (uri != null) { 1268 editorView.setFullSizePhoto(uri); 1269 } 1270 1271 // The editor is ready now so make it visible 1272 editorView.setEnabled(mEnabled); 1273 editorView.setVisibility(View.VISIBLE); 1274 1275 // Refresh the ActionBar as the visibility of the join command 1276 // Activity can be null if we have been detached from the Activity. 1277 invalidateOptionsMenu(); 1278 } 1279 1280 /** 1281 * Invalidates the options menu if we are still associated with an Activity. 1282 */ 1283 private void invalidateOptionsMenu() { 1284 final Activity activity = getActivity(); 1285 if (activity != null) { 1286 activity.invalidateOptionsMenu(); 1287 } 1288 } 1289 1290 private boolean isReadyToBindEditors() { 1291 if (mState.isEmpty()) { 1292 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1293 Log.v(TAG, "No data to bind editors"); 1294 } 1295 return false; 1296 } 1297 if (mIsEdit && !mExistingContactDataReady) { 1298 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1299 Log.v(TAG, "Existing contact data is not ready to bind editors."); 1300 } 1301 return false; 1302 } 1303 if (mHasNewContact && !mNewContactDataReady) { 1304 if (Log.isLoggable(TAG, Log.VERBOSE)) { 1305 Log.v(TAG, "New contact data is not ready to bind editors."); 1306 } 1307 return false; 1308 } 1309 return true; 1310 } 1311 1312 /** 1313 * Removes a current editor ({@link #mState}) and rebinds new editor for a new account. 1314 * Some of old data are reused with new restriction enforced by the new account. 1315 * 1316 * @param oldState Old data being edited. 1317 * @param oldAccount Old account associated with oldState. 1318 * @param newAccount New account to be used. 1319 */ 1320 private void rebindEditorsForNewContact( 1321 RawContactDelta oldState, AccountWithDataSet oldAccount, 1322 AccountWithDataSet newAccount) { 1323 AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 1324 AccountType oldAccountType = accountTypes.getAccountTypeForAccount(oldAccount); 1325 AccountType newAccountType = accountTypes.getAccountTypeForAccount(newAccount); 1326 1327 mExistingContactDataReady = false; 1328 mNewContactDataReady = false; 1329 mState = new RawContactDeltaList(); 1330 setStateForNewContact(newAccount, newAccountType, oldState, oldAccountType, 1331 isEditingUserProfile()); 1332 if (mIsEdit) { 1333 setStateForExistingContact(isEditingUserProfile(), mRawContacts); 1334 } 1335 } 1336 1337 // 1338 // ContactEditor 1339 // 1340 1341 @Override 1342 public void setListener(Listener listener) { 1343 mListener = listener; 1344 } 1345 1346 @Override 1347 public void load(String action, Uri lookupUri, Bundle intentExtras) { 1348 mAction = action; 1349 mLookupUri = lookupUri; 1350 mIntentExtras = intentExtras; 1351 1352 if (mIntentExtras != null) { 1353 mAutoAddToDefaultGroup = 1354 mIntentExtras.containsKey(INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY); 1355 mNewLocalProfile = 1356 mIntentExtras.getBoolean(INTENT_EXTRA_NEW_LOCAL_PROFILE); 1357 mDisableDeleteMenuOption = 1358 mIntentExtras.getBoolean(INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION); 1359 if (mIntentExtras.containsKey(INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR) 1360 && mIntentExtras.containsKey(INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR)) { 1361 mMaterialPalette = new MaterialColorMapUtils.MaterialPalette( 1362 mIntentExtras.getInt(INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR), 1363 mIntentExtras.getInt(INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR)); 1364 } 1365 mRawContactIdToDisplayAlone = mIntentExtras 1366 .getLong(INTENT_EXTRA_RAW_CONTACT_ID_TO_DISPLAY_ALONE); 1367 } 1368 } 1369 1370 @Override 1371 public void setIntentExtras(Bundle extras) { 1372 getContent().setIntentExtras(extras); 1373 } 1374 1375 @Override 1376 public void onJoinCompleted(Uri uri) { 1377 onSaveCompleted(false, SaveMode.RELOAD, uri != null, uri, /* joinContactId */ null); 1378 } 1379 1380 1381 private String getNameToDisplay(Uri contactUri) { 1382 // The contact has been deleted or the uri is otherwise no longer right. 1383 if (contactUri == null) { 1384 return null; 1385 } 1386 final ContentResolver resolver = mContext.getContentResolver(); 1387 final Cursor cursor = resolver.query(contactUri, new String[]{ 1388 ContactsContract.Contacts.DISPLAY_NAME, 1389 ContactsContract.Contacts.DISPLAY_NAME_ALTERNATIVE}, null, null, null); 1390 1391 if (cursor != null) { 1392 try { 1393 if (cursor.moveToFirst()) { 1394 final String displayName = cursor.getString(0); 1395 final String displayNameAlt = cursor.getString(1); 1396 cursor.close(); 1397 return ContactDisplayUtils.getPreferredDisplayName(displayName, displayNameAlt, 1398 new ContactsPreferences(mContext)); 1399 } 1400 } finally { 1401 cursor.close(); 1402 } 1403 } 1404 return null; 1405 } 1406 1407 1408 @Override 1409 public void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded, 1410 Uri contactLookupUri, Long joinContactId) { 1411 if (hadChanges) { 1412 if (saveSucceeded) { 1413 switch (saveMode) { 1414 case SaveMode.JOIN: 1415 break; 1416 case SaveMode.SPLIT: 1417 Toast.makeText(mContext, R.string.contactUnlinkedToast, Toast.LENGTH_SHORT) 1418 .show(); 1419 break; 1420 default: 1421 final String displayName = getNameToDisplay(contactLookupUri); 1422 final String toastMessage; 1423 if (!TextUtils.isEmpty(displayName)) { 1424 toastMessage = getResources().getString( 1425 R.string.contactSavedNamedToast, displayName); 1426 } else { 1427 toastMessage = getResources().getString(R.string.contactSavedToast); 1428 } 1429 Toast.makeText(mContext, toastMessage, Toast.LENGTH_SHORT).show(); 1430 } 1431 1432 } else { 1433 Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show(); 1434 } 1435 } 1436 switch (saveMode) { 1437 case SaveMode.CLOSE: { 1438 Intent resultIntent = null; 1439 if (saveSucceeded && contactLookupUri != null) { 1440 final Uri lookupUri = ContactEditorUtils.maybeConvertToLegacyLookupUri( 1441 mContext, contactLookupUri, mLookupUri); 1442 if (Flags.getInstance().getBoolean(Experiments.CONTACT_SHEET)) { 1443 resultIntent = ObjectFactory.getContactSheetIntent(mContext, lookupUri); 1444 } 1445 if (resultIntent == null) { 1446 resultIntent = ImplicitIntentsUtil.composeQuickContactIntent( 1447 mContext, lookupUri, ScreenType.EDITOR); 1448 resultIntent.putExtra(QuickContactActivity.EXTRA_CONTACT_EDITED, true); 1449 } 1450 } else { 1451 resultIntent = null; 1452 } 1453 // It is already saved, so prevent it from being saved again 1454 mStatus = Status.CLOSING; 1455 if (mListener != null) mListener.onSaveFinished(resultIntent); 1456 break; 1457 } 1458 case SaveMode.EDITOR: { 1459 // It is already saved, so prevent it from being saved again 1460 mStatus = Status.CLOSING; 1461 if (mListener != null) mListener.onSaveFinished(/* resultIntent= */ null); 1462 break; 1463 } 1464 case SaveMode.JOIN: 1465 if (saveSucceeded && contactLookupUri != null && joinContactId != null) { 1466 joinAggregate(joinContactId); 1467 } 1468 break; 1469 case SaveMode.RELOAD: 1470 if (saveSucceeded && contactLookupUri != null) { 1471 // If this was in INSERT, we are changing into an EDIT now. 1472 // If it already was an EDIT, we are changing to the new Uri now 1473 mState = new RawContactDeltaList(); 1474 load(Intent.ACTION_EDIT, contactLookupUri, null); 1475 mStatus = Status.LOADING; 1476 getLoaderManager().restartLoader(LOADER_CONTACT, null, mContactLoaderListener); 1477 } 1478 break; 1479 1480 case SaveMode.SPLIT: 1481 mStatus = Status.CLOSING; 1482 if (mListener != null) { 1483 mListener.onContactSplit(contactLookupUri); 1484 } else { 1485 Log.d(TAG, "No listener registered, can not call onSplitFinished"); 1486 } 1487 break; 1488 } 1489 } 1490 1491 /** 1492 * Shows a list of aggregates that can be joined into the currently viewed aggregate. 1493 * 1494 * @param contactLookupUri the fresh URI for the currently edited contact (after saving it) 1495 */ 1496 private void showJoinAggregateActivity(Uri contactLookupUri) { 1497 if (contactLookupUri == null || !isAdded()) { 1498 return; 1499 } 1500 1501 mContactIdForJoin = ContentUris.parseId(contactLookupUri); 1502 final Intent intent = new Intent(mContext, ContactSelectionActivity.class); 1503 intent.setAction(UiIntentActions.PICK_JOIN_CONTACT_ACTION); 1504 intent.putExtra(UiIntentActions.TARGET_CONTACT_ID_EXTRA_KEY, mContactIdForJoin); 1505 startActivityForResult(intent, REQUEST_CODE_JOIN); 1506 } 1507 1508 // 1509 // Aggregation PopupWindow 1510 // 1511 1512 /** 1513 * Triggers an asynchronous search for aggregation suggestions. 1514 */ 1515 protected void acquireAggregationSuggestions(Context context, 1516 long rawContactId, ValuesDelta valuesDelta) { 1517 if (mAggregationSuggestionsRawContactId != rawContactId 1518 && mAggregationSuggestionView != null) { 1519 mAggregationSuggestionView.setVisibility(View.GONE); 1520 mAggregationSuggestionView = null; 1521 mAggregationSuggestionEngine.reset(); 1522 } 1523 1524 mAggregationSuggestionsRawContactId = rawContactId; 1525 1526 if (mAggregationSuggestionEngine == null) { 1527 mAggregationSuggestionEngine = new AggregationSuggestionEngine(context); 1528 mAggregationSuggestionEngine.setListener(this); 1529 mAggregationSuggestionEngine.start(); 1530 } 1531 1532 mAggregationSuggestionEngine.setContactId(getContactId()); 1533 mAggregationSuggestionEngine.setAccountFilter( 1534 getContent().getCurrentRawContactDelta().getAccountWithDataSet()); 1535 1536 mAggregationSuggestionEngine.onNameChange(valuesDelta); 1537 } 1538 1539 /** 1540 * Returns the contact ID for the currently edited contact or 0 if the contact is new. 1541 */ 1542 private long getContactId() { 1543 for (RawContactDelta rawContact : mState) { 1544 Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID); 1545 if (contactId != null) { 1546 return contactId; 1547 } 1548 } 1549 return 0; 1550 } 1551 1552 @Override 1553 public void onAggregationSuggestionChange() { 1554 final Activity activity = getActivity(); 1555 if ((activity != null && activity.isFinishing()) 1556 || !isVisible() || mState.isEmpty() || mStatus != Status.EDITING) { 1557 return; 1558 } 1559 1560 UiClosables.closeQuietly(mAggregationSuggestionPopup); 1561 1562 if (mAggregationSuggestionEngine.getSuggestedContactCount() == 0) { 1563 return; 1564 } 1565 1566 final View anchorView = getAggregationAnchorView(); 1567 if (anchorView == null) { 1568 return; // Raw contact deleted? 1569 } 1570 mAggregationSuggestionPopup = new ListPopupWindow(mContext, null); 1571 mAggregationSuggestionPopup.setAnchorView(anchorView); 1572 mAggregationSuggestionPopup.setWidth(anchorView.getWidth()); 1573 mAggregationSuggestionPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); 1574 mAggregationSuggestionPopup.setAdapter( 1575 new AggregationSuggestionAdapter( 1576 getActivity(), 1577 /* listener =*/ this, 1578 mAggregationSuggestionEngine.getSuggestions())); 1579 mAggregationSuggestionPopup.setOnItemClickListener(new AdapterView.OnItemClickListener() { 1580 @Override 1581 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 1582 final AggregationSuggestionView suggestionView = (AggregationSuggestionView) view; 1583 suggestionView.handleItemClickEvent(); 1584 UiClosables.closeQuietly(mAggregationSuggestionPopup); 1585 mAggregationSuggestionPopup = null; 1586 } 1587 }); 1588 mAggregationSuggestionPopup.show(); 1589 } 1590 1591 /** 1592 * Returns the editor view that should be used as the anchor for aggregation suggestions. 1593 */ 1594 protected View getAggregationAnchorView() { 1595 return getContent().getAggregationAnchorView(); 1596 } 1597 1598 /** 1599 * Joins the suggested contact (specified by the id's of constituent raw 1600 * contacts), save all changes, and stay in the editor. 1601 */ 1602 public void doJoinSuggestedContact(long[] rawContactIds) { 1603 if (!hasValidState() || mStatus != Status.EDITING) { 1604 return; 1605 } 1606 1607 mState.setJoinWithRawContacts(rawContactIds); 1608 save(SaveMode.RELOAD); 1609 } 1610 1611 @Override 1612 public void onEditAction(Uri contactLookupUri, long rawContactId) { 1613 SuggestionEditConfirmationDialogFragment.show(this, contactLookupUri, rawContactId); 1614 } 1615 1616 /** 1617 * Abandons the currently edited contact and switches to editing the selected raw contact, 1618 * transferring all the data there 1619 */ 1620 public void doEditSuggestedContact(Uri contactUri, long rawContactId) { 1621 if (mListener != null) { 1622 // make sure we don't save this contact when closing down 1623 mStatus = Status.CLOSING; 1624 mListener.onEditOtherRawContactRequested(contactUri, rawContactId, 1625 getContent().getCurrentRawContactDelta().getContentValues()); 1626 } 1627 } 1628 1629 /** 1630 * Sets group metadata on all bound editors. 1631 */ 1632 protected void setGroupMetaData() { 1633 if (mGroupMetaData != null) { 1634 getContent().setGroupMetaData(mGroupMetaData); 1635 } 1636 } 1637 1638 /** 1639 * Persist the accumulated editor deltas. 1640 * 1641 * @param joinContactId the raw contact ID to join the contact being saved to after the save, 1642 * may be null. 1643 */ 1644 protected boolean doSaveAction(int saveMode, Long joinContactId) { 1645 final Intent intent = ContactSaveService.createSaveContactIntent(mContext, mState, 1646 SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(), 1647 ((Activity) mContext).getClass(), 1648 ContactEditorActivity.ACTION_SAVE_COMPLETED, mUpdatedPhotos, 1649 JOIN_CONTACT_ID_EXTRA_KEY, joinContactId); 1650 return startSaveService(mContext, intent, saveMode); 1651 } 1652 1653 private boolean startSaveService(Context context, Intent intent, int saveMode) { 1654 final boolean result = ContactSaveService.startService( 1655 context, intent, saveMode); 1656 if (!result) { 1657 onCancelEditConfirmed(); 1658 } 1659 return result; 1660 } 1661 1662 // 1663 // Join Activity 1664 // 1665 1666 /** 1667 * Performs aggregation with the contact selected by the user from suggestions or A-Z list. 1668 */ 1669 protected void joinAggregate(final long contactId) { 1670 final Intent intent = ContactSaveService.createJoinContactsIntent( 1671 mContext, mContactIdForJoin, contactId, ContactEditorActivity.class, 1672 ContactEditorActivity.ACTION_JOIN_COMPLETED); 1673 mContext.startService(intent); 1674 } 1675 1676 public void removePhoto() { 1677 getContent().removePhoto(); 1678 mUpdatedPhotos.remove(String.valueOf(mPhotoRawContactId)); 1679 } 1680 1681 public void updatePhoto(Uri uri) throws FileNotFoundException { 1682 final Bitmap bitmap = ContactPhotoUtils.getBitmapFromUri(getActivity(), uri); 1683 if (bitmap == null || bitmap.getHeight() <= 0 || bitmap.getWidth() <= 0) { 1684 Toast.makeText(mContext, R.string.contactPhotoSavedErrorToast, 1685 Toast.LENGTH_SHORT).show(); 1686 return; 1687 } 1688 mUpdatedPhotos.putParcelable(String.valueOf(mPhotoRawContactId), uri); 1689 getContent().updatePhoto(uri); 1690 } 1691 1692 public void setPrimaryPhoto() { 1693 getContent().setPrimaryPhoto(); 1694 } 1695 1696 @Override 1697 public void onNameFieldChanged(long rawContactId, ValuesDelta valuesDelta) { 1698 final Activity activity = getActivity(); 1699 if (activity == null || activity.isFinishing()) { 1700 return; 1701 } 1702 acquireAggregationSuggestions(activity, rawContactId, valuesDelta); 1703 } 1704 1705 @Override 1706 public void onRebindEditorsForNewContact(RawContactDelta oldState, 1707 AccountWithDataSet oldAccount, AccountWithDataSet newAccount) { 1708 mNewContactAccountChanged = true; 1709 mAccountWithDataSet = newAccount; 1710 rebindEditorsForNewContact(oldState, oldAccount, newAccount); 1711 } 1712 1713 @Override 1714 public void onBindEditorsFailed() { 1715 final Activity activity = getActivity(); 1716 if (activity != null && !activity.isFinishing()) { 1717 Toast.makeText(activity, R.string.editor_failed_to_load, 1718 Toast.LENGTH_SHORT).show(); 1719 activity.setResult(Activity.RESULT_CANCELED); 1720 activity.finish(); 1721 } 1722 } 1723 1724 @Override 1725 public void onEditorsBound() { 1726 final Activity activity = getActivity(); 1727 if (activity == null || activity.isFinishing()) { 1728 return; 1729 } 1730 getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupsLoaderListener); 1731 } 1732 1733 @Override 1734 public void onPhotoEditorViewClicked() { 1735 // For contacts composed of a single writable raw contact, or raw contacts have no more 1736 // than 1 photo, clicking the photo view simply opens the source photo dialog 1737 getEditorActivity().changePhoto(getPhotoMode()); 1738 } 1739 1740 private int getPhotoMode() { 1741 return getContent().isWritablePhotoSet() ? PhotoActionPopup.Modes.WRITE_ABLE_PHOTO 1742 : PhotoActionPopup.Modes.NO_PHOTO; 1743 } 1744 1745 private ContactEditorActivity getEditorActivity() { 1746 return (ContactEditorActivity) getActivity(); 1747 } 1748 1749 private RawContactEditorView getContent() { 1750 return (RawContactEditorView) mContent; 1751 } 1752} 1753