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