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