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