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