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