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