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