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 17 package com.android.contacts.editor; 18 19 import android.accounts.Account; 20 import android.app.Activity; 21 import android.app.AlertDialog; 22 import android.app.Dialog; 23 import android.app.DialogFragment; 24 import android.app.Fragment; 25 import android.app.LoaderManager; 26 import android.app.LoaderManager.LoaderCallbacks; 27 import android.content.ActivityNotFoundException; 28 import android.content.ContentUris; 29 import android.content.ContentValues; 30 import android.content.Context; 31 import android.content.CursorLoader; 32 import android.content.DialogInterface; 33 import android.content.Intent; 34 import android.content.Loader; 35 import android.database.Cursor; 36 import android.graphics.Bitmap; 37 import android.graphics.Rect; 38 import android.media.RingtoneManager; 39 import android.net.Uri; 40 import android.os.Bundle; 41 import android.os.SystemClock; 42 import android.provider.ContactsContract.CommonDataKinds.Email; 43 import android.provider.ContactsContract.CommonDataKinds.Event; 44 import android.provider.ContactsContract.CommonDataKinds.Organization; 45 import android.provider.ContactsContract.CommonDataKinds.Phone; 46 import android.provider.ContactsContract.CommonDataKinds.Photo; 47 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 48 import android.provider.ContactsContract.Contacts; 49 import android.provider.ContactsContract.Groups; 50 import android.provider.ContactsContract.Intents; 51 import android.provider.ContactsContract.Intents.UI; 52 import android.provider.ContactsContract.QuickContact; 53 import android.provider.ContactsContract.RawContacts; 54 import android.text.TextUtils; 55 import android.util.Log; 56 import android.view.LayoutInflater; 57 import android.view.Menu; 58 import android.view.MenuInflater; 59 import android.view.MenuItem; 60 import android.view.View; 61 import android.view.ViewGroup; 62 import android.widget.AdapterView; 63 import android.widget.AdapterView.OnItemClickListener; 64 import android.widget.BaseAdapter; 65 import android.widget.LinearLayout; 66 import android.widget.ListPopupWindow; 67 import android.widget.Toast; 68 69 import com.android.contacts.ContactSaveService; 70 import com.android.contacts.GroupMetaDataLoader; 71 import com.android.contacts.R; 72 import com.android.contacts.activities.ContactEditorAccountsChangedActivity; 73 import com.android.contacts.activities.ContactEditorActivity; 74 import com.android.contacts.common.model.AccountTypeManager; 75 import com.android.contacts.common.model.ValuesDelta; 76 import com.android.contacts.common.model.account.AccountType; 77 import com.android.contacts.common.model.account.AccountWithDataSet; 78 import com.android.contacts.common.model.account.GoogleAccountType; 79 import com.android.contacts.common.util.AccountsListAdapter; 80 import com.android.contacts.common.util.AccountsListAdapter.AccountListFilter; 81 import com.android.contacts.detail.PhotoSelectionHandler; 82 import com.android.contacts.editor.AggregationSuggestionEngine.Suggestion; 83 import com.android.contacts.editor.Editor.EditorListener; 84 import com.android.contacts.common.model.Contact; 85 import com.android.contacts.common.model.ContactLoader; 86 import com.android.contacts.common.model.RawContact; 87 import com.android.contacts.common.model.RawContactDelta; 88 import com.android.contacts.common.model.RawContactDeltaList; 89 import com.android.contacts.common.model.RawContactModifier; 90 import com.android.contacts.quickcontact.QuickContactActivity; 91 import com.android.contacts.util.ContactPhotoUtils; 92 import com.android.contacts.util.HelpUtils; 93 import com.android.contacts.util.PhoneCapabilityTester; 94 import com.android.contacts.util.UiClosables; 95 import com.google.common.collect.ImmutableList; 96 import com.google.common.collect.Lists; 97 98 import java.io.FileNotFoundException; 99 import java.util.ArrayList; 100 import java.util.Collections; 101 import java.util.Comparator; 102 import java.util.HashMap; 103 import java.util.List; 104 105 public 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 AggregationSuggestionAdapter(Activity activity, boolean setNewContact, AggregationSuggestionView.Listener listener, List<Suggestion> suggestions)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 getView(int position, View convertView, ViewGroup parent)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 getItemId(int position)318 public long getItemId(int position) { 319 return position; 320 } 321 322 @Override getItem(int position)323 public Object getItem(int position) { 324 return mSuggestions.get(position); 325 } 326 327 @Override getCount()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 ContactEditorFragment()352 public ContactEditorFragment() { 353 } 354 setEnabled(boolean enabled)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 onAttach(Activity activity)371 public void onAttach(Activity activity) { 372 super.onAttach(activity); 373 mContext = activity; 374 mEditorUtils = ContactEditorUtils.getInstance(mContext); 375 } 376 377 @Override onStop()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 onDestroy()390 public void onDestroy() { 391 super.onDestroy(); 392 if (mAggregationSuggestionEngine != null) { 393 mAggregationSuggestionEngine.quit(); 394 } 395 } 396 397 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState)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 onActivityCreated(Bundle savedInstanceState)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.ACCOUNT); 438 final String dataSet = mIntentExtras == null ? null : 439 mIntentExtras.getString(Intents.Insert.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 */ validateAction(String action)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 onStart()470 public void onStart() { 471 getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupLoaderListener); 472 super.onStart(); 473 } 474 load(String action, Uri lookupUri, Bundle intentExtras)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 setListener(Listener value)487 public void setListener(Listener value) { 488 mListener = value; 489 } 490 491 @Override onCreate(Bundle savedState)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 setData(Contact contact)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 onExternalEditorRequest(AccountWithDataSet account, Uri uri)590 public void onExternalEditorRequest(AccountWithDataSet account, Uri uri) { 591 mListener.onCustomEditContactActivityRequested(account, uri, null, false); 592 } 593 594 @Override onEditorExpansionChanged()595 public void onEditorExpansionChanged() { 596 updatedExpandedEditorsMap(); 597 } 598 bindEditorsForExistingContact(String displayName, boolean isUserProfile, ImmutableList<RawContact> rawContacts)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 bindMenuItemsForPhone(Contact contact)637 private void bindMenuItemsForPhone(Contact contact) { 638 mSendToVoicemailState = contact.isSendToVoicemail(); 639 mCustomRingtone = contact.getCustomRingtone(); 640 mArePhoneOptionsChangable = arePhoneOptionsChangable(contact); 641 } 642 arePhoneOptionsChangable(Contact contact)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 */ setIntentExtras(Bundle extras)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 selectAccountAndCreateContact()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 if (defaultAccount == null) { 686 createContact(null); 687 } else { 688 createContact(defaultAccount); 689 } 690 } 691 } 692 693 /** 694 * Create a contact by automatically selecting the first account. If there's no available 695 * account, a device-local contact should be created. 696 */ createContact()697 private void createContact() { 698 final List<AccountWithDataSet> accounts = 699 AccountTypeManager.getInstance(mContext).getAccounts(true); 700 // No Accounts available. Create a phone-local contact. 701 if (accounts.isEmpty()) { 702 createContact(null); 703 return; 704 } 705 706 // We have an account switcher in "create-account" screen, so don't need to ask a user to 707 // select an account here. 708 createContact(accounts.get(0)); 709 } 710 711 /** 712 * Shows account creation screen associated with a given account. 713 * 714 * @param account may be null to signal a device-local contact should be created. 715 */ createContact(AccountWithDataSet account)716 private void createContact(AccountWithDataSet account) { 717 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 718 final AccountType accountType = 719 accountTypes.getAccountType(account != null ? account.type : null, 720 account != null ? account.dataSet : null); 721 722 if (accountType.getCreateContactActivityClassName() != null) { 723 if (mListener != null) { 724 mListener.onCustomCreateContactActivityRequested(account, mIntentExtras); 725 } 726 } else { 727 bindEditorsForNewContact(account, accountType); 728 } 729 } 730 731 /** 732 * Removes a current editor ({@link #mState}) and rebinds new editor for a new account. 733 * Some of old data are reused with new restriction enforced by the new account. 734 * 735 * @param oldState Old data being edited. 736 * @param oldAccount Old account associated with oldState. 737 * @param newAccount New account to be used. 738 */ rebindEditorsForNewContact( RawContactDelta oldState, AccountWithDataSet oldAccount, AccountWithDataSet newAccount)739 private void rebindEditorsForNewContact( 740 RawContactDelta oldState, AccountWithDataSet oldAccount, 741 AccountWithDataSet newAccount) { 742 AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 743 AccountType oldAccountType = accountTypes.getAccountType( 744 oldAccount.type, oldAccount.dataSet); 745 AccountType newAccountType = accountTypes.getAccountType( 746 newAccount.type, newAccount.dataSet); 747 748 if (newAccountType.getCreateContactActivityClassName() != null) { 749 Log.w(TAG, "external activity called in rebind situation"); 750 if (mListener != null) { 751 mListener.onCustomCreateContactActivityRequested(newAccount, mIntentExtras); 752 } 753 } else { 754 mExistingContactDataReady = false; 755 mNewContactDataReady = false; 756 mState = new RawContactDeltaList(); 757 bindEditorsForNewContact(newAccount, newAccountType, oldState, oldAccountType); 758 if (mIsEdit) { 759 bindEditorsForExistingContact(mDefaultDisplayName, mIsUserProfile, mRawContacts); 760 } 761 } 762 } 763 bindEditorsForNewContact(AccountWithDataSet account, final AccountType accountType)764 private void bindEditorsForNewContact(AccountWithDataSet account, 765 final AccountType accountType) { 766 bindEditorsForNewContact(account, accountType, null, null); 767 } 768 bindEditorsForNewContact(AccountWithDataSet newAccount, final AccountType newAccountType, RawContactDelta oldState, AccountType oldAccountType)769 private void bindEditorsForNewContact(AccountWithDataSet newAccount, 770 final AccountType newAccountType, RawContactDelta oldState, 771 AccountType oldAccountType) { 772 mStatus = Status.EDITING; 773 774 final RawContact rawContact = new RawContact(); 775 if (newAccount != null) { 776 rawContact.setAccount(newAccount); 777 } else { 778 rawContact.setAccountToLocal(); 779 } 780 781 final ValuesDelta valuesDelta = ValuesDelta.fromAfter(rawContact.getValues()); 782 final RawContactDelta insert = new RawContactDelta(valuesDelta); 783 if (oldState == null) { 784 // Parse any values from incoming intent 785 RawContactModifier.parseExtras(mContext, newAccountType, insert, mIntentExtras); 786 } else { 787 RawContactModifier.migrateStateForNewContact(mContext, oldState, insert, 788 oldAccountType, newAccountType); 789 } 790 791 // Ensure we have some default fields (if the account type does not support a field, 792 // ensureKind will not add it, so it is safe to add e.g. Event) 793 RawContactModifier.ensureKindExists(insert, newAccountType, Phone.CONTENT_ITEM_TYPE); 794 RawContactModifier.ensureKindExists(insert, newAccountType, Email.CONTENT_ITEM_TYPE); 795 RawContactModifier.ensureKindExists(insert, newAccountType, Organization.CONTENT_ITEM_TYPE); 796 RawContactModifier.ensureKindExists(insert, newAccountType, Event.CONTENT_ITEM_TYPE); 797 RawContactModifier.ensureKindExists(insert, newAccountType, 798 StructuredPostal.CONTENT_ITEM_TYPE); 799 800 // Set the correct URI for saving the contact as a profile 801 if (mNewLocalProfile) { 802 insert.setProfileQueryUri(); 803 } 804 805 mState.add(insert); 806 807 mRequestFocus = true; 808 809 mNewContactDataReady = true; 810 bindEditors(); 811 } 812 bindEditors()813 private void bindEditors() { 814 // bindEditors() can only bind views if there is data in mState, so immediately return 815 // if mState is null 816 if (mState.isEmpty()) { 817 return; 818 } 819 820 // Check if delta list is ready. Delta list is populated from existing data and when 821 // editing an read-only contact, it's also populated with newly created data for the 822 // blank form. When the data is not ready, skip. This method will be called multiple times. 823 if ((mIsEdit && !mExistingContactDataReady) || (mHasNewContact && !mNewContactDataReady)) { 824 return; 825 } 826 827 // Sort the editors 828 Collections.sort(mState, mComparator); 829 830 // Remove any existing editors and rebuild any visible 831 mContent.removeAllViews(); 832 833 final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( 834 Context.LAYOUT_INFLATER_SERVICE); 835 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 836 int numRawContacts = mState.size(); 837 838 for (int i = 0; i < numRawContacts; i++) { 839 // TODO ensure proper ordering of entities in the list 840 final RawContactDelta rawContactDelta = mState.get(i); 841 if (!rawContactDelta.isVisible()) continue; 842 843 final AccountType type = rawContactDelta.getAccountType(accountTypes); 844 final long rawContactId = rawContactDelta.getRawContactId(); 845 846 final BaseRawContactEditorView editor; 847 if (!type.areContactsWritable()) { 848 editor = (BaseRawContactEditorView) inflater.inflate( 849 R.layout.raw_contact_readonly_editor_view, mContent, false); 850 } else { 851 editor = (RawContactEditorView) inflater.inflate(R.layout.raw_contact_editor_view, 852 mContent, false); 853 } 854 editor.setListener(this); 855 final List<AccountWithDataSet> accounts = AccountTypeManager.getInstance(mContext) 856 .getAccounts(true); 857 if (mHasNewContact && !mNewLocalProfile && accounts.size() > 1) { 858 addAccountSwitcher(mState.get(0), editor); 859 } 860 861 editor.setEnabled(mEnabled); 862 863 if (mExpandedEditors.containsKey(rawContactId)) { 864 editor.setCollapsed(mExpandedEditors.get(rawContactId)); 865 } else { 866 // By default, only the first editor will be expanded. 867 editor.setCollapsed(i != 0); 868 } 869 870 mContent.addView(editor); 871 872 editor.setState(rawContactDelta, type, mViewIdGenerator, isEditingUserProfile()); 873 editor.setCollapsible(numRawContacts > 1); 874 875 // Set up the photo handler. 876 bindPhotoHandler(editor, type, mState); 877 878 // If a new photo was chosen but not yet saved, we need to update the UI to 879 // reflect this. 880 final Uri photoUri = updatedPhotoUriForRawContact(rawContactId); 881 if (photoUri != null) editor.setFullSizedPhoto(photoUri); 882 883 if (editor instanceof RawContactEditorView) { 884 final Activity activity = getActivity(); 885 final RawContactEditorView rawContactEditor = (RawContactEditorView) editor; 886 EditorListener listener = new EditorListener() { 887 888 @Override 889 public void onRequest(int request) { 890 if (activity.isFinishing()) { // Make sure activity is still running. 891 return; 892 } 893 if (request == EditorListener.FIELD_CHANGED && !isEditingUserProfile()) { 894 acquireAggregationSuggestions(activity, rawContactEditor); 895 } else if (request == EditorListener.EDITOR_FOCUS_CHANGED) { 896 adjustNameFieldsHintDarkness(rawContactEditor); 897 } 898 } 899 900 @Override 901 public void onDeleteRequested(Editor removedEditor) { 902 } 903 }; 904 905 final StructuredNameEditorView nameEditor = rawContactEditor.getNameEditor(); 906 if (mRequestFocus) { 907 nameEditor.requestFocus(); 908 mRequestFocus = false; 909 } 910 nameEditor.setEditorListener(listener); 911 if (!TextUtils.isEmpty(mDefaultDisplayName)) { 912 nameEditor.setDisplayName(mDefaultDisplayName); 913 } 914 915 final TextFieldsEditorView phoneticNameEditor = 916 rawContactEditor.getPhoneticNameEditor(); 917 phoneticNameEditor.setEditorListener(listener); 918 rawContactEditor.setAutoAddToDefaultGroup(mAutoAddToDefaultGroup); 919 920 final TextFieldsEditorView nickNameEditor = 921 rawContactEditor.getNickNameEditor(); 922 nickNameEditor.setEditorListener(listener); 923 924 if (rawContactId == mAggregationSuggestionsRawContactId) { 925 acquireAggregationSuggestions(activity, rawContactEditor); 926 } 927 928 adjustNameFieldsHintDarkness(rawContactEditor); 929 } 930 } 931 932 mRequestFocus = false; 933 934 bindGroupMetaData(); 935 936 // Show editor now that we've loaded state 937 mContent.setVisibility(View.VISIBLE); 938 939 // Refresh Action Bar as the visibility of the join command 940 // Activity can be null if we have been detached from the Activity 941 final Activity activity = getActivity(); 942 if (activity != null) activity.invalidateOptionsMenu(); 943 944 updatedExpandedEditorsMap(); 945 } 946 947 /** 948 * Adjust how dark the hint text should be on all the names' text fields. 949 * 950 * @param rawContactEditor editor to update 951 */ adjustNameFieldsHintDarkness(RawContactEditorView rawContactEditor)952 private void adjustNameFieldsHintDarkness(RawContactEditorView rawContactEditor) { 953 // Check whether fields contain focus by calling findFocus() instead of hasFocus(). 954 // The hasFocus() value is not necessarily up to date. 955 final boolean nameFieldsAreNotFocused 956 = rawContactEditor.getNameEditor().findFocus() == null 957 && rawContactEditor.getPhoneticNameEditor().findFocus() == null 958 && rawContactEditor.getNickNameEditor().findFocus() == null; 959 rawContactEditor.getNameEditor().setHintColorDark(!nameFieldsAreNotFocused); 960 rawContactEditor.getPhoneticNameEditor().setHintColorDark(!nameFieldsAreNotFocused); 961 rawContactEditor.getNickNameEditor().setHintColorDark(!nameFieldsAreNotFocused); 962 } 963 964 /** 965 * Update the values in {@link #mExpandedEditors}. 966 */ updatedExpandedEditorsMap()967 private void updatedExpandedEditorsMap() { 968 for (int i = 0; i < mContent.getChildCount(); i++) { 969 final View childView = mContent.getChildAt(i); 970 if (childView instanceof BaseRawContactEditorView) { 971 BaseRawContactEditorView childEditor = (BaseRawContactEditorView) childView; 972 mExpandedEditors.put(childEditor.getRawContactId(), childEditor.isCollapsed()); 973 } 974 } 975 } 976 977 /** 978 * If we've stashed a temporary file containing a contact's new photo, return its URI. 979 * @param rawContactId identifies the raw-contact whose Bitmap we'll try to return. 980 * @return Uru of photo for specified raw-contact, or null 981 */ updatedPhotoUriForRawContact(long rawContactId)982 private Uri updatedPhotoUriForRawContact(long rawContactId) { 983 return (Uri) mUpdatedPhotos.get(String.valueOf(rawContactId)); 984 } 985 bindPhotoHandler(BaseRawContactEditorView editor, AccountType type, RawContactDeltaList state)986 private void bindPhotoHandler(BaseRawContactEditorView editor, AccountType type, 987 RawContactDeltaList state) { 988 final int mode; 989 final boolean showIsPrimaryOption; 990 if (type.areContactsWritable()) { 991 if (editor.hasSetPhoto()) { 992 mode = PhotoActionPopup.Modes.WRITE_ABLE_PHOTO; 993 showIsPrimaryOption = hasMoreThanOnePhoto(); 994 } else { 995 mode = PhotoActionPopup.Modes.NO_PHOTO; 996 showIsPrimaryOption = false; 997 } 998 } else if (editor.hasSetPhoto() && hasMoreThanOnePhoto()) { 999 mode = PhotoActionPopup.Modes.READ_ONLY_PHOTO; 1000 showIsPrimaryOption = true; 1001 } else { 1002 // Read-only and either no photo or the only photo ==> no options 1003 editor.getPhotoEditor().setEditorListener(null); 1004 editor.getPhotoEditor().setShowPrimary(false); 1005 return; 1006 } 1007 final PhotoHandler photoHandler = new PhotoHandler(mContext, editor, mode, state); 1008 editor.getPhotoEditor().setEditorListener( 1009 (PhotoHandler.PhotoEditorListener) photoHandler.getListener()); 1010 editor.getPhotoEditor().setShowPrimary(showIsPrimaryOption); 1011 1012 // Note a newly created raw contact gets some random negative ID, so any value is valid 1013 // here. (i.e. don't check against -1 or anything.) 1014 if (mRawContactIdRequestingPhoto == editor.getRawContactId()) { 1015 mCurrentPhotoHandler = photoHandler; 1016 } 1017 } 1018 bindGroupMetaData()1019 private void bindGroupMetaData() { 1020 if (mGroupMetaData == null) { 1021 return; 1022 } 1023 1024 int editorCount = mContent.getChildCount(); 1025 for (int i = 0; i < editorCount; i++) { 1026 BaseRawContactEditorView editor = (BaseRawContactEditorView) mContent.getChildAt(i); 1027 editor.setGroupMetaData(mGroupMetaData); 1028 } 1029 } 1030 saveDefaultAccountIfNecessary()1031 private void saveDefaultAccountIfNecessary() { 1032 // Verify that this is a newly created contact, that the contact is composed of only 1033 // 1 raw contact, and that the contact is not a user profile. 1034 if (!Intent.ACTION_INSERT.equals(mAction) && mState.size() == 1 && 1035 !isEditingUserProfile()) { 1036 return; 1037 } 1038 1039 // Find the associated account for this contact (retrieve it here because there are 1040 // multiple paths to creating a contact and this ensures we always have the correct 1041 // account). 1042 final RawContactDelta rawContactDelta = mState.get(0); 1043 String name = rawContactDelta.getAccountName(); 1044 String type = rawContactDelta.getAccountType(); 1045 String dataSet = rawContactDelta.getDataSet(); 1046 1047 AccountWithDataSet account = (name == null || type == null) ? null : 1048 new AccountWithDataSet(name, type, dataSet); 1049 mEditorUtils.saveDefaultAndAllAccounts(account); 1050 } 1051 addAccountSwitcher( final RawContactDelta currentState, BaseRawContactEditorView editor)1052 private void addAccountSwitcher( 1053 final RawContactDelta currentState, BaseRawContactEditorView editor) { 1054 final AccountWithDataSet currentAccount = new AccountWithDataSet( 1055 currentState.getAccountName(), 1056 currentState.getAccountType(), 1057 currentState.getDataSet()); 1058 final View accountView = editor.findViewById(R.id.account); 1059 final View anchorView = editor.findViewById(R.id.account_selector_container); 1060 if (accountView == null) { 1061 return; 1062 } 1063 anchorView.setVisibility(View.VISIBLE); 1064 accountView.setOnClickListener(new View.OnClickListener() { 1065 @Override 1066 public void onClick(View v) { 1067 final ListPopupWindow popup = new ListPopupWindow(mContext, null); 1068 final AccountsListAdapter adapter = 1069 new AccountsListAdapter(mContext, 1070 AccountListFilter.ACCOUNTS_CONTACT_WRITABLE, currentAccount); 1071 popup.setWidth(anchorView.getWidth()); 1072 popup.setAnchorView(anchorView); 1073 popup.setAdapter(adapter); 1074 popup.setModal(true); 1075 popup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); 1076 popup.setOnItemClickListener(new AdapterView.OnItemClickListener() { 1077 @Override 1078 public void onItemClick(AdapterView<?> parent, View view, int position, 1079 long id) { 1080 UiClosables.closeQuietly(popup); 1081 AccountWithDataSet newAccount = adapter.getItem(position); 1082 if (!newAccount.equals(currentAccount)) { 1083 rebindEditorsForNewContact(currentState, currentAccount, newAccount); 1084 } 1085 } 1086 }); 1087 popup.show(); 1088 } 1089 }); 1090 } 1091 1092 @Override onCreateOptionsMenu(Menu menu, final MenuInflater inflater)1093 public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) { 1094 inflater.inflate(R.menu.edit_contact, menu); 1095 } 1096 1097 @Override onPrepareOptionsMenu(Menu menu)1098 public void onPrepareOptionsMenu(Menu menu) { 1099 // This supports the keyboard shortcut to save changes to a contact but shouldn't be visible 1100 // because the custom action bar contains the "save" button now (not the overflow menu). 1101 // TODO: Find a better way to handle shortcuts, i.e. onKeyDown()? 1102 final MenuItem doneMenu = menu.findItem(R.id.menu_done); 1103 final MenuItem splitMenu = menu.findItem(R.id.menu_split); 1104 final MenuItem joinMenu = menu.findItem(R.id.menu_join); 1105 final MenuItem helpMenu = menu.findItem(R.id.menu_help); 1106 final MenuItem discardMenu = menu.findItem(R.id.menu_discard); 1107 final MenuItem sendToVoiceMailMenu = menu.findItem(R.id.menu_send_to_voicemail); 1108 final MenuItem ringToneMenu = menu.findItem(R.id.menu_set_ringtone); 1109 final MenuItem deleteMenu = menu.findItem(R.id.menu_delete); 1110 deleteMenu.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); 1111 deleteMenu.setIcon(R.drawable.ic_delete_white_24dp); 1112 1113 // Set visibility of menus 1114 doneMenu.setVisible(false); 1115 1116 // Discard menu is only available if at least one raw contact is editable 1117 discardMenu.setVisible(mState != null && 1118 mState.getFirstWritableRawContact(mContext) != null); 1119 1120 // help menu depending on whether this is inserting or editing 1121 if (Intent.ACTION_INSERT.equals(mAction)) { 1122 HelpUtils.prepareHelpMenuItem(mContext, helpMenu, R.string.help_url_people_add); 1123 splitMenu.setVisible(false); 1124 joinMenu.setVisible(false); 1125 deleteMenu.setVisible(false); 1126 } else if (Intent.ACTION_EDIT.equals(mAction)) { 1127 HelpUtils.prepareHelpMenuItem(mContext, helpMenu, R.string.help_url_people_edit); 1128 // Split only if more than one raw profile and not a user profile 1129 splitMenu.setVisible(mState.size() > 1 && !isEditingUserProfile()); 1130 // Cannot join a user profile 1131 joinMenu.setVisible(!isEditingUserProfile()); 1132 deleteMenu.setVisible(!mDisableDeleteMenuOption); 1133 } else { 1134 // something else, so don't show the help menu 1135 helpMenu.setVisible(false); 1136 } 1137 1138 // Hide telephony-related settings (ringtone, send to voicemail) 1139 // if we don't have a telephone or are editing a new contact. 1140 sendToVoiceMailMenu.setChecked(mSendToVoicemailState); 1141 sendToVoiceMailMenu.setVisible(mArePhoneOptionsChangable); 1142 ringToneMenu.setVisible(mArePhoneOptionsChangable); 1143 1144 int size = menu.size(); 1145 for (int i = 0; i < size; i++) { 1146 menu.getItem(i).setEnabled(mEnabled); 1147 } 1148 } 1149 1150 @Override onOptionsItemSelected(MenuItem item)1151 public boolean onOptionsItemSelected(MenuItem item) { 1152 switch (item.getItemId()) { 1153 case android.R.id.home: 1154 case R.id.menu_done: 1155 return save(SaveMode.CLOSE); 1156 case R.id.menu_discard: 1157 return revert(); 1158 case R.id.menu_delete: 1159 if (mListener != null) mListener.onDeleteRequested(mLookupUri); 1160 return true; 1161 case R.id.menu_split: 1162 return doSplitContactAction(); 1163 case R.id.menu_join: 1164 return doJoinContactAction(); 1165 case R.id.menu_set_ringtone: 1166 doPickRingtone(); 1167 return true; 1168 case R.id.menu_send_to_voicemail: 1169 // Update state and save 1170 mSendToVoicemailState = !mSendToVoicemailState; 1171 item.setChecked(mSendToVoicemailState); 1172 final Intent intent = ContactSaveService.createSetSendToVoicemail( 1173 mContext, mLookupUri, mSendToVoicemailState); 1174 mContext.startService(intent); 1175 return true; 1176 } 1177 1178 return false; 1179 } 1180 doSplitContactAction()1181 private boolean doSplitContactAction() { 1182 if (!hasValidState()) return false; 1183 1184 final SplitContactConfirmationDialogFragment dialog = 1185 new SplitContactConfirmationDialogFragment(); 1186 dialog.setTargetFragment(this, 0); 1187 dialog.show(getFragmentManager(), SplitContactConfirmationDialogFragment.TAG); 1188 return true; 1189 } 1190 doJoinContactAction()1191 private boolean doJoinContactAction() { 1192 if (!hasValidState()) { 1193 return false; 1194 } 1195 1196 // If we just started creating a new contact and haven't added any data, it's too 1197 // early to do a join 1198 if (mState.size() == 1 && mState.get(0).isContactInsert() && !hasPendingChanges()) { 1199 Toast.makeText(mContext, R.string.toast_join_with_empty_contact, 1200 Toast.LENGTH_LONG).show(); 1201 return true; 1202 } 1203 1204 return save(SaveMode.JOIN); 1205 } 1206 1207 /** 1208 * Check if our internal {@link #mState} is valid, usually checked before 1209 * performing user actions. 1210 */ hasValidState()1211 private boolean hasValidState() { 1212 return mState.size() > 0; 1213 } 1214 1215 /** 1216 * Return true if there are any edits to the current contact which need to 1217 * be saved. 1218 */ hasPendingChanges()1219 private boolean hasPendingChanges() { 1220 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 1221 return RawContactModifier.hasChanges(mState, accountTypes); 1222 } 1223 1224 /** 1225 * Saves or creates the contact based on the mode, and if successful 1226 * finishes the activity. 1227 */ save(int saveMode)1228 public boolean save(int saveMode) { 1229 if (!hasValidState() || mStatus != Status.EDITING) { 1230 return false; 1231 } 1232 1233 // If we are about to close the editor - there is no need to refresh the data 1234 if (saveMode == SaveMode.CLOSE || saveMode == SaveMode.SPLIT) { 1235 getLoaderManager().destroyLoader(LOADER_DATA); 1236 } 1237 1238 mStatus = Status.SAVING; 1239 1240 if (!hasPendingChanges()) { 1241 if (mLookupUri == null && saveMode == SaveMode.RELOAD) { 1242 // We don't have anything to save and there isn't even an existing contact yet. 1243 // Nothing to do, simply go back to editing mode 1244 mStatus = Status.EDITING; 1245 return true; 1246 } 1247 onSaveCompleted(false, saveMode, mLookupUri != null, mLookupUri); 1248 return true; 1249 } 1250 1251 setEnabled(false); 1252 1253 // Store account as default account, only if this is a new contact 1254 saveDefaultAccountIfNecessary(); 1255 1256 // Save contact 1257 Intent intent = ContactSaveService.createSaveContactIntent(mContext, mState, 1258 SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(), 1259 ((Activity)mContext).getClass(), ContactEditorActivity.ACTION_SAVE_COMPLETED, 1260 mUpdatedPhotos); 1261 mContext.startService(intent); 1262 1263 // Don't try to save the same photos twice. 1264 mUpdatedPhotos = new Bundle(); 1265 1266 return true; 1267 } 1268 doPickRingtone()1269 private void doPickRingtone() { 1270 1271 final Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); 1272 // Allow user to pick 'Default' 1273 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true); 1274 // Show only ringtones 1275 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_RINGTONE); 1276 // Allow the user to pick a silent ringtone 1277 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true); 1278 1279 final Uri ringtoneUri; 1280 if (mCustomRingtone != null) { 1281 ringtoneUri = Uri.parse(mCustomRingtone); 1282 } else { 1283 // Otherwise pick default ringtone Uri so that something is selected. 1284 ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE); 1285 } 1286 1287 // Put checkmark next to the current ringtone for this contact 1288 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, ringtoneUri); 1289 1290 // Launch! 1291 try { 1292 startActivityForResult(intent, REQUEST_CODE_PICK_RINGTONE); 1293 } catch (ActivityNotFoundException ex) { 1294 Toast.makeText(mContext, R.string.missing_app, Toast.LENGTH_SHORT).show(); 1295 } 1296 } 1297 handleRingtonePicked(Uri pickedUri)1298 private void handleRingtonePicked(Uri pickedUri) { 1299 if (pickedUri == null || RingtoneManager.isDefault(pickedUri)) { 1300 mCustomRingtone = null; 1301 } else { 1302 mCustomRingtone = pickedUri.toString(); 1303 } 1304 Intent intent = ContactSaveService.createSetRingtone( 1305 mContext, mLookupUri, mCustomRingtone); 1306 mContext.startService(intent); 1307 } 1308 1309 public static class CancelEditDialogFragment extends DialogFragment { 1310 show(ContactEditorFragment fragment)1311 public static void show(ContactEditorFragment fragment) { 1312 CancelEditDialogFragment dialog = new CancelEditDialogFragment(); 1313 dialog.setTargetFragment(fragment, 0); 1314 dialog.show(fragment.getFragmentManager(), "cancelEditor"); 1315 } 1316 1317 @Override onCreateDialog(Bundle savedInstanceState)1318 public Dialog onCreateDialog(Bundle savedInstanceState) { 1319 AlertDialog dialog = new AlertDialog.Builder(getActivity()) 1320 .setIconAttribute(android.R.attr.alertDialogIcon) 1321 .setMessage(R.string.cancel_confirmation_dialog_message) 1322 .setPositiveButton(android.R.string.ok, 1323 new DialogInterface.OnClickListener() { 1324 @Override 1325 public void onClick(DialogInterface dialogInterface, int whichButton) { 1326 ((ContactEditorFragment)getTargetFragment()).doRevertAction(); 1327 } 1328 } 1329 ) 1330 .setNegativeButton(android.R.string.cancel, null) 1331 .create(); 1332 return dialog; 1333 } 1334 } 1335 revert()1336 private boolean revert() { 1337 if (mState.isEmpty() || !hasPendingChanges()) { 1338 doRevertAction(); 1339 } else { 1340 CancelEditDialogFragment.show(this); 1341 } 1342 return true; 1343 } 1344 doRevertAction()1345 private void doRevertAction() { 1346 // When this Fragment is closed we don't want it to auto-save 1347 mStatus = Status.CLOSING; 1348 if (mListener != null) mListener.onReverted(); 1349 } 1350 doSaveAction()1351 public void doSaveAction() { 1352 save(SaveMode.CLOSE); 1353 } 1354 onJoinCompleted(Uri uri)1355 public void onJoinCompleted(Uri uri) { 1356 onSaveCompleted(false, SaveMode.RELOAD, uri != null, uri); 1357 } 1358 onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded, Uri contactLookupUri)1359 public void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded, 1360 Uri contactLookupUri) { 1361 if (hadChanges) { 1362 if (saveSucceeded) { 1363 if (saveMode != SaveMode.JOIN) { 1364 Toast.makeText(mContext, R.string.contactSavedToast, Toast.LENGTH_SHORT).show(); 1365 } 1366 } else { 1367 Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show(); 1368 } 1369 } 1370 switch (saveMode) { 1371 case SaveMode.CLOSE: 1372 case SaveMode.HOME: 1373 final Intent resultIntent; 1374 if (saveSucceeded && contactLookupUri != null) { 1375 final String requestAuthority = 1376 mLookupUri == null ? null : mLookupUri.getAuthority(); 1377 1378 final String legacyAuthority = "contacts"; 1379 final Uri lookupUri; 1380 if (legacyAuthority.equals(requestAuthority)) { 1381 // Build legacy Uri when requested by caller 1382 final long contactId = ContentUris.parseId(Contacts.lookupContact( 1383 mContext.getContentResolver(), contactLookupUri)); 1384 final Uri legacyContentUri = Uri.parse("content://contacts/people"); 1385 final Uri legacyUri = ContentUris.withAppendedId( 1386 legacyContentUri, contactId); 1387 lookupUri = legacyUri; 1388 } else { 1389 // Otherwise pass back a lookup-style Uri 1390 lookupUri = contactLookupUri; 1391 } 1392 resultIntent = QuickContact.composeQuickContactsIntent(getActivity(), 1393 (Rect) null, lookupUri, QuickContactActivity.MODE_FULLY_EXPANDED, null); 1394 // Make sure not to show QuickContacts on top of another QuickContacts. 1395 resultIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 1396 } else { 1397 resultIntent = null; 1398 } 1399 // It is already saved, so prevent that it is saved again 1400 mStatus = Status.CLOSING; 1401 if (mListener != null) mListener.onSaveFinished(resultIntent); 1402 break; 1403 1404 case SaveMode.RELOAD: 1405 case SaveMode.JOIN: 1406 if (saveSucceeded && contactLookupUri != null) { 1407 // If it was a JOIN, we are now ready to bring up the join activity. 1408 if (saveMode == SaveMode.JOIN && hasValidState()) { 1409 showJoinAggregateActivity(contactLookupUri); 1410 } 1411 1412 // If this was in INSERT, we are changing into an EDIT now. 1413 // If it already was an EDIT, we are changing to the new Uri now 1414 mState = new RawContactDeltaList(); 1415 load(Intent.ACTION_EDIT, contactLookupUri, null); 1416 mStatus = Status.LOADING; 1417 getLoaderManager().restartLoader(LOADER_DATA, null, mDataLoaderListener); 1418 } 1419 break; 1420 1421 case SaveMode.SPLIT: 1422 mStatus = Status.CLOSING; 1423 if (mListener != null) { 1424 mListener.onContactSplit(contactLookupUri); 1425 } else { 1426 Log.d(TAG, "No listener registered, can not call onSplitFinished"); 1427 } 1428 break; 1429 } 1430 } 1431 1432 /** 1433 * Shows a list of aggregates that can be joined into the currently viewed aggregate. 1434 * 1435 * @param contactLookupUri the fresh URI for the currently edited contact (after saving it) 1436 */ showJoinAggregateActivity(Uri contactLookupUri)1437 private void showJoinAggregateActivity(Uri contactLookupUri) { 1438 if (contactLookupUri == null || !isAdded()) { 1439 return; 1440 } 1441 1442 mContactIdForJoin = ContentUris.parseId(contactLookupUri); 1443 mContactWritableForJoin = isContactWritable(); 1444 final Intent intent = new Intent(UI.PICK_JOIN_CONTACT_ACTION); 1445 intent.putExtra(UI.TARGET_CONTACT_ID_EXTRA_KEY, mContactIdForJoin); 1446 startActivityForResult(intent, REQUEST_CODE_JOIN); 1447 } 1448 1449 /** 1450 * Performs aggregation with the contact selected by the user from suggestions or A-Z list. 1451 */ joinAggregate(final long contactId)1452 private void joinAggregate(final long contactId) { 1453 Intent intent = ContactSaveService.createJoinContactsIntent(mContext, mContactIdForJoin, 1454 contactId, mContactWritableForJoin, 1455 ContactEditorActivity.class, ContactEditorActivity.ACTION_JOIN_COMPLETED); 1456 mContext.startService(intent); 1457 } 1458 1459 /** 1460 * Returns true if there is at least one writable raw contact in the current contact. 1461 */ isContactWritable()1462 private boolean isContactWritable() { 1463 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 1464 int size = mState.size(); 1465 for (int i = 0; i < size; i++) { 1466 RawContactDelta entity = mState.get(i); 1467 final AccountType type = entity.getAccountType(accountTypes); 1468 if (type.areContactsWritable()) { 1469 return true; 1470 } 1471 } 1472 return false; 1473 } 1474 isEditingUserProfile()1475 private boolean isEditingUserProfile() { 1476 return mNewLocalProfile || mIsUserProfile; 1477 } 1478 1479 public static interface Listener { 1480 /** 1481 * Contact was not found, so somehow close this fragment. This is raised after a contact 1482 * is removed via Menu/Delete 1483 */ onContactNotFound()1484 void onContactNotFound(); 1485 1486 /** 1487 * Contact was split, so we can close now. 1488 * @param newLookupUri The lookup uri of the new contact that should be shown to the user. 1489 * The editor tries best to chose the most natural contact here. 1490 */ onContactSplit(Uri newLookupUri)1491 void onContactSplit(Uri newLookupUri); 1492 1493 /** 1494 * User has tapped Revert, close the fragment now. 1495 */ onReverted()1496 void onReverted(); 1497 1498 /** 1499 * Contact was saved and the Fragment can now be closed safely. 1500 */ onSaveFinished(Intent resultIntent)1501 void onSaveFinished(Intent resultIntent); 1502 1503 /** 1504 * User switched to editing a different contact (a suggestion from the 1505 * aggregation engine). 1506 */ onEditOtherContactRequested( Uri contactLookupUri, ArrayList<ContentValues> contentValues)1507 void onEditOtherContactRequested( 1508 Uri contactLookupUri, ArrayList<ContentValues> contentValues); 1509 1510 /** 1511 * Contact is being created for an external account that provides its own 1512 * new contact activity. 1513 */ onCustomCreateContactActivityRequested(AccountWithDataSet account, Bundle intentExtras)1514 void onCustomCreateContactActivityRequested(AccountWithDataSet account, 1515 Bundle intentExtras); 1516 1517 /** 1518 * The edited raw contact belongs to an external account that provides 1519 * its own edit activity. 1520 * 1521 * @param redirect indicates that the current editor should be closed 1522 * before the custom editor is shown. 1523 */ onCustomEditContactActivityRequested(AccountWithDataSet account, Uri rawContactUri, Bundle intentExtras, boolean redirect)1524 void onCustomEditContactActivityRequested(AccountWithDataSet account, Uri rawContactUri, 1525 Bundle intentExtras, boolean redirect); 1526 onDeleteRequested(Uri contactUri)1527 void onDeleteRequested(Uri contactUri); 1528 } 1529 1530 private class EntityDeltaComparator implements Comparator<RawContactDelta> { 1531 /** 1532 * Compare EntityDeltas for sorting the stack of editors. 1533 */ 1534 @Override compare(RawContactDelta one, RawContactDelta two)1535 public int compare(RawContactDelta one, RawContactDelta two) { 1536 // Check direct equality 1537 if (one.equals(two)) { 1538 return 0; 1539 } 1540 1541 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 1542 String accountType1 = one.getValues().getAsString(RawContacts.ACCOUNT_TYPE); 1543 String dataSet1 = one.getValues().getAsString(RawContacts.DATA_SET); 1544 final AccountType type1 = accountTypes.getAccountType(accountType1, dataSet1); 1545 String accountType2 = two.getValues().getAsString(RawContacts.ACCOUNT_TYPE); 1546 String dataSet2 = two.getValues().getAsString(RawContacts.DATA_SET); 1547 final AccountType type2 = accountTypes.getAccountType(accountType2, dataSet2); 1548 1549 // Check read-only. Sort read/write before read-only. 1550 if (!type1.areContactsWritable() && type2.areContactsWritable()) { 1551 return 1; 1552 } else if (type1.areContactsWritable() && !type2.areContactsWritable()) { 1553 return -1; 1554 } 1555 1556 // Check account type. Sort Google before non-Google. 1557 boolean skipAccountTypeCheck = false; 1558 boolean isGoogleAccount1 = type1 instanceof GoogleAccountType; 1559 boolean isGoogleAccount2 = type2 instanceof GoogleAccountType; 1560 if (isGoogleAccount1 && !isGoogleAccount2) { 1561 return -1; 1562 } else if (!isGoogleAccount1 && isGoogleAccount2) { 1563 return 1; 1564 } else if (isGoogleAccount1 && isGoogleAccount2){ 1565 skipAccountTypeCheck = true; 1566 } 1567 1568 int value; 1569 if (!skipAccountTypeCheck) { 1570 // Sort accounts with type before accounts without types. 1571 if (type1.accountType != null && type2.accountType == null) { 1572 return -1; 1573 } else if (type1.accountType == null && type2.accountType != null) { 1574 return 1; 1575 } 1576 1577 if (type1.accountType != null && type2.accountType != null) { 1578 value = type1.accountType.compareTo(type2.accountType); 1579 if (value != 0) { 1580 return value; 1581 } 1582 } 1583 1584 // Fall back to data set. Sort accounts with data sets before 1585 // those without. 1586 if (type1.dataSet != null && type2.dataSet == null) { 1587 return -1; 1588 } else if (type1.dataSet == null && type2.dataSet != null) { 1589 return 1; 1590 } 1591 1592 if (type1.dataSet != null && type2.dataSet != null) { 1593 value = type1.dataSet.compareTo(type2.dataSet); 1594 if (value != 0) { 1595 return value; 1596 } 1597 } 1598 } 1599 1600 // Check account name 1601 String oneAccount = one.getAccountName(); 1602 if (oneAccount == null) oneAccount = ""; 1603 String twoAccount = two.getAccountName(); 1604 if (twoAccount == null) twoAccount = ""; 1605 value = oneAccount.compareTo(twoAccount); 1606 if (value != 0) { 1607 return value; 1608 } 1609 1610 // Both are in the same account, fall back to contact ID 1611 Long oneId = one.getRawContactId(); 1612 Long twoId = two.getRawContactId(); 1613 if (oneId == null) { 1614 return -1; 1615 } else if (twoId == null) { 1616 return 1; 1617 } 1618 1619 return (int)(oneId - twoId); 1620 } 1621 } 1622 1623 /** 1624 * Returns the contact ID for the currently edited contact or 0 if the contact is new. 1625 */ getContactId()1626 protected long getContactId() { 1627 for (RawContactDelta rawContact : mState) { 1628 Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID); 1629 if (contactId != null) { 1630 return contactId; 1631 } 1632 } 1633 return 0; 1634 } 1635 1636 /** 1637 * Triggers an asynchronous search for aggregation suggestions. 1638 */ acquireAggregationSuggestions(Context context, RawContactEditorView rawContactEditor)1639 private void acquireAggregationSuggestions(Context context, 1640 RawContactEditorView rawContactEditor) { 1641 long rawContactId = rawContactEditor.getRawContactId(); 1642 if (mAggregationSuggestionsRawContactId != rawContactId 1643 && mAggregationSuggestionView != null) { 1644 mAggregationSuggestionView.setVisibility(View.GONE); 1645 mAggregationSuggestionView = null; 1646 mAggregationSuggestionEngine.reset(); 1647 } 1648 1649 mAggregationSuggestionsRawContactId = rawContactId; 1650 1651 if (mAggregationSuggestionEngine == null) { 1652 mAggregationSuggestionEngine = new AggregationSuggestionEngine(context); 1653 mAggregationSuggestionEngine.setListener(this); 1654 mAggregationSuggestionEngine.start(); 1655 } 1656 1657 mAggregationSuggestionEngine.setContactId(getContactId()); 1658 1659 LabeledEditorView nameEditor = rawContactEditor.getNameEditor(); 1660 mAggregationSuggestionEngine.onNameChange(nameEditor.getValues()); 1661 } 1662 1663 @Override onAggregationSuggestionChange()1664 public void onAggregationSuggestionChange() { 1665 Activity activity = getActivity(); 1666 if ((activity != null && activity.isFinishing()) 1667 || !isVisible() || mState.isEmpty() || mStatus != Status.EDITING) { 1668 return; 1669 } 1670 1671 UiClosables.closeQuietly(mAggregationSuggestionPopup); 1672 1673 if (mAggregationSuggestionEngine.getSuggestedContactCount() == 0) { 1674 return; 1675 } 1676 1677 final RawContactEditorView rawContactView = 1678 (RawContactEditorView)getRawContactEditorView(mAggregationSuggestionsRawContactId); 1679 if (rawContactView == null) { 1680 return; // Raw contact deleted? 1681 } 1682 final View anchorView = rawContactView.findViewById(R.id.anchor_view); 1683 mAggregationSuggestionPopup = new ListPopupWindow(mContext, null); 1684 mAggregationSuggestionPopup.setAnchorView(anchorView); 1685 mAggregationSuggestionPopup.setWidth(anchorView.getWidth()); 1686 mAggregationSuggestionPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); 1687 mAggregationSuggestionPopup.setAdapter( 1688 new AggregationSuggestionAdapter(getActivity(), 1689 mState.size() == 1 && mState.get(0).isContactInsert(), 1690 this, mAggregationSuggestionEngine.getSuggestions())); 1691 mAggregationSuggestionPopup.setOnItemClickListener(mAggregationSuggestionItemClickListener); 1692 mAggregationSuggestionPopup.show(); 1693 } 1694 1695 @Override onJoinAction(long contactId, List<Long> rawContactIdList)1696 public void onJoinAction(long contactId, List<Long> rawContactIdList) { 1697 long rawContactIds[] = new long[rawContactIdList.size()]; 1698 for (int i = 0; i < rawContactIds.length; i++) { 1699 rawContactIds[i] = rawContactIdList.get(i); 1700 } 1701 JoinSuggestedContactDialogFragment dialog = 1702 new JoinSuggestedContactDialogFragment(); 1703 Bundle args = new Bundle(); 1704 args.putLongArray("rawContactIds", rawContactIds); 1705 dialog.setArguments(args); 1706 dialog.setTargetFragment(this, 0); 1707 try { 1708 dialog.show(getFragmentManager(), "join"); 1709 } catch (Exception ex) { 1710 // No problem - the activity is no longer available to display the dialog 1711 } 1712 } 1713 1714 public static class JoinSuggestedContactDialogFragment extends DialogFragment { 1715 1716 @Override onCreateDialog(Bundle savedInstanceState)1717 public Dialog onCreateDialog(Bundle savedInstanceState) { 1718 return new AlertDialog.Builder(getActivity()) 1719 .setIconAttribute(android.R.attr.alertDialogIcon) 1720 .setMessage(R.string.aggregation_suggestion_join_dialog_message) 1721 .setPositiveButton(android.R.string.yes, 1722 new DialogInterface.OnClickListener() { 1723 @Override 1724 public void onClick(DialogInterface dialog, int whichButton) { 1725 ContactEditorFragment targetFragment = 1726 (ContactEditorFragment) getTargetFragment(); 1727 long rawContactIds[] = 1728 getArguments().getLongArray("rawContactIds"); 1729 targetFragment.doJoinSuggestedContact(rawContactIds); 1730 } 1731 } 1732 ) 1733 .setNegativeButton(android.R.string.no, null) 1734 .create(); 1735 } 1736 } 1737 1738 /** 1739 * Joins the suggested contact (specified by the id's of constituent raw 1740 * contacts), save all changes, and stay in the editor. 1741 */ 1742 protected void doJoinSuggestedContact(long[] rawContactIds) { 1743 if (!hasValidState() || mStatus != Status.EDITING) { 1744 return; 1745 } 1746 1747 mState.setJoinWithRawContacts(rawContactIds); 1748 save(SaveMode.RELOAD); 1749 } 1750 1751 @Override 1752 public void onEditAction(Uri contactLookupUri) { 1753 SuggestionEditConfirmationDialogFragment dialog = 1754 new SuggestionEditConfirmationDialogFragment(); 1755 Bundle args = new Bundle(); 1756 args.putParcelable("contactUri", contactLookupUri); 1757 dialog.setArguments(args); 1758 dialog.setTargetFragment(this, 0); 1759 dialog.show(getFragmentManager(), "edit"); 1760 } 1761 1762 public static class SuggestionEditConfirmationDialogFragment extends DialogFragment { 1763 1764 @Override 1765 public Dialog onCreateDialog(Bundle savedInstanceState) { 1766 return new AlertDialog.Builder(getActivity()) 1767 .setIconAttribute(android.R.attr.alertDialogIcon) 1768 .setMessage(R.string.aggregation_suggestion_edit_dialog_message) 1769 .setPositiveButton(android.R.string.yes, 1770 new DialogInterface.OnClickListener() { 1771 @Override 1772 public void onClick(DialogInterface dialog, int whichButton) { 1773 ContactEditorFragment targetFragment = 1774 (ContactEditorFragment) getTargetFragment(); 1775 Uri contactUri = 1776 getArguments().getParcelable("contactUri"); 1777 targetFragment.doEditSuggestedContact(contactUri); 1778 } 1779 } 1780 ) 1781 .setNegativeButton(android.R.string.no, null) 1782 .create(); 1783 } 1784 } 1785 1786 /** 1787 * Abandons the currently edited contact and switches to editing the suggested 1788 * one, transferring all the data there 1789 */ 1790 protected void doEditSuggestedContact(Uri contactUri) { 1791 if (mListener != null) { 1792 // make sure we don't save this contact when closing down 1793 mStatus = Status.CLOSING; 1794 mListener.onEditOtherContactRequested( 1795 contactUri, mState.get(0).getContentValues()); 1796 } 1797 } 1798 1799 public void setAggregationSuggestionViewEnabled(boolean enabled) { 1800 if (mAggregationSuggestionView == null) { 1801 return; 1802 } 1803 1804 LinearLayout itemList = (LinearLayout) mAggregationSuggestionView.findViewById( 1805 R.id.aggregation_suggestions); 1806 int count = itemList.getChildCount(); 1807 for (int i = 0; i < count; i++) { 1808 itemList.getChildAt(i).setEnabled(enabled); 1809 } 1810 } 1811 1812 @Override 1813 public void onSaveInstanceState(Bundle outState) { 1814 outState.putParcelable(KEY_URI, mLookupUri); 1815 outState.putString(KEY_ACTION, mAction); 1816 1817 if (hasValidState()) { 1818 // Store entities with modifications 1819 outState.putParcelable(KEY_EDIT_STATE, mState); 1820 } 1821 outState.putLong(KEY_RAW_CONTACT_ID_REQUESTING_PHOTO, mRawContactIdRequestingPhoto); 1822 outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator); 1823 outState.putParcelable(KEY_CURRENT_PHOTO_URI, mCurrentPhotoUri); 1824 outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin); 1825 outState.putBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN, mContactWritableForJoin); 1826 outState.putLong(KEY_SHOW_JOIN_SUGGESTIONS, mAggregationSuggestionsRawContactId); 1827 outState.putBoolean(KEY_ENABLED, mEnabled); 1828 outState.putBoolean(KEY_NEW_LOCAL_PROFILE, mNewLocalProfile); 1829 outState.putBoolean(KEY_DISABLE_DELETE_MENU_OPTION, mDisableDeleteMenuOption); 1830 outState.putBoolean(KEY_IS_USER_PROFILE, mIsUserProfile); 1831 outState.putInt(KEY_STATUS, mStatus); 1832 outState.putParcelable(KEY_UPDATED_PHOTOS, mUpdatedPhotos); 1833 outState.putBoolean(KEY_HAS_NEW_CONTACT, mHasNewContact); 1834 outState.putBoolean(KEY_IS_EDIT, mIsEdit); 1835 outState.putBoolean(KEY_NEW_CONTACT_READY, mNewContactDataReady); 1836 outState.putBoolean(KEY_EXISTING_CONTACT_READY, mExistingContactDataReady); 1837 outState.putParcelableArrayList(KEY_RAW_CONTACTS, 1838 mRawContacts == null ? 1839 Lists.<RawContact>newArrayList() : Lists.newArrayList(mRawContacts)); 1840 outState.putBoolean(KEY_SEND_TO_VOICE_MAIL_STATE, mSendToVoicemailState); 1841 outState.putString(KEY_CUSTOM_RINGTONE, mCustomRingtone); 1842 outState.putBoolean(KEY_ARE_PHONE_OPTIONS_CHANGEABLE, mArePhoneOptionsChangable); 1843 outState.putSerializable(KEY_EXPANDED_EDITORS, mExpandedEditors); 1844 1845 super.onSaveInstanceState(outState); 1846 } 1847 1848 @Override 1849 public void onActivityResult(int requestCode, int resultCode, Intent data) { 1850 if (mStatus == Status.SUB_ACTIVITY) { 1851 mStatus = Status.EDITING; 1852 } 1853 1854 // See if the photo selection handler handles this result. 1855 if (mCurrentPhotoHandler != null && mCurrentPhotoHandler.handlePhotoActivityResult( 1856 requestCode, resultCode, data)) { 1857 return; 1858 } 1859 1860 switch (requestCode) { 1861 case REQUEST_CODE_JOIN: { 1862 // Ignore failed requests 1863 if (resultCode != Activity.RESULT_OK) return; 1864 if (data != null) { 1865 final long contactId = ContentUris.parseId(data.getData()); 1866 joinAggregate(contactId); 1867 } 1868 break; 1869 } 1870 case REQUEST_CODE_ACCOUNTS_CHANGED: { 1871 // Bail if the account selector was not successful. 1872 if (resultCode != Activity.RESULT_OK) { 1873 mListener.onReverted(); 1874 return; 1875 } 1876 // If there's an account specified, use it. 1877 if (data != null) { 1878 AccountWithDataSet account = data.getParcelableExtra(Intents.Insert.ACCOUNT); 1879 if (account != null) { 1880 createContact(account); 1881 return; 1882 } 1883 } 1884 // If there isn't an account specified, then this is likely a phone-local 1885 // contact, so we should continue setting up the editor by automatically selecting 1886 // the most appropriate account. 1887 createContact(); 1888 break; 1889 } 1890 case REQUEST_CODE_PICK_RINGTONE: { 1891 if (data != null) { 1892 final Uri pickedUri = data.getParcelableExtra( 1893 RingtoneManager.EXTRA_RINGTONE_PICKED_URI); 1894 handleRingtonePicked(pickedUri); 1895 } 1896 break; 1897 } 1898 } 1899 } 1900 1901 /** 1902 * Sets the photo stored in mPhoto and writes it to the RawContact with the given id 1903 */ 1904 private void setPhoto(long rawContact, Bitmap photo, Uri photoUri) { 1905 BaseRawContactEditorView requestingEditor = getRawContactEditorView(rawContact); 1906 1907 if (photo == null || photo.getHeight() < 0 || photo.getWidth() < 0) { 1908 // This is unexpected. 1909 Log.w(TAG, "Invalid bitmap passed to setPhoto()"); 1910 } 1911 1912 if (requestingEditor != null) { 1913 requestingEditor.setPhotoEntry(photo); 1914 // Immediately set all other photos as non-primary. Otherwise the UI can display 1915 // multiple photos as "Primary photo". 1916 for (int i = 0; i < mContent.getChildCount(); i++) { 1917 final View childView = mContent.getChildAt(i); 1918 if (childView instanceof BaseRawContactEditorView 1919 && childView != requestingEditor) { 1920 final BaseRawContactEditorView rawContactEditor 1921 = (BaseRawContactEditorView) childView; 1922 rawContactEditor.getPhotoEditor().setSuperPrimary(false); 1923 } 1924 } 1925 } else { 1926 Log.w(TAG, "The contact that requested the photo is no longer present."); 1927 } 1928 1929 mUpdatedPhotos.putParcelable(String.valueOf(rawContact), photoUri); 1930 } 1931 1932 /** 1933 * Finds raw contact editor view for the given rawContactId. 1934 */ 1935 public BaseRawContactEditorView getRawContactEditorView(long rawContactId) { 1936 for (int i = 0; i < mContent.getChildCount(); i++) { 1937 final View childView = mContent.getChildAt(i); 1938 if (childView instanceof BaseRawContactEditorView) { 1939 final BaseRawContactEditorView editor = (BaseRawContactEditorView) childView; 1940 if (editor.getRawContactId() == rawContactId) { 1941 return editor; 1942 } 1943 } 1944 } 1945 return null; 1946 } 1947 1948 /** 1949 * Returns true if there is currently more than one photo on screen. 1950 */ 1951 private boolean hasMoreThanOnePhoto() { 1952 int countWithPicture = 0; 1953 final int numEntities = mState.size(); 1954 for (int i = 0; i < numEntities; i++) { 1955 final RawContactDelta entity = mState.get(i); 1956 if (entity.isVisible()) { 1957 final ValuesDelta primary = entity.getPrimaryEntry(Photo.CONTENT_ITEM_TYPE); 1958 if (primary != null && primary.getPhoto() != null) { 1959 countWithPicture++; 1960 } else { 1961 final long rawContactId = entity.getRawContactId(); 1962 final Uri uri = mUpdatedPhotos.getParcelable(String.valueOf(rawContactId)); 1963 if (uri != null) { 1964 try { 1965 mContext.getContentResolver().openInputStream(uri); 1966 countWithPicture++; 1967 } catch (FileNotFoundException e) { 1968 } 1969 } 1970 } 1971 1972 if (countWithPicture > 1) { 1973 return true; 1974 } 1975 } 1976 } 1977 return false; 1978 } 1979 1980 /** 1981 * The listener for the data loader 1982 */ 1983 private final LoaderManager.LoaderCallbacks<Contact> mDataLoaderListener = 1984 new LoaderCallbacks<Contact>() { 1985 @Override 1986 public Loader<Contact> onCreateLoader(int id, Bundle args) { 1987 mLoaderStartTime = SystemClock.elapsedRealtime(); 1988 return new ContactLoader(mContext, mLookupUri, true); 1989 } 1990 1991 @Override 1992 public void onLoadFinished(Loader<Contact> loader, Contact data) { 1993 final long loaderCurrentTime = SystemClock.elapsedRealtime(); 1994 Log.v(TAG, "Time needed for loading: " + (loaderCurrentTime-mLoaderStartTime)); 1995 if (!data.isLoaded()) { 1996 // Item has been deleted. Close activity without saving again. 1997 Log.i(TAG, "No contact found. Closing activity"); 1998 mStatus = Status.CLOSING; 1999 if (mListener != null) mListener.onContactNotFound(); 2000 return; 2001 } 2002 2003 mStatus = Status.EDITING; 2004 mLookupUri = data.getLookupUri(); 2005 final long setDataStartTime = SystemClock.elapsedRealtime(); 2006 setData(data); 2007 final long setDataEndTime = SystemClock.elapsedRealtime(); 2008 2009 Log.v(TAG, "Time needed for setting UI: " + (setDataEndTime-setDataStartTime)); 2010 } 2011 2012 @Override 2013 public void onLoaderReset(Loader<Contact> loader) { 2014 } 2015 }; 2016 2017 /** 2018 * The listener for the group meta data loader for all groups. 2019 */ 2020 private final LoaderManager.LoaderCallbacks<Cursor> mGroupLoaderListener = 2021 new LoaderCallbacks<Cursor>() { 2022 2023 @Override 2024 public CursorLoader onCreateLoader(int id, Bundle args) { 2025 return new GroupMetaDataLoader(mContext, Groups.CONTENT_URI); 2026 } 2027 2028 @Override 2029 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 2030 mGroupMetaData = data; 2031 bindGroupMetaData(); 2032 } 2033 2034 @Override 2035 public void onLoaderReset(Loader<Cursor> loader) { 2036 } 2037 }; 2038 2039 @Override 2040 public void onSplitContactConfirmed() { 2041 if (mState.isEmpty()) { 2042 // This may happen when this Fragment is recreated by the system during users 2043 // confirming the split action (and thus this method is called just before onCreate()), 2044 // for example. 2045 Log.e(TAG, "mState became null during the user's confirming split action. " + 2046 "Cannot perform the save action."); 2047 return; 2048 } 2049 2050 mState.markRawContactsForSplitting(); 2051 save(SaveMode.SPLIT); 2052 } 2053 2054 /** 2055 * Custom photo handler for the editor. The inner listener that this creates also has a 2056 * reference to the editor and acts as an {@link EditorListener}, and uses that editor to hold 2057 * state information in several of the listener methods. 2058 */ 2059 private final class PhotoHandler extends PhotoSelectionHandler { 2060 2061 final long mRawContactId; 2062 private final BaseRawContactEditorView mEditor; 2063 private final PhotoActionListener mPhotoEditorListener; 2064 2065 public PhotoHandler(Context context, BaseRawContactEditorView editor, int photoMode, 2066 RawContactDeltaList state) { 2067 super(context, editor.getPhotoEditor().getChangeAnchorView(), photoMode, false, state); 2068 mEditor = editor; 2069 mRawContactId = editor.getRawContactId(); 2070 mPhotoEditorListener = new PhotoEditorListener(); 2071 } 2072 2073 @Override 2074 public PhotoActionListener getListener() { 2075 return mPhotoEditorListener; 2076 } 2077 2078 @Override 2079 public void startPhotoActivity(Intent intent, int requestCode, Uri photoUri) { 2080 mRawContactIdRequestingPhoto = mEditor.getRawContactId(); 2081 mCurrentPhotoHandler = this; 2082 mStatus = Status.SUB_ACTIVITY; 2083 mCurrentPhotoUri = photoUri; 2084 ContactEditorFragment.this.startActivityForResult(intent, requestCode); 2085 } 2086 2087 private final class PhotoEditorListener extends PhotoSelectionHandler.PhotoActionListener 2088 implements EditorListener { 2089 2090 @Override 2091 public void onRequest(int request) { 2092 if (!hasValidState()) return; 2093 2094 if (request == EditorListener.REQUEST_PICK_PHOTO) { 2095 onClick(mEditor.getPhotoEditor()); 2096 } 2097 if (request == EditorListener.REQUEST_PICK_PRIMARY_PHOTO) { 2098 useAsPrimaryChosen(); 2099 } 2100 } 2101 2102 @Override 2103 public void onDeleteRequested(Editor removedEditor) { 2104 // The picture cannot be deleted, it can only be removed, which is handled by 2105 // onRemovePictureChosen() 2106 } 2107 2108 /** 2109 * User has chosen to set the selected photo as the (super) primary photo 2110 */ 2111 public void useAsPrimaryChosen() { 2112 // Set the IsSuperPrimary for each editor 2113 int count = mContent.getChildCount(); 2114 for (int i = 0; i < count; i++) { 2115 final View childView = mContent.getChildAt(i); 2116 if (childView instanceof BaseRawContactEditorView) { 2117 final BaseRawContactEditorView editor = 2118 (BaseRawContactEditorView) childView; 2119 final PhotoEditorView photoEditor = editor.getPhotoEditor(); 2120 photoEditor.setSuperPrimary(editor == mEditor); 2121 } 2122 } 2123 bindEditors(); 2124 } 2125 2126 /** 2127 * User has chosen to remove a picture 2128 */ 2129 @Override 2130 public void onRemovePictureChosen() { 2131 mEditor.setPhotoEntry(null); 2132 2133 // Prevent bitmap from being restored if rotate the device. 2134 // (only if we first chose a new photo before removing it) 2135 mUpdatedPhotos.remove(String.valueOf(mRawContactId)); 2136 bindEditors(); 2137 } 2138 2139 @Override 2140 public void onPhotoSelected(Uri uri) throws FileNotFoundException { 2141 final Bitmap bitmap = ContactPhotoUtils.getBitmapFromUri(mContext, uri); 2142 setPhoto(mRawContactId, bitmap, uri); 2143 mCurrentPhotoHandler = null; 2144 bindEditors(); 2145 } 2146 2147 @Override 2148 public Uri getCurrentPhotoUri() { 2149 return mCurrentPhotoUri; 2150 } 2151 2152 @Override 2153 public void onPhotoSelectionDismissed() { 2154 // Nothing to do. 2155 } 2156 } 2157 } 2158 } 2159