ContactEditorFragment.java revision 4a1c574cd62eb7ca1e0fcc3a61e5378e5e0787fe
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17package com.android.contacts.editor; 18 19import android.accounts.Account; 20import android.app.Activity; 21import android.app.AlertDialog; 22import android.app.Dialog; 23import android.app.DialogFragment; 24import android.app.Fragment; 25import android.app.LoaderManager; 26import android.app.LoaderManager.LoaderCallbacks; 27import android.content.ActivityNotFoundException; 28import android.content.ContentUris; 29import android.content.ContentValues; 30import android.content.Context; 31import android.content.CursorLoader; 32import android.content.DialogInterface; 33import android.content.Intent; 34import android.content.Loader; 35import android.database.Cursor; 36import android.graphics.Bitmap; 37import android.graphics.Rect; 38import android.media.RingtoneManager; 39import android.net.Uri; 40import android.os.Bundle; 41import android.os.SystemClock; 42import android.provider.ContactsContract.CommonDataKinds.Email; 43import android.provider.ContactsContract.CommonDataKinds.Event; 44import android.provider.ContactsContract.CommonDataKinds.Organization; 45import android.provider.ContactsContract.CommonDataKinds.Phone; 46import android.provider.ContactsContract.CommonDataKinds.Photo; 47import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 48import android.provider.ContactsContract.Contacts; 49import android.provider.ContactsContract.Groups; 50import android.provider.ContactsContract.Intents; 51import android.provider.ContactsContract.QuickContact; 52import android.provider.ContactsContract.RawContacts; 53import android.text.TextUtils; 54import android.util.Log; 55import android.view.LayoutInflater; 56import android.view.Menu; 57import android.view.MenuInflater; 58import android.view.MenuItem; 59import android.view.View; 60import android.view.ViewGroup; 61import android.widget.AdapterView; 62import android.widget.AdapterView.OnItemClickListener; 63import android.widget.BaseAdapter; 64import android.widget.LinearLayout; 65import android.widget.ListPopupWindow; 66import android.widget.Toast; 67 68import com.android.contacts.ContactSaveService; 69import com.android.contacts.GroupMetaDataLoader; 70import com.android.contacts.R; 71import com.android.contacts.activities.ContactEditorAccountsChangedActivity; 72import com.android.contacts.activities.ContactEditorActivity; 73import com.android.contacts.common.model.AccountTypeManager; 74import com.android.contacts.common.model.ValuesDelta; 75import com.android.contacts.common.model.account.AccountType; 76import com.android.contacts.common.model.account.AccountWithDataSet; 77import com.android.contacts.common.model.account.GoogleAccountType; 78import com.android.contacts.common.util.AccountsListAdapter; 79import com.android.contacts.common.util.AccountsListAdapter.AccountListFilter; 80import com.android.contacts.detail.PhotoSelectionHandler; 81import com.android.contacts.editor.AggregationSuggestionEngine.Suggestion; 82import com.android.contacts.editor.Editor.EditorListener; 83import com.android.contacts.common.model.Contact; 84import com.android.contacts.common.model.ContactLoader; 85import com.android.contacts.common.model.RawContact; 86import com.android.contacts.common.model.RawContactDelta; 87import com.android.contacts.common.model.RawContactDeltaList; 88import com.android.contacts.common.model.RawContactModifier; 89import com.android.contacts.list.UiIntentActions; 90import com.android.contacts.quickcontact.QuickContactActivity; 91import com.android.contacts.util.ContactPhotoUtils; 92import com.android.contacts.util.HelpUtils; 93import com.android.contacts.util.PhoneCapabilityTester; 94import com.android.contacts.util.UiClosables; 95import com.google.common.collect.ImmutableList; 96import com.google.common.collect.Lists; 97 98import java.io.FileNotFoundException; 99import java.util.ArrayList; 100import java.util.Collections; 101import java.util.Comparator; 102import java.util.HashMap; 103import java.util.List; 104 105public class ContactEditorFragment extends Fragment implements 106 SplitContactConfirmationDialogFragment.Listener, 107 AggregationSuggestionEngine.Listener, AggregationSuggestionView.Listener, 108 RawContactReadOnlyEditorView.Listener { 109 110 private static final String TAG = ContactEditorFragment.class.getSimpleName(); 111 112 private static final int LOADER_DATA = 1; 113 private static final int LOADER_GROUPS = 2; 114 115 private static final String KEY_URI = "uri"; 116 private static final String KEY_ACTION = "action"; 117 private static final String KEY_EDIT_STATE = "state"; 118 private static final String KEY_RAW_CONTACT_ID_REQUESTING_PHOTO = "photorequester"; 119 private static final String KEY_VIEW_ID_GENERATOR = "viewidgenerator"; 120 private static final String KEY_CURRENT_PHOTO_URI = "currentphotouri"; 121 private static final String KEY_CONTACT_ID_FOR_JOIN = "contactidforjoin"; 122 private static final String KEY_CONTACT_WRITABLE_FOR_JOIN = "contactwritableforjoin"; 123 private static final String KEY_SHOW_JOIN_SUGGESTIONS = "showJoinSuggestions"; 124 private static final String KEY_ENABLED = "enabled"; 125 private static final String KEY_STATUS = "status"; 126 private static final String KEY_NEW_LOCAL_PROFILE = "newLocalProfile"; 127 private static final String KEY_IS_USER_PROFILE = "isUserProfile"; 128 private static final String KEY_DISABLE_DELETE_MENU_OPTION = "disableDeleteMenuOption"; 129 private static final String KEY_UPDATED_PHOTOS = "updatedPhotos"; 130 private static final String KEY_IS_EDIT = "isEdit"; 131 private static final String KEY_HAS_NEW_CONTACT = "hasNewContact"; 132 private static final String KEY_NEW_CONTACT_READY = "newContactDataReady"; 133 private static final String KEY_EXISTING_CONTACT_READY = "existingContactDataReady"; 134 private static final String KEY_RAW_CONTACTS = "rawContacts"; 135 private static final String KEY_SEND_TO_VOICE_MAIL_STATE = "sendToVoicemailState"; 136 private static final String KEY_CUSTOM_RINGTONE = "customRingtone"; 137 private static final String KEY_ARE_PHONE_OPTIONS_CHANGEABLE = "arePhoneOptionsChangable"; 138 private static final String KEY_EXPANDED_EDITORS = "expandedEditors"; 139 140 public static final String SAVE_MODE_EXTRA_KEY = "saveMode"; 141 142 143 /** 144 * An intent extra that forces the editor to add the edited contact 145 * to the default group (e.g. "My Contacts"). 146 */ 147 public static final String INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY = "addToDefaultDirectory"; 148 149 public static final String INTENT_EXTRA_NEW_LOCAL_PROFILE = "newLocalProfile"; 150 151 public static final String INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION = 152 "disableDeleteMenuOption"; 153 154 /** 155 * Modes that specify what the AsyncTask has to perform after saving 156 */ 157 public interface SaveMode { 158 /** 159 * Close the editor after saving 160 */ 161 public static final int CLOSE = 0; 162 163 /** 164 * Reload the data so that the user can continue editing 165 */ 166 public static final int RELOAD = 1; 167 168 /** 169 * Split the contact after saving 170 */ 171 public static final int SPLIT = 2; 172 173 /** 174 * Join another contact after saving 175 */ 176 public static final int JOIN = 3; 177 178 /** 179 * Navigate to Contacts Home activity after saving. 180 */ 181 public static final int HOME = 4; 182 } 183 184 private interface Status { 185 /** 186 * The loader is fetching data 187 */ 188 public static final int LOADING = 0; 189 190 /** 191 * Not currently busy. We are waiting for the user to enter data 192 */ 193 public static final int EDITING = 1; 194 195 /** 196 * The data is currently being saved. This is used to prevent more 197 * auto-saves (they shouldn't overlap) 198 */ 199 public static final int SAVING = 2; 200 201 /** 202 * Prevents any more saves. This is used if in the following cases: 203 * - After Save/Close 204 * - After Revert 205 * - After the user has accepted an edit suggestion 206 */ 207 public static final int CLOSING = 3; 208 209 /** 210 * Prevents saving while running a child activity. 211 */ 212 public static final int SUB_ACTIVITY = 4; 213 } 214 215 private static final int REQUEST_CODE_JOIN = 0; 216 private static final int REQUEST_CODE_ACCOUNTS_CHANGED = 1; 217 private static final int REQUEST_CODE_PICK_RINGTONE = 2; 218 219 /** 220 * The raw contact for which we started "take photo" or "choose photo from gallery" most 221 * recently. Used to restore {@link #mCurrentPhotoHandler} after orientation change. 222 */ 223 private long mRawContactIdRequestingPhoto; 224 /** 225 * The {@link PhotoHandler} for the photo editor for the {@link #mRawContactIdRequestingPhoto} 226 * raw contact. 227 * 228 * A {@link PhotoHandler} is created for each photo editor in {@link #bindPhotoHandler}, but 229 * the only "active" one should get the activity result. This member represents the active 230 * one. 231 */ 232 private PhotoHandler mCurrentPhotoHandler; 233 234 private final EntityDeltaComparator mComparator = new EntityDeltaComparator(); 235 236 private Cursor mGroupMetaData; 237 238 private Uri mCurrentPhotoUri; 239 private Bundle mUpdatedPhotos = new Bundle(); 240 241 private Context mContext; 242 private String mAction; 243 private Uri mLookupUri; 244 private Bundle mIntentExtras; 245 private Listener mListener; 246 247 private long mContactIdForJoin; 248 private boolean mContactWritableForJoin; 249 250 private ContactEditorUtils mEditorUtils; 251 252 private LinearLayout mContent; 253 private RawContactDeltaList mState; 254 255 private ViewIdGenerator mViewIdGenerator; 256 257 private long mLoaderStartTime; 258 259 private int mStatus; 260 261 // Whether to show the new contact blank form and if it's corresponding delta is ready. 262 private boolean mHasNewContact = false; 263 private boolean mNewContactDataReady = false; 264 265 // Whether it's an edit of existing contact and if it's corresponding delta is ready. 266 private boolean mIsEdit = false; 267 private boolean mExistingContactDataReady = false; 268 269 // Variables related to phone specific option menus 270 private boolean mSendToVoicemailState; 271 private boolean mArePhoneOptionsChangable; 272 private String mCustomRingtone; 273 274 // This is used to pre-populate the editor with a display name when a user edits a read-only 275 // contact. 276 private String mDefaultDisplayName; 277 278 // Used to temporarily store existing contact data during a rebind call (i.e. account switch) 279 private ImmutableList<RawContact> mRawContacts; 280 281 // Used to store which raw contact editors have been expanded. Keyed on raw contact ids. 282 private HashMap<Long, Boolean> mExpandedEditors = new HashMap<Long, Boolean>(); 283 284 private AggregationSuggestionEngine mAggregationSuggestionEngine; 285 private long mAggregationSuggestionsRawContactId; 286 private View mAggregationSuggestionView; 287 288 private ListPopupWindow mAggregationSuggestionPopup; 289 290 private static final class AggregationSuggestionAdapter extends BaseAdapter { 291 private final Activity mActivity; 292 private final boolean mSetNewContact; 293 private final AggregationSuggestionView.Listener mListener; 294 private final List<Suggestion> mSuggestions; 295 296 public AggregationSuggestionAdapter(Activity activity, boolean setNewContact, 297 AggregationSuggestionView.Listener listener, List<Suggestion> suggestions) { 298 mActivity = activity; 299 mSetNewContact = setNewContact; 300 mListener = listener; 301 mSuggestions = suggestions; 302 } 303 304 @Override 305 public View getView(int position, View convertView, ViewGroup parent) { 306 Suggestion suggestion = (Suggestion) getItem(position); 307 LayoutInflater inflater = mActivity.getLayoutInflater(); 308 AggregationSuggestionView suggestionView = 309 (AggregationSuggestionView) inflater.inflate( 310 R.layout.aggregation_suggestions_item, null); 311 suggestionView.setNewContact(mSetNewContact); 312 suggestionView.setListener(mListener); 313 suggestionView.bindSuggestion(suggestion); 314 return suggestionView; 315 } 316 317 @Override 318 public long getItemId(int position) { 319 return position; 320 } 321 322 @Override 323 public Object getItem(int position) { 324 return mSuggestions.get(position); 325 } 326 327 @Override 328 public int getCount() { 329 return mSuggestions.size(); 330 } 331 } 332 333 private OnItemClickListener mAggregationSuggestionItemClickListener = 334 new OnItemClickListener() { 335 @Override 336 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 337 final AggregationSuggestionView suggestionView = (AggregationSuggestionView) view; 338 suggestionView.handleItemClickEvent(); 339 UiClosables.closeQuietly(mAggregationSuggestionPopup); 340 mAggregationSuggestionPopup = null; 341 } 342 }; 343 344 private boolean mAutoAddToDefaultGroup; 345 346 private boolean mEnabled = true; 347 private boolean mRequestFocus; 348 private boolean mNewLocalProfile = false; 349 private boolean mIsUserProfile = false; 350 private boolean mDisableDeleteMenuOption = false; 351 352 public ContactEditorFragment() { 353 } 354 355 public void setEnabled(boolean enabled) { 356 if (mEnabled != enabled) { 357 mEnabled = enabled; 358 if (mContent != null) { 359 int count = mContent.getChildCount(); 360 for (int i = 0; i < count; i++) { 361 mContent.getChildAt(i).setEnabled(enabled); 362 } 363 } 364 setAggregationSuggestionViewEnabled(enabled); 365 final Activity activity = getActivity(); 366 if (activity != null) activity.invalidateOptionsMenu(); 367 } 368 } 369 370 @Override 371 public void onAttach(Activity activity) { 372 super.onAttach(activity); 373 mContext = activity; 374 mEditorUtils = ContactEditorUtils.getInstance(mContext); 375 } 376 377 @Override 378 public void onStop() { 379 super.onStop(); 380 381 UiClosables.closeQuietly(mAggregationSuggestionPopup); 382 383 // If anything was left unsaved, save it now but keep the editor open. 384 if (!getActivity().isChangingConfigurations() && mStatus == Status.EDITING) { 385 save(SaveMode.RELOAD); 386 } 387 } 388 389 @Override 390 public void onDestroy() { 391 super.onDestroy(); 392 if (mAggregationSuggestionEngine != null) { 393 mAggregationSuggestionEngine.quit(); 394 } 395 } 396 397 @Override 398 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { 399 final View view = inflater.inflate(R.layout.contact_editor_fragment, container, false); 400 401 mContent = (LinearLayout) view.findViewById(R.id.editors); 402 403 setHasOptionsMenu(true); 404 405 return view; 406 } 407 408 @Override 409 public void onActivityCreated(Bundle savedInstanceState) { 410 super.onActivityCreated(savedInstanceState); 411 412 validateAction(mAction); 413 414 if (mState.isEmpty()) { 415 // The delta list may not have finished loading before orientation change happens. 416 // In this case, there will be a saved state but deltas will be missing. Reload from 417 // database. 418 if (Intent.ACTION_EDIT.equals(mAction)) { 419 // Either... 420 // 1) orientation change but load never finished. 421 // or 422 // 2) not an orientation change. data needs to be loaded for first time. 423 getLoaderManager().initLoader(LOADER_DATA, null, mDataLoaderListener); 424 } 425 } else { 426 // Orientation change, we already have mState, it was loaded by onCreate 427 bindEditors(); 428 } 429 430 // Handle initial actions only when existing state missing 431 if (savedInstanceState == null) { 432 if (Intent.ACTION_EDIT.equals(mAction)) { 433 mIsEdit = true; 434 } else if (Intent.ACTION_INSERT.equals(mAction)) { 435 mHasNewContact = true; 436 final Account account = mIntentExtras == null ? null : 437 (Account) mIntentExtras.getParcelable(Intents.Insert.EXTRA_ACCOUNT); 438 final String dataSet = mIntentExtras == null ? null : 439 mIntentExtras.getString(Intents.Insert.EXTRA_DATA_SET); 440 441 if (account != null) { 442 // Account specified in Intent 443 createContact(new AccountWithDataSet(account.name, account.type, dataSet)); 444 } else { 445 // No Account specified. Let the user choose 446 // Load Accounts async so that we can present them 447 selectAccountAndCreateContact(); 448 } 449 } 450 } 451 } 452 453 /** 454 * Checks if the requested action is valid. 455 * 456 * @param action The action to test. 457 * @throws IllegalArgumentException when the action is invalid. 458 */ 459 private void validateAction(String action) { 460 if (Intent.ACTION_EDIT.equals(action) || Intent.ACTION_INSERT.equals(action) || 461 ContactEditorActivity.ACTION_SAVE_COMPLETED.equals(action)) { 462 return; 463 } 464 throw new IllegalArgumentException("Unknown Action String " + mAction + 465 ". Only support " + Intent.ACTION_EDIT + " or " + Intent.ACTION_INSERT + " or " + 466 ContactEditorActivity.ACTION_SAVE_COMPLETED); 467 } 468 469 @Override 470 public void onStart() { 471 getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupLoaderListener); 472 super.onStart(); 473 } 474 475 public void load(String action, Uri lookupUri, Bundle intentExtras) { 476 mAction = action; 477 mLookupUri = lookupUri; 478 mIntentExtras = intentExtras; 479 mAutoAddToDefaultGroup = mIntentExtras != null 480 && mIntentExtras.containsKey(INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY); 481 mNewLocalProfile = mIntentExtras != null 482 && mIntentExtras.getBoolean(INTENT_EXTRA_NEW_LOCAL_PROFILE); 483 mDisableDeleteMenuOption = mIntentExtras != null 484 && mIntentExtras.getBoolean(INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION); 485 } 486 487 public void setListener(Listener value) { 488 mListener = value; 489 } 490 491 @Override 492 public void onCreate(Bundle savedState) { 493 if (savedState != null) { 494 // Restore mUri before calling super.onCreate so that onInitializeLoaders 495 // would already have a uri and an action to work with 496 mLookupUri = savedState.getParcelable(KEY_URI); 497 mAction = savedState.getString(KEY_ACTION); 498 } 499 500 super.onCreate(savedState); 501 502 if (savedState == null) { 503 // If savedState is non-null, onRestoreInstanceState() will restore the generator. 504 mViewIdGenerator = new ViewIdGenerator(); 505 } else { 506 // Read state from savedState. No loading involved here 507 mState = savedState.<RawContactDeltaList> getParcelable(KEY_EDIT_STATE); 508 mRawContactIdRequestingPhoto = savedState.getLong( 509 KEY_RAW_CONTACT_ID_REQUESTING_PHOTO); 510 mViewIdGenerator = savedState.getParcelable(KEY_VIEW_ID_GENERATOR); 511 mCurrentPhotoUri = savedState.getParcelable(KEY_CURRENT_PHOTO_URI); 512 mContactIdForJoin = savedState.getLong(KEY_CONTACT_ID_FOR_JOIN); 513 mContactWritableForJoin = savedState.getBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN); 514 mAggregationSuggestionsRawContactId = savedState.getLong(KEY_SHOW_JOIN_SUGGESTIONS); 515 mEnabled = savedState.getBoolean(KEY_ENABLED); 516 mStatus = savedState.getInt(KEY_STATUS); 517 mNewLocalProfile = savedState.getBoolean(KEY_NEW_LOCAL_PROFILE); 518 mDisableDeleteMenuOption = savedState.getBoolean(KEY_DISABLE_DELETE_MENU_OPTION); 519 mIsUserProfile = savedState.getBoolean(KEY_IS_USER_PROFILE); 520 mUpdatedPhotos = savedState.getParcelable(KEY_UPDATED_PHOTOS); 521 mIsEdit = savedState.getBoolean(KEY_IS_EDIT); 522 mHasNewContact = savedState.getBoolean(KEY_HAS_NEW_CONTACT); 523 mNewContactDataReady = savedState.getBoolean(KEY_NEW_CONTACT_READY); 524 mExistingContactDataReady = savedState.getBoolean(KEY_EXISTING_CONTACT_READY); 525 mRawContacts = ImmutableList.copyOf(savedState.<RawContact>getParcelableArrayList( 526 KEY_RAW_CONTACTS)); 527 mSendToVoicemailState = savedState.getBoolean(KEY_SEND_TO_VOICE_MAIL_STATE); 528 mCustomRingtone = savedState.getString(KEY_CUSTOM_RINGTONE); 529 mArePhoneOptionsChangable = savedState.getBoolean(KEY_ARE_PHONE_OPTIONS_CHANGEABLE); 530 mExpandedEditors = (HashMap<Long, Boolean>) 531 savedState.getSerializable(KEY_EXPANDED_EDITORS); 532 } 533 534 // mState can still be null because it may not have have finished loading before 535 // onSaveInstanceState was called. 536 if (mState == null) { 537 mState = new RawContactDeltaList(); 538 } 539 } 540 541 public void setData(Contact contact) { 542 543 // If we have already loaded data, we do not want to change it here to not confuse the user 544 if (!mState.isEmpty()) { 545 Log.v(TAG, "Ignoring background change. This will have to be rebased later"); 546 return; 547 } 548 549 // See if this edit operation needs to be redirected to a custom editor 550 mRawContacts = contact.getRawContacts(); 551 if (mRawContacts.size() == 1) { 552 RawContact rawContact = mRawContacts.get(0); 553 String type = rawContact.getAccountTypeString(); 554 String dataSet = rawContact.getDataSet(); 555 AccountType accountType = rawContact.getAccountType(mContext); 556 if (accountType.getEditContactActivityClassName() != null && 557 !accountType.areContactsWritable()) { 558 if (mListener != null) { 559 String name = rawContact.getAccountName(); 560 long rawContactId = rawContact.getId(); 561 mListener.onCustomEditContactActivityRequested( 562 new AccountWithDataSet(name, type, dataSet), 563 ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId), 564 mIntentExtras, true); 565 } 566 return; 567 } 568 } 569 570 String displayName = null; 571 // Check for writable raw contacts. If there are none, then we need to create one so user 572 // can edit. For the user profile case, there is already an editable contact. 573 if (!contact.isUserProfile() && !contact.isWritableContact(mContext)) { 574 mHasNewContact = true; 575 576 // This is potentially an asynchronous call and will add deltas to list. 577 selectAccountAndCreateContact(); 578 displayName = contact.getDisplayName(); 579 } 580 581 // This also adds deltas to list 582 // If displayName is null at this point it is simply ignored later on by the editor. 583 bindEditorsForExistingContact(displayName, contact.isUserProfile(), 584 mRawContacts); 585 586 bindMenuItemsForPhone(contact); 587 } 588 589 @Override 590 public void onExternalEditorRequest(AccountWithDataSet account, Uri uri) { 591 mListener.onCustomEditContactActivityRequested(account, uri, null, false); 592 } 593 594 @Override 595 public void onEditorExpansionChanged() { 596 updatedExpandedEditorsMap(); 597 } 598 599 private void bindEditorsForExistingContact(String displayName, boolean isUserProfile, 600 ImmutableList<RawContact> rawContacts) { 601 setEnabled(true); 602 mDefaultDisplayName = displayName; 603 604 mState.addAll(rawContacts.iterator()); 605 setIntentExtras(mIntentExtras); 606 mIntentExtras = null; 607 608 // For user profile, change the contacts query URI 609 mIsUserProfile = isUserProfile; 610 boolean localProfileExists = false; 611 612 if (mIsUserProfile) { 613 for (RawContactDelta state : mState) { 614 // For profile contacts, we need a different query URI 615 state.setProfileQueryUri(); 616 // Try to find a local profile contact 617 if (state.getValues().getAsString(RawContacts.ACCOUNT_TYPE) == null) { 618 localProfileExists = true; 619 } 620 } 621 // Editor should always present a local profile for editing 622 if (!localProfileExists) { 623 final RawContact rawContact = new RawContact(); 624 rawContact.setAccountToLocal(); 625 626 RawContactDelta insert = new RawContactDelta(ValuesDelta.fromAfter( 627 rawContact.getValues())); 628 insert.setProfileQueryUri(); 629 mState.add(insert); 630 } 631 } 632 mRequestFocus = true; 633 mExistingContactDataReady = true; 634 bindEditors(); 635 } 636 637 private void bindMenuItemsForPhone(Contact contact) { 638 mSendToVoicemailState = contact.isSendToVoicemail(); 639 mCustomRingtone = contact.getCustomRingtone(); 640 mArePhoneOptionsChangable = arePhoneOptionsChangable(contact); 641 } 642 643 private boolean arePhoneOptionsChangable(Contact contact) { 644 return contact != null && !contact.isDirectoryEntry() 645 && PhoneCapabilityTester.isPhone(mContext); 646 } 647 648 /** 649 * Merges extras from the intent. 650 */ 651 public void setIntentExtras(Bundle extras) { 652 if (extras == null || extras.size() == 0) { 653 return; 654 } 655 656 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 657 for (RawContactDelta state : mState) { 658 final AccountType type = state.getAccountType(accountTypes); 659 if (type.areContactsWritable()) { 660 // Apply extras to the first writable raw contact only 661 RawContactModifier.parseExtras(mContext, type, state, extras); 662 break; 663 } 664 } 665 } 666 667 private void selectAccountAndCreateContact() { 668 // If this is a local profile, then skip the logic about showing the accounts changed 669 // activity and create a phone-local contact. 670 if (mNewLocalProfile) { 671 createContact(null); 672 return; 673 } 674 675 // If there is no default account or the accounts have changed such that we need to 676 // prompt the user again, then launch the account prompt. 677 if (mEditorUtils.shouldShowAccountChangedNotification()) { 678 Intent intent = new Intent(mContext, ContactEditorAccountsChangedActivity.class); 679 mStatus = Status.SUB_ACTIVITY; 680 startActivityForResult(intent, REQUEST_CODE_ACCOUNTS_CHANGED); 681 } else { 682 // Otherwise, there should be a default account. Then either create a local contact 683 // (if default account is null) or create a contact with the specified account. 684 AccountWithDataSet defaultAccount = mEditorUtils.getDefaultAccount(); 685 createContact(defaultAccount); 686 } 687 } 688 689 /** 690 * Create a contact by automatically selecting the first account. If there's no available 691 * account, a device-local contact should be created. 692 */ 693 private void createContact() { 694 final List<AccountWithDataSet> accounts = 695 AccountTypeManager.getInstance(mContext).getAccounts(true); 696 // No Accounts available. Create a phone-local contact. 697 if (accounts.isEmpty()) { 698 createContact(null); 699 return; 700 } 701 702 // We have an account switcher in "create-account" screen, so don't need to ask a user to 703 // select an account here. 704 createContact(accounts.get(0)); 705 } 706 707 /** 708 * Shows account creation screen associated with a given account. 709 * 710 * @param account may be null to signal a device-local contact should be created. 711 */ 712 private void createContact(AccountWithDataSet account) { 713 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 714 final AccountType accountType = accountTypes.getAccountTypeForAccount(account); 715 716 if (accountType.getCreateContactActivityClassName() != null) { 717 if (mListener != null) { 718 mListener.onCustomCreateContactActivityRequested(account, mIntentExtras); 719 } 720 } else { 721 bindEditorsForNewContact(account, accountType); 722 } 723 } 724 725 /** 726 * Removes a current editor ({@link #mState}) and rebinds new editor for a new account. 727 * Some of old data are reused with new restriction enforced by the new account. 728 * 729 * @param oldState Old data being edited. 730 * @param oldAccount Old account associated with oldState. 731 * @param newAccount New account to be used. 732 */ 733 private void rebindEditorsForNewContact( 734 RawContactDelta oldState, AccountWithDataSet oldAccount, 735 AccountWithDataSet newAccount) { 736 AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 737 AccountType oldAccountType = accountTypes.getAccountTypeForAccount(oldAccount); 738 AccountType newAccountType = accountTypes.getAccountTypeForAccount(newAccount); 739 740 if (newAccountType.getCreateContactActivityClassName() != null) { 741 Log.w(TAG, "external activity called in rebind situation"); 742 if (mListener != null) { 743 mListener.onCustomCreateContactActivityRequested(newAccount, mIntentExtras); 744 } 745 } else { 746 mExistingContactDataReady = false; 747 mNewContactDataReady = false; 748 mState = new RawContactDeltaList(); 749 bindEditorsForNewContact(newAccount, newAccountType, oldState, oldAccountType); 750 if (mIsEdit) { 751 bindEditorsForExistingContact(mDefaultDisplayName, mIsUserProfile, mRawContacts); 752 } 753 } 754 } 755 756 private void bindEditorsForNewContact(AccountWithDataSet account, 757 final AccountType accountType) { 758 bindEditorsForNewContact(account, accountType, null, null); 759 } 760 761 private void bindEditorsForNewContact(AccountWithDataSet newAccount, 762 final AccountType newAccountType, RawContactDelta oldState, 763 AccountType oldAccountType) { 764 mStatus = Status.EDITING; 765 766 final RawContact rawContact = new RawContact(); 767 rawContact.setAccount(newAccount); 768 769 final ValuesDelta valuesDelta = ValuesDelta.fromAfter(rawContact.getValues()); 770 final RawContactDelta insert = new RawContactDelta(valuesDelta); 771 if (oldState == null) { 772 // Parse any values from incoming intent 773 RawContactModifier.parseExtras(mContext, newAccountType, insert, mIntentExtras); 774 } else { 775 RawContactModifier.migrateStateForNewContact(mContext, oldState, insert, 776 oldAccountType, newAccountType); 777 } 778 779 // Ensure we have some default fields (if the account type does not support a field, 780 // ensureKind will not add it, so it is safe to add e.g. Event) 781 RawContactModifier.ensureKindExists(insert, newAccountType, Phone.CONTENT_ITEM_TYPE); 782 RawContactModifier.ensureKindExists(insert, newAccountType, Email.CONTENT_ITEM_TYPE); 783 RawContactModifier.ensureKindExists(insert, newAccountType, Organization.CONTENT_ITEM_TYPE); 784 RawContactModifier.ensureKindExists(insert, newAccountType, Event.CONTENT_ITEM_TYPE); 785 RawContactModifier.ensureKindExists(insert, newAccountType, 786 StructuredPostal.CONTENT_ITEM_TYPE); 787 788 // Set the correct URI for saving the contact as a profile 789 if (mNewLocalProfile) { 790 insert.setProfileQueryUri(); 791 } 792 793 mState.add(insert); 794 795 mRequestFocus = true; 796 797 mNewContactDataReady = true; 798 bindEditors(); 799 } 800 801 private void bindEditors() { 802 // bindEditors() can only bind views if there is data in mState, so immediately return 803 // if mState is null 804 if (mState.isEmpty()) { 805 return; 806 } 807 808 // Check if delta list is ready. Delta list is populated from existing data and when 809 // editing an read-only contact, it's also populated with newly created data for the 810 // blank form. When the data is not ready, skip. This method will be called multiple times. 811 if ((mIsEdit && !mExistingContactDataReady) || (mHasNewContact && !mNewContactDataReady)) { 812 return; 813 } 814 815 // Sort the editors 816 Collections.sort(mState, mComparator); 817 818 // Remove any existing editors and rebuild any visible 819 mContent.removeAllViews(); 820 821 final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( 822 Context.LAYOUT_INFLATER_SERVICE); 823 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 824 int numRawContacts = mState.size(); 825 826 for (int i = 0; i < numRawContacts; i++) { 827 // TODO ensure proper ordering of entities in the list 828 final RawContactDelta rawContactDelta = mState.get(i); 829 if (!rawContactDelta.isVisible()) continue; 830 831 final AccountType type = rawContactDelta.getAccountType(accountTypes); 832 final long rawContactId = rawContactDelta.getRawContactId(); 833 834 final BaseRawContactEditorView editor; 835 if (!type.areContactsWritable()) { 836 editor = (BaseRawContactEditorView) inflater.inflate( 837 R.layout.raw_contact_readonly_editor_view, mContent, false); 838 } else { 839 editor = (RawContactEditorView) inflater.inflate(R.layout.raw_contact_editor_view, 840 mContent, false); 841 } 842 editor.setListener(this); 843 final List<AccountWithDataSet> accounts = AccountTypeManager.getInstance(mContext) 844 .getAccounts(true); 845 if (mHasNewContact && !mNewLocalProfile && accounts.size() > 1) { 846 addAccountSwitcher(mState.get(0), editor); 847 } 848 849 editor.setEnabled(mEnabled); 850 851 if (mExpandedEditors.containsKey(rawContactId)) { 852 editor.setCollapsed(mExpandedEditors.get(rawContactId)); 853 } else { 854 // By default, only the first editor will be expanded. 855 editor.setCollapsed(i != 0); 856 } 857 858 mContent.addView(editor); 859 860 editor.setState(rawContactDelta, type, mViewIdGenerator, isEditingUserProfile()); 861 editor.setCollapsible(numRawContacts > 1); 862 863 // Set up the photo handler. 864 bindPhotoHandler(editor, type, mState); 865 866 // If a new photo was chosen but not yet saved, we need to update the UI to 867 // reflect this. 868 final Uri photoUri = updatedPhotoUriForRawContact(rawContactId); 869 if (photoUri != null) editor.setFullSizedPhoto(photoUri); 870 871 if (editor instanceof RawContactEditorView) { 872 final Activity activity = getActivity(); 873 final RawContactEditorView rawContactEditor = (RawContactEditorView) editor; 874 EditorListener listener = new EditorListener() { 875 876 @Override 877 public void onRequest(int request) { 878 if (activity.isFinishing()) { // Make sure activity is still running. 879 return; 880 } 881 if (request == EditorListener.FIELD_CHANGED && !isEditingUserProfile()) { 882 acquireAggregationSuggestions(activity, rawContactEditor); 883 } else if (request == EditorListener.EDITOR_FOCUS_CHANGED) { 884 adjustNameFieldsHintDarkness(rawContactEditor); 885 } 886 } 887 888 @Override 889 public void onDeleteRequested(Editor removedEditor) { 890 } 891 }; 892 893 final StructuredNameEditorView nameEditor = rawContactEditor.getNameEditor(); 894 if (mRequestFocus) { 895 nameEditor.requestFocus(); 896 mRequestFocus = false; 897 } 898 nameEditor.setEditorListener(listener); 899 if (!TextUtils.isEmpty(mDefaultDisplayName)) { 900 nameEditor.setDisplayName(mDefaultDisplayName); 901 } 902 903 final TextFieldsEditorView phoneticNameEditor = 904 rawContactEditor.getPhoneticNameEditor(); 905 phoneticNameEditor.setEditorListener(listener); 906 rawContactEditor.setAutoAddToDefaultGroup(mAutoAddToDefaultGroup); 907 908 final TextFieldsEditorView nickNameEditor = 909 rawContactEditor.getNickNameEditor(); 910 nickNameEditor.setEditorListener(listener); 911 912 if (rawContactId == mAggregationSuggestionsRawContactId) { 913 acquireAggregationSuggestions(activity, rawContactEditor); 914 } 915 916 adjustNameFieldsHintDarkness(rawContactEditor); 917 } 918 } 919 920 mRequestFocus = false; 921 922 bindGroupMetaData(); 923 924 // Show editor now that we've loaded state 925 mContent.setVisibility(View.VISIBLE); 926 927 // Refresh Action Bar as the visibility of the join command 928 // Activity can be null if we have been detached from the Activity 929 final Activity activity = getActivity(); 930 if (activity != null) activity.invalidateOptionsMenu(); 931 932 updatedExpandedEditorsMap(); 933 } 934 935 /** 936 * Adjust how dark the hint text should be on all the names' text fields. 937 * 938 * @param rawContactEditor editor to update 939 */ 940 private void adjustNameFieldsHintDarkness(RawContactEditorView rawContactEditor) { 941 // Check whether fields contain focus by calling findFocus() instead of hasFocus(). 942 // The hasFocus() value is not necessarily up to date. 943 final boolean nameFieldsAreNotFocused 944 = rawContactEditor.getNameEditor().findFocus() == null 945 && rawContactEditor.getPhoneticNameEditor().findFocus() == null 946 && rawContactEditor.getNickNameEditor().findFocus() == null; 947 rawContactEditor.getNameEditor().setHintColorDark(!nameFieldsAreNotFocused); 948 rawContactEditor.getPhoneticNameEditor().setHintColorDark(!nameFieldsAreNotFocused); 949 rawContactEditor.getNickNameEditor().setHintColorDark(!nameFieldsAreNotFocused); 950 } 951 952 /** 953 * Update the values in {@link #mExpandedEditors}. 954 */ 955 private void updatedExpandedEditorsMap() { 956 for (int i = 0; i < mContent.getChildCount(); i++) { 957 final View childView = mContent.getChildAt(i); 958 if (childView instanceof BaseRawContactEditorView) { 959 BaseRawContactEditorView childEditor = (BaseRawContactEditorView) childView; 960 mExpandedEditors.put(childEditor.getRawContactId(), childEditor.isCollapsed()); 961 } 962 } 963 } 964 965 /** 966 * If we've stashed a temporary file containing a contact's new photo, return its URI. 967 * @param rawContactId identifies the raw-contact whose Bitmap we'll try to return. 968 * @return Uru of photo for specified raw-contact, or null 969 */ 970 private Uri updatedPhotoUriForRawContact(long rawContactId) { 971 return (Uri) mUpdatedPhotos.get(String.valueOf(rawContactId)); 972 } 973 974 private void bindPhotoHandler(BaseRawContactEditorView editor, AccountType type, 975 RawContactDeltaList state) { 976 final int mode; 977 final boolean showIsPrimaryOption; 978 if (type.areContactsWritable()) { 979 if (editor.hasSetPhoto()) { 980 mode = PhotoActionPopup.Modes.WRITE_ABLE_PHOTO; 981 showIsPrimaryOption = hasMoreThanOnePhoto(); 982 } else { 983 mode = PhotoActionPopup.Modes.NO_PHOTO; 984 showIsPrimaryOption = false; 985 } 986 } else if (editor.hasSetPhoto() && hasMoreThanOnePhoto()) { 987 mode = PhotoActionPopup.Modes.READ_ONLY_PHOTO; 988 showIsPrimaryOption = true; 989 } else { 990 // Read-only and either no photo or the only photo ==> no options 991 editor.getPhotoEditor().setEditorListener(null); 992 editor.getPhotoEditor().setShowPrimary(false); 993 return; 994 } 995 final PhotoHandler photoHandler = new PhotoHandler(mContext, editor, mode, state); 996 editor.getPhotoEditor().setEditorListener( 997 (PhotoHandler.PhotoEditorListener) photoHandler.getListener()); 998 editor.getPhotoEditor().setShowPrimary(showIsPrimaryOption); 999 1000 // Note a newly created raw contact gets some random negative ID, so any value is valid 1001 // here. (i.e. don't check against -1 or anything.) 1002 if (mRawContactIdRequestingPhoto == editor.getRawContactId()) { 1003 mCurrentPhotoHandler = photoHandler; 1004 } 1005 } 1006 1007 private void bindGroupMetaData() { 1008 if (mGroupMetaData == null) { 1009 return; 1010 } 1011 1012 int editorCount = mContent.getChildCount(); 1013 for (int i = 0; i < editorCount; i++) { 1014 BaseRawContactEditorView editor = (BaseRawContactEditorView) mContent.getChildAt(i); 1015 editor.setGroupMetaData(mGroupMetaData); 1016 } 1017 } 1018 1019 private void saveDefaultAccountIfNecessary() { 1020 // Verify that this is a newly created contact, that the contact is composed of only 1021 // 1 raw contact, and that the contact is not a user profile. 1022 if (!Intent.ACTION_INSERT.equals(mAction) && mState.size() == 1 && 1023 !isEditingUserProfile()) { 1024 return; 1025 } 1026 1027 // Find the associated account for this contact (retrieve it here because there are 1028 // multiple paths to creating a contact and this ensures we always have the correct 1029 // account). 1030 final RawContactDelta rawContactDelta = mState.get(0); 1031 String name = rawContactDelta.getAccountName(); 1032 String type = rawContactDelta.getAccountType(); 1033 String dataSet = rawContactDelta.getDataSet(); 1034 1035 AccountWithDataSet account = (name == null || type == null) ? null : 1036 new AccountWithDataSet(name, type, dataSet); 1037 mEditorUtils.saveDefaultAndAllAccounts(account); 1038 } 1039 1040 private void addAccountSwitcher( 1041 final RawContactDelta currentState, BaseRawContactEditorView editor) { 1042 final AccountWithDataSet currentAccount = new AccountWithDataSet( 1043 currentState.getAccountName(), 1044 currentState.getAccountType(), 1045 currentState.getDataSet()); 1046 final View accountView = editor.findViewById(R.id.account); 1047 final View anchorView = editor.findViewById(R.id.account_selector_container); 1048 if (accountView == null) { 1049 return; 1050 } 1051 anchorView.setVisibility(View.VISIBLE); 1052 accountView.setOnClickListener(new View.OnClickListener() { 1053 @Override 1054 public void onClick(View v) { 1055 final ListPopupWindow popup = new ListPopupWindow(mContext, null); 1056 final AccountsListAdapter adapter = 1057 new AccountsListAdapter(mContext, 1058 AccountListFilter.ACCOUNTS_CONTACT_WRITABLE, currentAccount); 1059 popup.setWidth(anchorView.getWidth()); 1060 popup.setAnchorView(anchorView); 1061 popup.setAdapter(adapter); 1062 popup.setModal(true); 1063 popup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); 1064 popup.setOnItemClickListener(new AdapterView.OnItemClickListener() { 1065 @Override 1066 public void onItemClick(AdapterView<?> parent, View view, int position, 1067 long id) { 1068 UiClosables.closeQuietly(popup); 1069 AccountWithDataSet newAccount = adapter.getItem(position); 1070 if (!newAccount.equals(currentAccount)) { 1071 rebindEditorsForNewContact(currentState, currentAccount, newAccount); 1072 } 1073 } 1074 }); 1075 popup.show(); 1076 } 1077 }); 1078 } 1079 1080 @Override 1081 public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) { 1082 inflater.inflate(R.menu.edit_contact, menu); 1083 } 1084 1085 @Override 1086 public void onPrepareOptionsMenu(Menu menu) { 1087 // This supports the keyboard shortcut to save changes to a contact but shouldn't be visible 1088 // because the custom action bar contains the "save" button now (not the overflow menu). 1089 // TODO: Find a better way to handle shortcuts, i.e. onKeyDown()? 1090 final MenuItem doneMenu = menu.findItem(R.id.menu_done); 1091 final MenuItem splitMenu = menu.findItem(R.id.menu_split); 1092 final MenuItem joinMenu = menu.findItem(R.id.menu_join); 1093 final MenuItem helpMenu = menu.findItem(R.id.menu_help); 1094 final MenuItem discardMenu = menu.findItem(R.id.menu_discard); 1095 final MenuItem sendToVoiceMailMenu = menu.findItem(R.id.menu_send_to_voicemail); 1096 final MenuItem ringToneMenu = menu.findItem(R.id.menu_set_ringtone); 1097 final MenuItem deleteMenu = menu.findItem(R.id.menu_delete); 1098 deleteMenu.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); 1099 deleteMenu.setIcon(R.drawable.ic_delete_white_24dp); 1100 1101 // Set visibility of menus 1102 doneMenu.setVisible(false); 1103 1104 // Discard menu is only available if at least one raw contact is editable 1105 discardMenu.setVisible(mState != null && 1106 mState.getFirstWritableRawContact(mContext) != null); 1107 1108 // help menu depending on whether this is inserting or editing 1109 if (Intent.ACTION_INSERT.equals(mAction)) { 1110 HelpUtils.prepareHelpMenuItem(mContext, helpMenu, R.string.help_url_people_add); 1111 splitMenu.setVisible(false); 1112 joinMenu.setVisible(false); 1113 deleteMenu.setVisible(false); 1114 } else if (Intent.ACTION_EDIT.equals(mAction)) { 1115 HelpUtils.prepareHelpMenuItem(mContext, helpMenu, R.string.help_url_people_edit); 1116 // Split only if more than one raw profile and not a user profile 1117 splitMenu.setVisible(mState.size() > 1 && !isEditingUserProfile()); 1118 // Cannot join a user profile 1119 joinMenu.setVisible(!isEditingUserProfile()); 1120 deleteMenu.setVisible(!mDisableDeleteMenuOption); 1121 } else { 1122 // something else, so don't show the help menu 1123 helpMenu.setVisible(false); 1124 } 1125 1126 // Hide telephony-related settings (ringtone, send to voicemail) 1127 // if we don't have a telephone or are editing a new contact. 1128 sendToVoiceMailMenu.setChecked(mSendToVoicemailState); 1129 sendToVoiceMailMenu.setVisible(mArePhoneOptionsChangable); 1130 ringToneMenu.setVisible(mArePhoneOptionsChangable); 1131 1132 int size = menu.size(); 1133 for (int i = 0; i < size; i++) { 1134 menu.getItem(i).setEnabled(mEnabled); 1135 } 1136 } 1137 1138 @Override 1139 public boolean onOptionsItemSelected(MenuItem item) { 1140 switch (item.getItemId()) { 1141 case android.R.id.home: 1142 case R.id.menu_done: 1143 return save(SaveMode.CLOSE); 1144 case R.id.menu_discard: 1145 return revert(); 1146 case R.id.menu_delete: 1147 if (mListener != null) mListener.onDeleteRequested(mLookupUri); 1148 return true; 1149 case R.id.menu_split: 1150 return doSplitContactAction(); 1151 case R.id.menu_join: 1152 return doJoinContactAction(); 1153 case R.id.menu_set_ringtone: 1154 doPickRingtone(); 1155 return true; 1156 case R.id.menu_send_to_voicemail: 1157 // Update state and save 1158 mSendToVoicemailState = !mSendToVoicemailState; 1159 item.setChecked(mSendToVoicemailState); 1160 final Intent intent = ContactSaveService.createSetSendToVoicemail( 1161 mContext, mLookupUri, mSendToVoicemailState); 1162 mContext.startService(intent); 1163 return true; 1164 } 1165 1166 return false; 1167 } 1168 1169 private boolean doSplitContactAction() { 1170 if (!hasValidState()) return false; 1171 1172 final SplitContactConfirmationDialogFragment dialog = 1173 new SplitContactConfirmationDialogFragment(); 1174 dialog.setTargetFragment(this, 0); 1175 dialog.show(getFragmentManager(), SplitContactConfirmationDialogFragment.TAG); 1176 return true; 1177 } 1178 1179 private boolean doJoinContactAction() { 1180 if (!hasValidState()) { 1181 return false; 1182 } 1183 1184 // If we just started creating a new contact and haven't added any data, it's too 1185 // early to do a join 1186 if (mState.size() == 1 && mState.get(0).isContactInsert() && !hasPendingChanges()) { 1187 Toast.makeText(mContext, R.string.toast_join_with_empty_contact, 1188 Toast.LENGTH_LONG).show(); 1189 return true; 1190 } 1191 1192 return save(SaveMode.JOIN); 1193 } 1194 1195 /** 1196 * Check if our internal {@link #mState} is valid, usually checked before 1197 * performing user actions. 1198 */ 1199 private boolean hasValidState() { 1200 return mState.size() > 0; 1201 } 1202 1203 /** 1204 * Return true if there are any edits to the current contact which need to 1205 * be saved. 1206 */ 1207 private boolean hasPendingChanges() { 1208 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 1209 return RawContactModifier.hasChanges(mState, accountTypes); 1210 } 1211 1212 /** 1213 * Saves or creates the contact based on the mode, and if successful 1214 * finishes the activity. 1215 */ 1216 public boolean save(int saveMode) { 1217 if (!hasValidState() || mStatus != Status.EDITING) { 1218 return false; 1219 } 1220 1221 // If we are about to close the editor - there is no need to refresh the data 1222 if (saveMode == SaveMode.CLOSE || saveMode == SaveMode.SPLIT) { 1223 getLoaderManager().destroyLoader(LOADER_DATA); 1224 } 1225 1226 mStatus = Status.SAVING; 1227 1228 if (!hasPendingChanges()) { 1229 if (mLookupUri == null && saveMode == SaveMode.RELOAD) { 1230 // We don't have anything to save and there isn't even an existing contact yet. 1231 // Nothing to do, simply go back to editing mode 1232 mStatus = Status.EDITING; 1233 return true; 1234 } 1235 onSaveCompleted(false, saveMode, mLookupUri != null, mLookupUri); 1236 return true; 1237 } 1238 1239 setEnabled(false); 1240 1241 // Store account as default account, only if this is a new contact 1242 saveDefaultAccountIfNecessary(); 1243 1244 // Save contact 1245 Intent intent = ContactSaveService.createSaveContactIntent(mContext, mState, 1246 SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(), 1247 ((Activity)mContext).getClass(), ContactEditorActivity.ACTION_SAVE_COMPLETED, 1248 mUpdatedPhotos); 1249 mContext.startService(intent); 1250 1251 // Don't try to save the same photos twice. 1252 mUpdatedPhotos = new Bundle(); 1253 1254 return true; 1255 } 1256 1257 private void doPickRingtone() { 1258 1259 final Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); 1260 // Allow user to pick 'Default' 1261 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true); 1262 // Show only ringtones 1263 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_RINGTONE); 1264 // Allow the user to pick a silent ringtone 1265 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true); 1266 1267 final Uri ringtoneUri; 1268 if (mCustomRingtone != null) { 1269 ringtoneUri = Uri.parse(mCustomRingtone); 1270 } else { 1271 // Otherwise pick default ringtone Uri so that something is selected. 1272 ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE); 1273 } 1274 1275 // Put checkmark next to the current ringtone for this contact 1276 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, ringtoneUri); 1277 1278 // Launch! 1279 try { 1280 startActivityForResult(intent, REQUEST_CODE_PICK_RINGTONE); 1281 } catch (ActivityNotFoundException ex) { 1282 Toast.makeText(mContext, R.string.missing_app, Toast.LENGTH_SHORT).show(); 1283 } 1284 } 1285 1286 private void handleRingtonePicked(Uri pickedUri) { 1287 if (pickedUri == null || RingtoneManager.isDefault(pickedUri)) { 1288 mCustomRingtone = null; 1289 } else { 1290 mCustomRingtone = pickedUri.toString(); 1291 } 1292 Intent intent = ContactSaveService.createSetRingtone( 1293 mContext, mLookupUri, mCustomRingtone); 1294 mContext.startService(intent); 1295 } 1296 1297 public static class CancelEditDialogFragment extends DialogFragment { 1298 1299 public static void show(ContactEditorFragment fragment) { 1300 CancelEditDialogFragment dialog = new CancelEditDialogFragment(); 1301 dialog.setTargetFragment(fragment, 0); 1302 dialog.show(fragment.getFragmentManager(), "cancelEditor"); 1303 } 1304 1305 @Override 1306 public Dialog onCreateDialog(Bundle savedInstanceState) { 1307 AlertDialog dialog = new AlertDialog.Builder(getActivity()) 1308 .setIconAttribute(android.R.attr.alertDialogIcon) 1309 .setMessage(R.string.cancel_confirmation_dialog_message) 1310 .setPositiveButton(android.R.string.ok, 1311 new DialogInterface.OnClickListener() { 1312 @Override 1313 public void onClick(DialogInterface dialogInterface, int whichButton) { 1314 ((ContactEditorFragment)getTargetFragment()).doRevertAction(); 1315 } 1316 } 1317 ) 1318 .setNegativeButton(android.R.string.cancel, null) 1319 .create(); 1320 return dialog; 1321 } 1322 } 1323 1324 private boolean revert() { 1325 if (mState.isEmpty() || !hasPendingChanges()) { 1326 doRevertAction(); 1327 } else { 1328 CancelEditDialogFragment.show(this); 1329 } 1330 return true; 1331 } 1332 1333 private void doRevertAction() { 1334 // When this Fragment is closed we don't want it to auto-save 1335 mStatus = Status.CLOSING; 1336 if (mListener != null) mListener.onReverted(); 1337 } 1338 1339 public void doSaveAction() { 1340 save(SaveMode.CLOSE); 1341 } 1342 1343 public void onJoinCompleted(Uri uri) { 1344 onSaveCompleted(false, SaveMode.RELOAD, uri != null, uri); 1345 } 1346 1347 public void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded, 1348 Uri contactLookupUri) { 1349 if (hadChanges) { 1350 if (saveSucceeded) { 1351 if (saveMode != SaveMode.JOIN) { 1352 Toast.makeText(mContext, R.string.contactSavedToast, Toast.LENGTH_SHORT).show(); 1353 } 1354 } else { 1355 Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show(); 1356 } 1357 } 1358 switch (saveMode) { 1359 case SaveMode.CLOSE: 1360 case SaveMode.HOME: 1361 final Intent resultIntent; 1362 if (saveSucceeded && contactLookupUri != null) { 1363 final String requestAuthority = 1364 mLookupUri == null ? null : mLookupUri.getAuthority(); 1365 1366 final String legacyAuthority = "contacts"; 1367 final Uri lookupUri; 1368 if (legacyAuthority.equals(requestAuthority)) { 1369 // Build legacy Uri when requested by caller 1370 final long contactId = ContentUris.parseId(Contacts.lookupContact( 1371 mContext.getContentResolver(), contactLookupUri)); 1372 final Uri legacyContentUri = Uri.parse("content://contacts/people"); 1373 final Uri legacyUri = ContentUris.withAppendedId( 1374 legacyContentUri, contactId); 1375 lookupUri = legacyUri; 1376 } else { 1377 // Otherwise pass back a lookup-style Uri 1378 lookupUri = contactLookupUri; 1379 } 1380 resultIntent = QuickContact.composeQuickContactsIntent(getActivity(), 1381 (Rect) null, lookupUri, QuickContactActivity.MODE_FULLY_EXPANDED, null); 1382 // Make sure not to show QuickContacts on top of another QuickContacts. 1383 resultIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 1384 } else { 1385 resultIntent = null; 1386 } 1387 // It is already saved, so prevent that it is saved again 1388 mStatus = Status.CLOSING; 1389 if (mListener != null) mListener.onSaveFinished(resultIntent); 1390 break; 1391 1392 case SaveMode.RELOAD: 1393 case SaveMode.JOIN: 1394 if (saveSucceeded && contactLookupUri != null) { 1395 // If it was a JOIN, we are now ready to bring up the join activity. 1396 if (saveMode == SaveMode.JOIN && hasValidState()) { 1397 showJoinAggregateActivity(contactLookupUri); 1398 } 1399 1400 // If this was in INSERT, we are changing into an EDIT now. 1401 // If it already was an EDIT, we are changing to the new Uri now 1402 mState = new RawContactDeltaList(); 1403 load(Intent.ACTION_EDIT, contactLookupUri, null); 1404 mStatus = Status.LOADING; 1405 getLoaderManager().restartLoader(LOADER_DATA, null, mDataLoaderListener); 1406 } 1407 break; 1408 1409 case SaveMode.SPLIT: 1410 mStatus = Status.CLOSING; 1411 if (mListener != null) { 1412 mListener.onContactSplit(contactLookupUri); 1413 } else { 1414 Log.d(TAG, "No listener registered, can not call onSplitFinished"); 1415 } 1416 break; 1417 } 1418 } 1419 1420 /** 1421 * Shows a list of aggregates that can be joined into the currently viewed aggregate. 1422 * 1423 * @param contactLookupUri the fresh URI for the currently edited contact (after saving it) 1424 */ 1425 private void showJoinAggregateActivity(Uri contactLookupUri) { 1426 if (contactLookupUri == null || !isAdded()) { 1427 return; 1428 } 1429 1430 mContactIdForJoin = ContentUris.parseId(contactLookupUri); 1431 mContactWritableForJoin = isContactWritable(); 1432 final Intent intent = new Intent(UiIntentActions.PICK_JOIN_CONTACT_ACTION); 1433 intent.putExtra(UiIntentActions.TARGET_CONTACT_ID_EXTRA_KEY, mContactIdForJoin); 1434 startActivityForResult(intent, REQUEST_CODE_JOIN); 1435 } 1436 1437 /** 1438 * Performs aggregation with the contact selected by the user from suggestions or A-Z list. 1439 */ 1440 private void joinAggregate(final long contactId) { 1441 Intent intent = ContactSaveService.createJoinContactsIntent(mContext, mContactIdForJoin, 1442 contactId, mContactWritableForJoin, 1443 ContactEditorActivity.class, ContactEditorActivity.ACTION_JOIN_COMPLETED); 1444 mContext.startService(intent); 1445 } 1446 1447 /** 1448 * Returns true if there is at least one writable raw contact in the current contact. 1449 */ 1450 private boolean isContactWritable() { 1451 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 1452 int size = mState.size(); 1453 for (int i = 0; i < size; i++) { 1454 RawContactDelta entity = mState.get(i); 1455 final AccountType type = entity.getAccountType(accountTypes); 1456 if (type.areContactsWritable()) { 1457 return true; 1458 } 1459 } 1460 return false; 1461 } 1462 1463 private boolean isEditingUserProfile() { 1464 return mNewLocalProfile || mIsUserProfile; 1465 } 1466 1467 public static interface Listener { 1468 /** 1469 * Contact was not found, so somehow close this fragment. This is raised after a contact 1470 * is removed via Menu/Delete 1471 */ 1472 void onContactNotFound(); 1473 1474 /** 1475 * Contact was split, so we can close now. 1476 * @param newLookupUri The lookup uri of the new contact that should be shown to the user. 1477 * The editor tries best to chose the most natural contact here. 1478 */ 1479 void onContactSplit(Uri newLookupUri); 1480 1481 /** 1482 * User has tapped Revert, close the fragment now. 1483 */ 1484 void onReverted(); 1485 1486 /** 1487 * Contact was saved and the Fragment can now be closed safely. 1488 */ 1489 void onSaveFinished(Intent resultIntent); 1490 1491 /** 1492 * User switched to editing a different contact (a suggestion from the 1493 * aggregation engine). 1494 */ 1495 void onEditOtherContactRequested( 1496 Uri contactLookupUri, ArrayList<ContentValues> contentValues); 1497 1498 /** 1499 * Contact is being created for an external account that provides its own 1500 * new contact activity. 1501 */ 1502 void onCustomCreateContactActivityRequested(AccountWithDataSet account, 1503 Bundle intentExtras); 1504 1505 /** 1506 * The edited raw contact belongs to an external account that provides 1507 * its own edit activity. 1508 * 1509 * @param redirect indicates that the current editor should be closed 1510 * before the custom editor is shown. 1511 */ 1512 void onCustomEditContactActivityRequested(AccountWithDataSet account, Uri rawContactUri, 1513 Bundle intentExtras, boolean redirect); 1514 1515 void onDeleteRequested(Uri contactUri); 1516 } 1517 1518 private class EntityDeltaComparator implements Comparator<RawContactDelta> { 1519 /** 1520 * Compare EntityDeltas for sorting the stack of editors. 1521 */ 1522 @Override 1523 public int compare(RawContactDelta one, RawContactDelta two) { 1524 // Check direct equality 1525 if (one.equals(two)) { 1526 return 0; 1527 } 1528 1529 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 1530 String accountType1 = one.getValues().getAsString(RawContacts.ACCOUNT_TYPE); 1531 String dataSet1 = one.getValues().getAsString(RawContacts.DATA_SET); 1532 final AccountType type1 = accountTypes.getAccountType(accountType1, dataSet1); 1533 String accountType2 = two.getValues().getAsString(RawContacts.ACCOUNT_TYPE); 1534 String dataSet2 = two.getValues().getAsString(RawContacts.DATA_SET); 1535 final AccountType type2 = accountTypes.getAccountType(accountType2, dataSet2); 1536 1537 // Check read-only. Sort read/write before read-only. 1538 if (!type1.areContactsWritable() && type2.areContactsWritable()) { 1539 return 1; 1540 } else if (type1.areContactsWritable() && !type2.areContactsWritable()) { 1541 return -1; 1542 } 1543 1544 // Check account type. Sort Google before non-Google. 1545 boolean skipAccountTypeCheck = false; 1546 boolean isGoogleAccount1 = type1 instanceof GoogleAccountType; 1547 boolean isGoogleAccount2 = type2 instanceof GoogleAccountType; 1548 if (isGoogleAccount1 && !isGoogleAccount2) { 1549 return -1; 1550 } else if (!isGoogleAccount1 && isGoogleAccount2) { 1551 return 1; 1552 } else if (isGoogleAccount1 && isGoogleAccount2){ 1553 skipAccountTypeCheck = true; 1554 } 1555 1556 int value; 1557 if (!skipAccountTypeCheck) { 1558 // Sort accounts with type before accounts without types. 1559 if (type1.accountType != null && type2.accountType == null) { 1560 return -1; 1561 } else if (type1.accountType == null && type2.accountType != null) { 1562 return 1; 1563 } 1564 1565 if (type1.accountType != null && type2.accountType != null) { 1566 value = type1.accountType.compareTo(type2.accountType); 1567 if (value != 0) { 1568 return value; 1569 } 1570 } 1571 1572 // Fall back to data set. Sort accounts with data sets before 1573 // those without. 1574 if (type1.dataSet != null && type2.dataSet == null) { 1575 return -1; 1576 } else if (type1.dataSet == null && type2.dataSet != null) { 1577 return 1; 1578 } 1579 1580 if (type1.dataSet != null && type2.dataSet != null) { 1581 value = type1.dataSet.compareTo(type2.dataSet); 1582 if (value != 0) { 1583 return value; 1584 } 1585 } 1586 } 1587 1588 // Check account name 1589 String oneAccount = one.getAccountName(); 1590 if (oneAccount == null) oneAccount = ""; 1591 String twoAccount = two.getAccountName(); 1592 if (twoAccount == null) twoAccount = ""; 1593 value = oneAccount.compareTo(twoAccount); 1594 if (value != 0) { 1595 return value; 1596 } 1597 1598 // Both are in the same account, fall back to contact ID 1599 Long oneId = one.getRawContactId(); 1600 Long twoId = two.getRawContactId(); 1601 if (oneId == null) { 1602 return -1; 1603 } else if (twoId == null) { 1604 return 1; 1605 } 1606 1607 return (int)(oneId - twoId); 1608 } 1609 } 1610 1611 /** 1612 * Returns the contact ID for the currently edited contact or 0 if the contact is new. 1613 */ 1614 protected long getContactId() { 1615 for (RawContactDelta rawContact : mState) { 1616 Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID); 1617 if (contactId != null) { 1618 return contactId; 1619 } 1620 } 1621 return 0; 1622 } 1623 1624 /** 1625 * Triggers an asynchronous search for aggregation suggestions. 1626 */ 1627 private void acquireAggregationSuggestions(Context context, 1628 RawContactEditorView rawContactEditor) { 1629 long rawContactId = rawContactEditor.getRawContactId(); 1630 if (mAggregationSuggestionsRawContactId != rawContactId 1631 && mAggregationSuggestionView != null) { 1632 mAggregationSuggestionView.setVisibility(View.GONE); 1633 mAggregationSuggestionView = null; 1634 mAggregationSuggestionEngine.reset(); 1635 } 1636 1637 mAggregationSuggestionsRawContactId = rawContactId; 1638 1639 if (mAggregationSuggestionEngine == null) { 1640 mAggregationSuggestionEngine = new AggregationSuggestionEngine(context); 1641 mAggregationSuggestionEngine.setListener(this); 1642 mAggregationSuggestionEngine.start(); 1643 } 1644 1645 mAggregationSuggestionEngine.setContactId(getContactId()); 1646 1647 LabeledEditorView nameEditor = rawContactEditor.getNameEditor(); 1648 mAggregationSuggestionEngine.onNameChange(nameEditor.getValues()); 1649 } 1650 1651 @Override 1652 public void onAggregationSuggestionChange() { 1653 Activity activity = getActivity(); 1654 if ((activity != null && activity.isFinishing()) 1655 || !isVisible() || mState.isEmpty() || mStatus != Status.EDITING) { 1656 return; 1657 } 1658 1659 UiClosables.closeQuietly(mAggregationSuggestionPopup); 1660 1661 if (mAggregationSuggestionEngine.getSuggestedContactCount() == 0) { 1662 return; 1663 } 1664 1665 final RawContactEditorView rawContactView = 1666 (RawContactEditorView)getRawContactEditorView(mAggregationSuggestionsRawContactId); 1667 if (rawContactView == null) { 1668 return; // Raw contact deleted? 1669 } 1670 final View anchorView = rawContactView.findViewById(R.id.anchor_view); 1671 mAggregationSuggestionPopup = new ListPopupWindow(mContext, null); 1672 mAggregationSuggestionPopup.setAnchorView(anchorView); 1673 mAggregationSuggestionPopup.setWidth(anchorView.getWidth()); 1674 mAggregationSuggestionPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); 1675 mAggregationSuggestionPopup.setAdapter( 1676 new AggregationSuggestionAdapter(getActivity(), 1677 mState.size() == 1 && mState.get(0).isContactInsert(), 1678 this, mAggregationSuggestionEngine.getSuggestions())); 1679 mAggregationSuggestionPopup.setOnItemClickListener(mAggregationSuggestionItemClickListener); 1680 mAggregationSuggestionPopup.show(); 1681 } 1682 1683 @Override 1684 public void onJoinAction(long contactId, List<Long> rawContactIdList) { 1685 long rawContactIds[] = new long[rawContactIdList.size()]; 1686 for (int i = 0; i < rawContactIds.length; i++) { 1687 rawContactIds[i] = rawContactIdList.get(i); 1688 } 1689 JoinSuggestedContactDialogFragment dialog = 1690 new JoinSuggestedContactDialogFragment(); 1691 Bundle args = new Bundle(); 1692 args.putLongArray("rawContactIds", rawContactIds); 1693 dialog.setArguments(args); 1694 dialog.setTargetFragment(this, 0); 1695 try { 1696 dialog.show(getFragmentManager(), "join"); 1697 } catch (Exception ex) { 1698 // No problem - the activity is no longer available to display the dialog 1699 } 1700 } 1701 1702 public static class JoinSuggestedContactDialogFragment extends DialogFragment { 1703 1704 @Override 1705 public Dialog onCreateDialog(Bundle savedInstanceState) { 1706 return new AlertDialog.Builder(getActivity()) 1707 .setIconAttribute(android.R.attr.alertDialogIcon) 1708 .setMessage(R.string.aggregation_suggestion_join_dialog_message) 1709 .setPositiveButton(android.R.string.yes, 1710 new DialogInterface.OnClickListener() { 1711 @Override 1712 public void onClick(DialogInterface dialog, int whichButton) { 1713 ContactEditorFragment targetFragment = 1714 (ContactEditorFragment) getTargetFragment(); 1715 long rawContactIds[] = 1716 getArguments().getLongArray("rawContactIds"); 1717 targetFragment.doJoinSuggestedContact(rawContactIds); 1718 } 1719 } 1720 ) 1721 .setNegativeButton(android.R.string.no, null) 1722 .create(); 1723 } 1724 } 1725 1726 /** 1727 * Joins the suggested contact (specified by the id's of constituent raw 1728 * contacts), save all changes, and stay in the editor. 1729 */ 1730 protected void doJoinSuggestedContact(long[] rawContactIds) { 1731 if (!hasValidState() || mStatus != Status.EDITING) { 1732 return; 1733 } 1734 1735 mState.setJoinWithRawContacts(rawContactIds); 1736 save(SaveMode.RELOAD); 1737 } 1738 1739 @Override 1740 public void onEditAction(Uri contactLookupUri) { 1741 SuggestionEditConfirmationDialogFragment dialog = 1742 new SuggestionEditConfirmationDialogFragment(); 1743 Bundle args = new Bundle(); 1744 args.putParcelable("contactUri", contactLookupUri); 1745 dialog.setArguments(args); 1746 dialog.setTargetFragment(this, 0); 1747 dialog.show(getFragmentManager(), "edit"); 1748 } 1749 1750 public static class SuggestionEditConfirmationDialogFragment extends DialogFragment { 1751 1752 @Override 1753 public Dialog onCreateDialog(Bundle savedInstanceState) { 1754 return new AlertDialog.Builder(getActivity()) 1755 .setIconAttribute(android.R.attr.alertDialogIcon) 1756 .setMessage(R.string.aggregation_suggestion_edit_dialog_message) 1757 .setPositiveButton(android.R.string.yes, 1758 new DialogInterface.OnClickListener() { 1759 @Override 1760 public void onClick(DialogInterface dialog, int whichButton) { 1761 ContactEditorFragment targetFragment = 1762 (ContactEditorFragment) getTargetFragment(); 1763 Uri contactUri = 1764 getArguments().getParcelable("contactUri"); 1765 targetFragment.doEditSuggestedContact(contactUri); 1766 } 1767 } 1768 ) 1769 .setNegativeButton(android.R.string.no, null) 1770 .create(); 1771 } 1772 } 1773 1774 /** 1775 * Abandons the currently edited contact and switches to editing the suggested 1776 * one, transferring all the data there 1777 */ 1778 protected void doEditSuggestedContact(Uri contactUri) { 1779 if (mListener != null) { 1780 // make sure we don't save this contact when closing down 1781 mStatus = Status.CLOSING; 1782 mListener.onEditOtherContactRequested( 1783 contactUri, mState.get(0).getContentValues()); 1784 } 1785 } 1786 1787 public void setAggregationSuggestionViewEnabled(boolean enabled) { 1788 if (mAggregationSuggestionView == null) { 1789 return; 1790 } 1791 1792 LinearLayout itemList = (LinearLayout) mAggregationSuggestionView.findViewById( 1793 R.id.aggregation_suggestions); 1794 int count = itemList.getChildCount(); 1795 for (int i = 0; i < count; i++) { 1796 itemList.getChildAt(i).setEnabled(enabled); 1797 } 1798 } 1799 1800 @Override 1801 public void onSaveInstanceState(Bundle outState) { 1802 outState.putParcelable(KEY_URI, mLookupUri); 1803 outState.putString(KEY_ACTION, mAction); 1804 1805 if (hasValidState()) { 1806 // Store entities with modifications 1807 outState.putParcelable(KEY_EDIT_STATE, mState); 1808 } 1809 outState.putLong(KEY_RAW_CONTACT_ID_REQUESTING_PHOTO, mRawContactIdRequestingPhoto); 1810 outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator); 1811 outState.putParcelable(KEY_CURRENT_PHOTO_URI, mCurrentPhotoUri); 1812 outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin); 1813 outState.putBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN, mContactWritableForJoin); 1814 outState.putLong(KEY_SHOW_JOIN_SUGGESTIONS, mAggregationSuggestionsRawContactId); 1815 outState.putBoolean(KEY_ENABLED, mEnabled); 1816 outState.putBoolean(KEY_NEW_LOCAL_PROFILE, mNewLocalProfile); 1817 outState.putBoolean(KEY_DISABLE_DELETE_MENU_OPTION, mDisableDeleteMenuOption); 1818 outState.putBoolean(KEY_IS_USER_PROFILE, mIsUserProfile); 1819 outState.putInt(KEY_STATUS, mStatus); 1820 outState.putParcelable(KEY_UPDATED_PHOTOS, mUpdatedPhotos); 1821 outState.putBoolean(KEY_HAS_NEW_CONTACT, mHasNewContact); 1822 outState.putBoolean(KEY_IS_EDIT, mIsEdit); 1823 outState.putBoolean(KEY_NEW_CONTACT_READY, mNewContactDataReady); 1824 outState.putBoolean(KEY_EXISTING_CONTACT_READY, mExistingContactDataReady); 1825 outState.putParcelableArrayList(KEY_RAW_CONTACTS, 1826 mRawContacts == null ? 1827 Lists.<RawContact>newArrayList() : Lists.newArrayList(mRawContacts)); 1828 outState.putBoolean(KEY_SEND_TO_VOICE_MAIL_STATE, mSendToVoicemailState); 1829 outState.putString(KEY_CUSTOM_RINGTONE, mCustomRingtone); 1830 outState.putBoolean(KEY_ARE_PHONE_OPTIONS_CHANGEABLE, mArePhoneOptionsChangable); 1831 outState.putSerializable(KEY_EXPANDED_EDITORS, mExpandedEditors); 1832 1833 super.onSaveInstanceState(outState); 1834 } 1835 1836 @Override 1837 public void onActivityResult(int requestCode, int resultCode, Intent data) { 1838 if (mStatus == Status.SUB_ACTIVITY) { 1839 mStatus = Status.EDITING; 1840 } 1841 1842 // See if the photo selection handler handles this result. 1843 if (mCurrentPhotoHandler != null && mCurrentPhotoHandler.handlePhotoActivityResult( 1844 requestCode, resultCode, data)) { 1845 return; 1846 } 1847 1848 switch (requestCode) { 1849 case REQUEST_CODE_JOIN: { 1850 // Ignore failed requests 1851 if (resultCode != Activity.RESULT_OK) return; 1852 if (data != null) { 1853 final long contactId = ContentUris.parseId(data.getData()); 1854 joinAggregate(contactId); 1855 } 1856 break; 1857 } 1858 case REQUEST_CODE_ACCOUNTS_CHANGED: { 1859 // Bail if the account selector was not successful. 1860 if (resultCode != Activity.RESULT_OK) { 1861 mListener.onReverted(); 1862 return; 1863 } 1864 // If there's an account specified, use it. 1865 if (data != null) { 1866 AccountWithDataSet account = data.getParcelableExtra( 1867 Intents.Insert.EXTRA_ACCOUNT); 1868 if (account != null) { 1869 createContact(account); 1870 return; 1871 } 1872 } 1873 // If there isn't an account specified, then this is likely a phone-local 1874 // contact, so we should continue setting up the editor by automatically selecting 1875 // the most appropriate account. 1876 createContact(); 1877 break; 1878 } 1879 case REQUEST_CODE_PICK_RINGTONE: { 1880 if (data != null) { 1881 final Uri pickedUri = data.getParcelableExtra( 1882 RingtoneManager.EXTRA_RINGTONE_PICKED_URI); 1883 handleRingtonePicked(pickedUri); 1884 } 1885 break; 1886 } 1887 } 1888 } 1889 1890 /** 1891 * Sets the photo stored in mPhoto and writes it to the RawContact with the given id 1892 */ 1893 private void setPhoto(long rawContact, Bitmap photo, Uri photoUri) { 1894 BaseRawContactEditorView requestingEditor = getRawContactEditorView(rawContact); 1895 1896 if (photo == null || photo.getHeight() < 0 || photo.getWidth() < 0) { 1897 // This is unexpected. 1898 Log.w(TAG, "Invalid bitmap passed to setPhoto()"); 1899 } 1900 1901 if (requestingEditor != null) { 1902 requestingEditor.setPhotoEntry(photo); 1903 // Immediately set all other photos as non-primary. Otherwise the UI can display 1904 // multiple photos as "Primary photo". 1905 for (int i = 0; i < mContent.getChildCount(); i++) { 1906 final View childView = mContent.getChildAt(i); 1907 if (childView instanceof BaseRawContactEditorView 1908 && childView != requestingEditor) { 1909 final BaseRawContactEditorView rawContactEditor 1910 = (BaseRawContactEditorView) childView; 1911 rawContactEditor.getPhotoEditor().setSuperPrimary(false); 1912 } 1913 } 1914 } else { 1915 Log.w(TAG, "The contact that requested the photo is no longer present."); 1916 } 1917 1918 mUpdatedPhotos.putParcelable(String.valueOf(rawContact), photoUri); 1919 } 1920 1921 /** 1922 * Finds raw contact editor view for the given rawContactId. 1923 */ 1924 public BaseRawContactEditorView getRawContactEditorView(long rawContactId) { 1925 for (int i = 0; i < mContent.getChildCount(); i++) { 1926 final View childView = mContent.getChildAt(i); 1927 if (childView instanceof BaseRawContactEditorView) { 1928 final BaseRawContactEditorView editor = (BaseRawContactEditorView) childView; 1929 if (editor.getRawContactId() == rawContactId) { 1930 return editor; 1931 } 1932 } 1933 } 1934 return null; 1935 } 1936 1937 /** 1938 * Returns true if there is currently more than one photo on screen. 1939 */ 1940 private boolean hasMoreThanOnePhoto() { 1941 int countWithPicture = 0; 1942 final int numEntities = mState.size(); 1943 for (int i = 0; i < numEntities; i++) { 1944 final RawContactDelta entity = mState.get(i); 1945 if (entity.isVisible()) { 1946 final ValuesDelta primary = entity.getPrimaryEntry(Photo.CONTENT_ITEM_TYPE); 1947 if (primary != null && primary.getPhoto() != null) { 1948 countWithPicture++; 1949 } else { 1950 final long rawContactId = entity.getRawContactId(); 1951 final Uri uri = mUpdatedPhotos.getParcelable(String.valueOf(rawContactId)); 1952 if (uri != null) { 1953 try { 1954 mContext.getContentResolver().openInputStream(uri); 1955 countWithPicture++; 1956 } catch (FileNotFoundException e) { 1957 } 1958 } 1959 } 1960 1961 if (countWithPicture > 1) { 1962 return true; 1963 } 1964 } 1965 } 1966 return false; 1967 } 1968 1969 /** 1970 * The listener for the data loader 1971 */ 1972 private final LoaderManager.LoaderCallbacks<Contact> mDataLoaderListener = 1973 new LoaderCallbacks<Contact>() { 1974 @Override 1975 public Loader<Contact> onCreateLoader(int id, Bundle args) { 1976 mLoaderStartTime = SystemClock.elapsedRealtime(); 1977 return new ContactLoader(mContext, mLookupUri, true); 1978 } 1979 1980 @Override 1981 public void onLoadFinished(Loader<Contact> loader, Contact data) { 1982 final long loaderCurrentTime = SystemClock.elapsedRealtime(); 1983 Log.v(TAG, "Time needed for loading: " + (loaderCurrentTime-mLoaderStartTime)); 1984 if (!data.isLoaded()) { 1985 // Item has been deleted. Close activity without saving again. 1986 Log.i(TAG, "No contact found. Closing activity"); 1987 mStatus = Status.CLOSING; 1988 if (mListener != null) mListener.onContactNotFound(); 1989 return; 1990 } 1991 1992 mStatus = Status.EDITING; 1993 mLookupUri = data.getLookupUri(); 1994 final long setDataStartTime = SystemClock.elapsedRealtime(); 1995 setData(data); 1996 final long setDataEndTime = SystemClock.elapsedRealtime(); 1997 1998 Log.v(TAG, "Time needed for setting UI: " + (setDataEndTime-setDataStartTime)); 1999 } 2000 2001 @Override 2002 public void onLoaderReset(Loader<Contact> loader) { 2003 } 2004 }; 2005 2006 /** 2007 * The listener for the group meta data loader for all groups. 2008 */ 2009 private final LoaderManager.LoaderCallbacks<Cursor> mGroupLoaderListener = 2010 new LoaderCallbacks<Cursor>() { 2011 2012 @Override 2013 public CursorLoader onCreateLoader(int id, Bundle args) { 2014 return new GroupMetaDataLoader(mContext, Groups.CONTENT_URI); 2015 } 2016 2017 @Override 2018 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 2019 mGroupMetaData = data; 2020 bindGroupMetaData(); 2021 } 2022 2023 @Override 2024 public void onLoaderReset(Loader<Cursor> loader) { 2025 } 2026 }; 2027 2028 @Override 2029 public void onSplitContactConfirmed() { 2030 if (mState.isEmpty()) { 2031 // This may happen when this Fragment is recreated by the system during users 2032 // confirming the split action (and thus this method is called just before onCreate()), 2033 // for example. 2034 Log.e(TAG, "mState became null during the user's confirming split action. " + 2035 "Cannot perform the save action."); 2036 return; 2037 } 2038 2039 mState.markRawContactsForSplitting(); 2040 save(SaveMode.SPLIT); 2041 } 2042 2043 /** 2044 * Custom photo handler for the editor. The inner listener that this creates also has a 2045 * reference to the editor and acts as an {@link EditorListener}, and uses that editor to hold 2046 * state information in several of the listener methods. 2047 */ 2048 private final class PhotoHandler extends PhotoSelectionHandler { 2049 2050 final long mRawContactId; 2051 private final BaseRawContactEditorView mEditor; 2052 private final PhotoActionListener mPhotoEditorListener; 2053 2054 public PhotoHandler(Context context, BaseRawContactEditorView editor, int photoMode, 2055 RawContactDeltaList state) { 2056 super(context, editor.getPhotoEditor().getChangeAnchorView(), photoMode, false, state); 2057 mEditor = editor; 2058 mRawContactId = editor.getRawContactId(); 2059 mPhotoEditorListener = new PhotoEditorListener(); 2060 } 2061 2062 @Override 2063 public PhotoActionListener getListener() { 2064 return mPhotoEditorListener; 2065 } 2066 2067 @Override 2068 public void startPhotoActivity(Intent intent, int requestCode, Uri photoUri) { 2069 mRawContactIdRequestingPhoto = mEditor.getRawContactId(); 2070 mCurrentPhotoHandler = this; 2071 mStatus = Status.SUB_ACTIVITY; 2072 mCurrentPhotoUri = photoUri; 2073 ContactEditorFragment.this.startActivityForResult(intent, requestCode); 2074 } 2075 2076 private final class PhotoEditorListener extends PhotoSelectionHandler.PhotoActionListener 2077 implements EditorListener { 2078 2079 @Override 2080 public void onRequest(int request) { 2081 if (!hasValidState()) return; 2082 2083 if (request == EditorListener.REQUEST_PICK_PHOTO) { 2084 onClick(mEditor.getPhotoEditor()); 2085 } 2086 if (request == EditorListener.REQUEST_PICK_PRIMARY_PHOTO) { 2087 useAsPrimaryChosen(); 2088 } 2089 } 2090 2091 @Override 2092 public void onDeleteRequested(Editor removedEditor) { 2093 // The picture cannot be deleted, it can only be removed, which is handled by 2094 // onRemovePictureChosen() 2095 } 2096 2097 /** 2098 * User has chosen to set the selected photo as the (super) primary photo 2099 */ 2100 public void useAsPrimaryChosen() { 2101 // Set the IsSuperPrimary for each editor 2102 int count = mContent.getChildCount(); 2103 for (int i = 0; i < count; i++) { 2104 final View childView = mContent.getChildAt(i); 2105 if (childView instanceof BaseRawContactEditorView) { 2106 final BaseRawContactEditorView editor = 2107 (BaseRawContactEditorView) childView; 2108 final PhotoEditorView photoEditor = editor.getPhotoEditor(); 2109 photoEditor.setSuperPrimary(editor == mEditor); 2110 } 2111 } 2112 bindEditors(); 2113 } 2114 2115 /** 2116 * User has chosen to remove a picture 2117 */ 2118 @Override 2119 public void onRemovePictureChosen() { 2120 mEditor.setPhotoEntry(null); 2121 2122 // Prevent bitmap from being restored if rotate the device. 2123 // (only if we first chose a new photo before removing it) 2124 mUpdatedPhotos.remove(String.valueOf(mRawContactId)); 2125 bindEditors(); 2126 } 2127 2128 @Override 2129 public void onPhotoSelected(Uri uri) throws FileNotFoundException { 2130 final Bitmap bitmap = ContactPhotoUtils.getBitmapFromUri(mContext, uri); 2131 setPhoto(mRawContactId, bitmap, uri); 2132 mCurrentPhotoHandler = null; 2133 bindEditors(); 2134 } 2135 2136 @Override 2137 public Uri getCurrentPhotoUri() { 2138 return mCurrentPhotoUri; 2139 } 2140 2141 @Override 2142 public void onPhotoSelectionDismissed() { 2143 // Nothing to do. 2144 } 2145 } 2146 } 2147} 2148