ContactEditorFragment.java revision 5d2e262ab3658118a12fbf6d0edab76484368c24
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.BitmapFactory; 38import android.graphics.Rect; 39import android.media.RingtoneManager; 40import android.net.Uri; 41import android.os.Bundle; 42import android.os.SystemClock; 43import android.provider.ContactsContract.CommonDataKinds.Email; 44import android.provider.ContactsContract.CommonDataKinds.Event; 45import android.provider.ContactsContract.CommonDataKinds.Organization; 46import android.provider.ContactsContract.CommonDataKinds.Phone; 47import android.provider.ContactsContract.CommonDataKinds.Photo; 48import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 49import android.provider.ContactsContract.Contacts; 50import android.provider.ContactsContract.Groups; 51import android.provider.ContactsContract.Intents; 52import android.provider.ContactsContract.Intents.UI; 53import android.provider.ContactsContract.QuickContact; 54import android.provider.ContactsContract.RawContacts; 55import android.text.TextUtils; 56import android.util.Log; 57import android.view.LayoutInflater; 58import android.view.Menu; 59import android.view.MenuInflater; 60import android.view.MenuItem; 61import android.view.View; 62import android.view.ViewGroup; 63import android.widget.AdapterView; 64import android.widget.AdapterView.OnItemClickListener; 65import android.widget.BaseAdapter; 66import android.widget.LinearLayout; 67import android.widget.ListPopupWindow; 68import android.widget.Toast; 69 70import com.android.contacts.ContactSaveService; 71import com.android.contacts.GroupMetaDataLoader; 72import com.android.contacts.R; 73import com.android.contacts.activities.ContactEditorAccountsChangedActivity; 74import com.android.contacts.activities.ContactEditorActivity; 75import com.android.contacts.common.model.AccountTypeManager; 76import com.android.contacts.common.model.ValuesDelta; 77import com.android.contacts.common.model.account.AccountType; 78import com.android.contacts.common.model.account.AccountWithDataSet; 79import com.android.contacts.common.model.account.GoogleAccountType; 80import com.android.contacts.common.util.AccountsListAdapter; 81import com.android.contacts.common.util.AccountsListAdapter.AccountListFilter; 82import com.android.contacts.detail.PhotoSelectionHandler; 83import com.android.contacts.editor.AggregationSuggestionEngine.Suggestion; 84import com.android.contacts.editor.Editor.EditorListener; 85import com.android.contacts.common.model.Contact; 86import com.android.contacts.common.model.ContactLoader; 87import com.android.contacts.common.model.RawContact; 88import com.android.contacts.common.model.RawContactDelta; 89import com.android.contacts.common.model.RawContactDeltaList; 90import com.android.contacts.common.model.RawContactModifier; 91import com.android.contacts.quickcontact.QuickContactActivity; 92import com.android.contacts.util.ContactPhotoUtils; 93import com.android.contacts.util.HelpUtils; 94import com.android.contacts.util.PhoneCapabilityTester; 95import com.android.contacts.util.UiClosables; 96import com.google.common.collect.ImmutableList; 97import com.google.common.collect.Lists; 98 99import java.io.FileNotFoundException; 100import java.util.ArrayList; 101import java.util.Collections; 102import java.util.Comparator; 103import java.util.List; 104 105public class ContactEditorFragment extends Fragment implements 106 SplitContactConfirmationDialogFragment.Listener, 107 AggregationSuggestionEngine.Listener, AggregationSuggestionView.Listener, 108 RawContactReadOnlyEditorView.Listener { 109 110 private static final String TAG = ContactEditorFragment.class.getSimpleName(); 111 112 private static final int LOADER_DATA = 1; 113 private static final int LOADER_GROUPS = 2; 114 115 private static final String KEY_URI = "uri"; 116 private static final String KEY_ACTION = "action"; 117 private static final String KEY_EDIT_STATE = "state"; 118 private static final String KEY_RAW_CONTACT_ID_REQUESTING_PHOTO = "photorequester"; 119 private static final String KEY_VIEW_ID_GENERATOR = "viewidgenerator"; 120 private static final String KEY_CURRENT_PHOTO_URI = "currentphotouri"; 121 private static final String KEY_CONTACT_ID_FOR_JOIN = "contactidforjoin"; 122 private static final String KEY_CONTACT_WRITABLE_FOR_JOIN = "contactwritableforjoin"; 123 private static final String KEY_SHOW_JOIN_SUGGESTIONS = "showJoinSuggestions"; 124 private static final String KEY_ENABLED = "enabled"; 125 private static final String KEY_STATUS = "status"; 126 private static final String KEY_NEW_LOCAL_PROFILE = "newLocalProfile"; 127 private static final String KEY_IS_USER_PROFILE = "isUserProfile"; 128 private static final String KEY_DISABLE_DELETE_MENU_OPTION = "disableDeleteMenuOption"; 129 private static final String KEY_UPDATED_PHOTOS = "updatedPhotos"; 130 private static final String KEY_IS_EDIT = "isEdit"; 131 private static final String KEY_HAS_NEW_CONTACT = "hasNewContact"; 132 private static final String KEY_NEW_CONTACT_READY = "newContactDataReady"; 133 private static final String KEY_EXISTING_CONTACT_READY = "existingContactDataReady"; 134 private static final String KEY_RAW_CONTACTS = "rawContacts"; 135 private static final String KEY_SEND_TO_VOICE_MAIL_STATE = "sendToVoicemailState"; 136 private static final String KEY_CUSTOM_RINGTONE = "customRingtone"; 137 private static final String KEY_ARE_PHONE_OPTIONS_CHANGEABLE = "arePhoneOptionsChangable"; 138 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 deleteMenu.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); 1064 deleteMenu.setIcon(R.drawable.ic_trash_white_24); 1065 1066 // Set visibility of menus 1067 doneMenu.setVisible(false); 1068 1069 // Discard menu is only available if at least one raw contact is editable 1070 discardMenu.setVisible(mState != null && 1071 mState.getFirstWritableRawContact(mContext) != null); 1072 1073 // help menu depending on whether this is inserting or editing 1074 if (Intent.ACTION_INSERT.equals(mAction)) { 1075 HelpUtils.prepareHelpMenuItem(mContext, helpMenu, R.string.help_url_people_add); 1076 splitMenu.setVisible(false); 1077 joinMenu.setVisible(false); 1078 deleteMenu.setVisible(false); 1079 } else if (Intent.ACTION_EDIT.equals(mAction)) { 1080 HelpUtils.prepareHelpMenuItem(mContext, helpMenu, R.string.help_url_people_edit); 1081 // Split only if more than one raw profile and not a user profile 1082 splitMenu.setVisible(mState.size() > 1 && !isEditingUserProfile()); 1083 // Cannot join a user profile 1084 joinMenu.setVisible(!isEditingUserProfile()); 1085 deleteMenu.setVisible(!mDisableDeleteMenuOption); 1086 } else { 1087 // something else, so don't show the help menu 1088 helpMenu.setVisible(false); 1089 } 1090 1091 // Hide telephony-related settings (ringtone, send to voicemail) 1092 // if we don't have a telephone or are editing a new contact. 1093 sendToVoiceMailMenu.setChecked(mSendToVoicemailState); 1094 sendToVoiceMailMenu.setVisible(mArePhoneOptionsChangable); 1095 ringToneMenu.setVisible(mArePhoneOptionsChangable); 1096 1097 int size = menu.size(); 1098 for (int i = 0; i < size; i++) { 1099 menu.getItem(i).setEnabled(mEnabled); 1100 } 1101 } 1102 1103 @Override 1104 public boolean onOptionsItemSelected(MenuItem item) { 1105 switch (item.getItemId()) { 1106 case R.id.menu_done: 1107 return save(SaveMode.CLOSE); 1108 case R.id.menu_discard: 1109 return revert(); 1110 case R.id.menu_delete: 1111 if (mListener != null) mListener.onDeleteRequested(mLookupUri); 1112 return true; 1113 case R.id.menu_split: 1114 return doSplitContactAction(); 1115 case R.id.menu_join: 1116 return doJoinContactAction(); 1117 case R.id.menu_set_ringtone: 1118 doPickRingtone(); 1119 return true; 1120 case R.id.menu_send_to_voicemail: 1121 // Update state and save 1122 mSendToVoicemailState = !mSendToVoicemailState; 1123 item.setChecked(mSendToVoicemailState); 1124 final Intent intent = ContactSaveService.createSetSendToVoicemail( 1125 mContext, mLookupUri, mSendToVoicemailState); 1126 mContext.startService(intent); 1127 return true; 1128 } 1129 1130 return false; 1131 } 1132 1133 private boolean doSplitContactAction() { 1134 if (!hasValidState()) return false; 1135 1136 final SplitContactConfirmationDialogFragment dialog = 1137 new SplitContactConfirmationDialogFragment(); 1138 dialog.setTargetFragment(this, 0); 1139 dialog.show(getFragmentManager(), SplitContactConfirmationDialogFragment.TAG); 1140 return true; 1141 } 1142 1143 private boolean doJoinContactAction() { 1144 if (!hasValidState()) { 1145 return false; 1146 } 1147 1148 // If we just started creating a new contact and haven't added any data, it's too 1149 // early to do a join 1150 if (mState.size() == 1 && mState.get(0).isContactInsert() && !hasPendingChanges()) { 1151 Toast.makeText(mContext, R.string.toast_join_with_empty_contact, 1152 Toast.LENGTH_LONG).show(); 1153 return true; 1154 } 1155 1156 return save(SaveMode.JOIN); 1157 } 1158 1159 /** 1160 * Check if our internal {@link #mState} is valid, usually checked before 1161 * performing user actions. 1162 */ 1163 private boolean hasValidState() { 1164 return mState.size() > 0; 1165 } 1166 1167 /** 1168 * Return true if there are any edits to the current contact which need to 1169 * be saved. 1170 */ 1171 private boolean hasPendingChanges() { 1172 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 1173 return RawContactModifier.hasChanges(mState, accountTypes); 1174 } 1175 1176 /** 1177 * Saves or creates the contact based on the mode, and if successful 1178 * finishes the activity. 1179 */ 1180 public boolean save(int saveMode) { 1181 if (!hasValidState() || mStatus != Status.EDITING) { 1182 return false; 1183 } 1184 1185 // If we are about to close the editor - there is no need to refresh the data 1186 if (saveMode == SaveMode.CLOSE || saveMode == SaveMode.SPLIT) { 1187 getLoaderManager().destroyLoader(LOADER_DATA); 1188 } 1189 1190 mStatus = Status.SAVING; 1191 1192 if (!hasPendingChanges()) { 1193 if (mLookupUri == null && saveMode == SaveMode.RELOAD) { 1194 // We don't have anything to save and there isn't even an existing contact yet. 1195 // Nothing to do, simply go back to editing mode 1196 mStatus = Status.EDITING; 1197 return true; 1198 } 1199 onSaveCompleted(false, saveMode, mLookupUri != null, mLookupUri); 1200 return true; 1201 } 1202 1203 setEnabled(false); 1204 1205 // Store account as default account, only if this is a new contact 1206 saveDefaultAccountIfNecessary(); 1207 1208 // Save contact 1209 Intent intent = ContactSaveService.createSaveContactIntent(mContext, mState, 1210 SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(), 1211 ((Activity)mContext).getClass(), ContactEditorActivity.ACTION_SAVE_COMPLETED, 1212 mUpdatedPhotos); 1213 mContext.startService(intent); 1214 1215 // Don't try to save the same photos twice. 1216 mUpdatedPhotos = new Bundle(); 1217 1218 return true; 1219 } 1220 1221 private void doPickRingtone() { 1222 1223 final Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); 1224 // Allow user to pick 'Default' 1225 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true); 1226 // Show only ringtones 1227 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_RINGTONE); 1228 // Allow the user to pick a silent ringtone 1229 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true); 1230 1231 final Uri ringtoneUri; 1232 if (mCustomRingtone != null) { 1233 ringtoneUri = Uri.parse(mCustomRingtone); 1234 } else { 1235 // Otherwise pick default ringtone Uri so that something is selected. 1236 ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE); 1237 } 1238 1239 // Put checkmark next to the current ringtone for this contact 1240 intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, ringtoneUri); 1241 1242 // Launch! 1243 try { 1244 startActivityForResult(intent, REQUEST_CODE_PICK_RINGTONE); 1245 } catch (ActivityNotFoundException ex) { 1246 Toast.makeText(mContext, R.string.missing_app, Toast.LENGTH_SHORT).show(); 1247 } 1248 } 1249 1250 private void handleRingtonePicked(Uri pickedUri) { 1251 if (pickedUri == null || RingtoneManager.isDefault(pickedUri)) { 1252 mCustomRingtone = null; 1253 } else { 1254 mCustomRingtone = pickedUri.toString(); 1255 } 1256 Intent intent = ContactSaveService.createSetRingtone( 1257 mContext, mLookupUri, mCustomRingtone); 1258 mContext.startService(intent); 1259 } 1260 1261 public static class CancelEditDialogFragment extends DialogFragment { 1262 1263 public static void show(ContactEditorFragment fragment) { 1264 CancelEditDialogFragment dialog = new CancelEditDialogFragment(); 1265 dialog.setTargetFragment(fragment, 0); 1266 dialog.show(fragment.getFragmentManager(), "cancelEditor"); 1267 } 1268 1269 @Override 1270 public Dialog onCreateDialog(Bundle savedInstanceState) { 1271 AlertDialog dialog = new AlertDialog.Builder(getActivity()) 1272 .setIconAttribute(android.R.attr.alertDialogIcon) 1273 .setMessage(R.string.cancel_confirmation_dialog_message) 1274 .setPositiveButton(android.R.string.ok, 1275 new DialogInterface.OnClickListener() { 1276 @Override 1277 public void onClick(DialogInterface dialogInterface, int whichButton) { 1278 ((ContactEditorFragment)getTargetFragment()).doRevertAction(); 1279 } 1280 } 1281 ) 1282 .setNegativeButton(android.R.string.cancel, null) 1283 .create(); 1284 return dialog; 1285 } 1286 } 1287 1288 private boolean revert() { 1289 if (mState.isEmpty() || !hasPendingChanges()) { 1290 doRevertAction(); 1291 } else { 1292 CancelEditDialogFragment.show(this); 1293 } 1294 return true; 1295 } 1296 1297 private void doRevertAction() { 1298 // When this Fragment is closed we don't want it to auto-save 1299 mStatus = Status.CLOSING; 1300 if (mListener != null) mListener.onReverted(); 1301 } 1302 1303 public void doSaveAction() { 1304 save(SaveMode.CLOSE); 1305 } 1306 1307 public void onJoinCompleted(Uri uri) { 1308 onSaveCompleted(false, SaveMode.RELOAD, uri != null, uri); 1309 } 1310 1311 public void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded, 1312 Uri contactLookupUri) { 1313 if (hadChanges) { 1314 if (saveSucceeded) { 1315 if (saveMode != SaveMode.JOIN) { 1316 Toast.makeText(mContext, R.string.contactSavedToast, Toast.LENGTH_SHORT).show(); 1317 } 1318 } else { 1319 Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show(); 1320 } 1321 } 1322 switch (saveMode) { 1323 case SaveMode.CLOSE: 1324 case SaveMode.HOME: 1325 final Intent resultIntent; 1326 if (saveSucceeded && contactLookupUri != null) { 1327 final String requestAuthority = 1328 mLookupUri == null ? null : mLookupUri.getAuthority(); 1329 1330 final String legacyAuthority = "contacts"; 1331 final Uri lookupUri; 1332 if (legacyAuthority.equals(requestAuthority)) { 1333 // Build legacy Uri when requested by caller 1334 final long contactId = ContentUris.parseId(Contacts.lookupContact( 1335 mContext.getContentResolver(), contactLookupUri)); 1336 final Uri legacyContentUri = Uri.parse("content://contacts/people"); 1337 final Uri legacyUri = ContentUris.withAppendedId( 1338 legacyContentUri, contactId); 1339 lookupUri = legacyUri; 1340 } else { 1341 // Otherwise pass back a lookup-style Uri 1342 lookupUri = contactLookupUri; 1343 } 1344 resultIntent = QuickContact.composeQuickContactsIntent(getActivity(), 1345 (Rect) null, lookupUri, QuickContactActivity.MODE_FULLY_EXPANDED, null); 1346 // Make sure not to show QuickContacts on top of another QuickContacts. 1347 resultIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 1348 } else { 1349 resultIntent = null; 1350 } 1351 // It is already saved, so prevent that it is saved again 1352 mStatus = Status.CLOSING; 1353 if (mListener != null) mListener.onSaveFinished(resultIntent); 1354 break; 1355 1356 case SaveMode.RELOAD: 1357 case SaveMode.JOIN: 1358 if (saveSucceeded && contactLookupUri != null) { 1359 // If it was a JOIN, we are now ready to bring up the join activity. 1360 if (saveMode == SaveMode.JOIN && hasValidState()) { 1361 showJoinAggregateActivity(contactLookupUri); 1362 } 1363 1364 // If this was in INSERT, we are changing into an EDIT now. 1365 // If it already was an EDIT, we are changing to the new Uri now 1366 mState = new RawContactDeltaList(); 1367 load(Intent.ACTION_EDIT, contactLookupUri, null); 1368 mStatus = Status.LOADING; 1369 getLoaderManager().restartLoader(LOADER_DATA, null, mDataLoaderListener); 1370 } 1371 break; 1372 1373 case SaveMode.SPLIT: 1374 mStatus = Status.CLOSING; 1375 if (mListener != null) { 1376 mListener.onContactSplit(contactLookupUri); 1377 } else { 1378 Log.d(TAG, "No listener registered, can not call onSplitFinished"); 1379 } 1380 break; 1381 } 1382 } 1383 1384 /** 1385 * Shows a list of aggregates that can be joined into the currently viewed aggregate. 1386 * 1387 * @param contactLookupUri the fresh URI for the currently edited contact (after saving it) 1388 */ 1389 private void showJoinAggregateActivity(Uri contactLookupUri) { 1390 if (contactLookupUri == null || !isAdded()) { 1391 return; 1392 } 1393 1394 mContactIdForJoin = ContentUris.parseId(contactLookupUri); 1395 mContactWritableForJoin = isContactWritable(); 1396 final Intent intent = new Intent(UI.PICK_JOIN_CONTACT_ACTION); 1397 intent.putExtra(UI.TARGET_CONTACT_ID_EXTRA_KEY, mContactIdForJoin); 1398 startActivityForResult(intent, REQUEST_CODE_JOIN); 1399 } 1400 1401 /** 1402 * Performs aggregation with the contact selected by the user from suggestions or A-Z list. 1403 */ 1404 private void joinAggregate(final long contactId) { 1405 Intent intent = ContactSaveService.createJoinContactsIntent(mContext, mContactIdForJoin, 1406 contactId, mContactWritableForJoin, 1407 ContactEditorActivity.class, ContactEditorActivity.ACTION_JOIN_COMPLETED); 1408 mContext.startService(intent); 1409 } 1410 1411 /** 1412 * Returns true if there is at least one writable raw contact in the current contact. 1413 */ 1414 private boolean isContactWritable() { 1415 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 1416 int size = mState.size(); 1417 for (int i = 0; i < size; i++) { 1418 RawContactDelta entity = mState.get(i); 1419 final AccountType type = entity.getAccountType(accountTypes); 1420 if (type.areContactsWritable()) { 1421 return true; 1422 } 1423 } 1424 return false; 1425 } 1426 1427 private boolean isEditingUserProfile() { 1428 return mNewLocalProfile || mIsUserProfile; 1429 } 1430 1431 public static interface Listener { 1432 /** 1433 * Contact was not found, so somehow close this fragment. This is raised after a contact 1434 * is removed via Menu/Delete (unless it was a new contact) 1435 */ 1436 void onContactNotFound(); 1437 1438 /** 1439 * Contact was split, so we can close now. 1440 * @param newLookupUri The lookup uri of the new contact that should be shown to the user. 1441 * The editor tries best to chose the most natural contact here. 1442 */ 1443 void onContactSplit(Uri newLookupUri); 1444 1445 /** 1446 * User has tapped Revert, close the fragment now. 1447 */ 1448 void onReverted(); 1449 1450 /** 1451 * Contact was saved and the Fragment can now be closed safely. 1452 */ 1453 void onSaveFinished(Intent resultIntent); 1454 1455 /** 1456 * User switched to editing a different contact (a suggestion from the 1457 * aggregation engine). 1458 */ 1459 void onEditOtherContactRequested( 1460 Uri contactLookupUri, ArrayList<ContentValues> contentValues); 1461 1462 /** 1463 * Contact is being created for an external account that provides its own 1464 * new contact activity. 1465 */ 1466 void onCustomCreateContactActivityRequested(AccountWithDataSet account, 1467 Bundle intentExtras); 1468 1469 /** 1470 * The edited raw contact belongs to an external account that provides 1471 * its own edit activity. 1472 * 1473 * @param redirect indicates that the current editor should be closed 1474 * before the custom editor is shown. 1475 */ 1476 void onCustomEditContactActivityRequested(AccountWithDataSet account, Uri rawContactUri, 1477 Bundle intentExtras, boolean redirect); 1478 1479 void onDeleteRequested(Uri contactUri); 1480 } 1481 1482 private class EntityDeltaComparator implements Comparator<RawContactDelta> { 1483 /** 1484 * Compare EntityDeltas for sorting the stack of editors. 1485 */ 1486 @Override 1487 public int compare(RawContactDelta one, RawContactDelta two) { 1488 // Check direct equality 1489 if (one.equals(two)) { 1490 return 0; 1491 } 1492 1493 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 1494 String accountType1 = one.getValues().getAsString(RawContacts.ACCOUNT_TYPE); 1495 String dataSet1 = one.getValues().getAsString(RawContacts.DATA_SET); 1496 final AccountType type1 = accountTypes.getAccountType(accountType1, dataSet1); 1497 String accountType2 = two.getValues().getAsString(RawContacts.ACCOUNT_TYPE); 1498 String dataSet2 = two.getValues().getAsString(RawContacts.DATA_SET); 1499 final AccountType type2 = accountTypes.getAccountType(accountType2, dataSet2); 1500 1501 // Check read-only. Sort read/write before read-only. 1502 if (!type1.areContactsWritable() && type2.areContactsWritable()) { 1503 return 1; 1504 } else if (type1.areContactsWritable() && !type2.areContactsWritable()) { 1505 return -1; 1506 } 1507 1508 // Check account type. Sort Google before non-Google. 1509 boolean skipAccountTypeCheck = false; 1510 boolean isGoogleAccount1 = type1 instanceof GoogleAccountType; 1511 boolean isGoogleAccount2 = type2 instanceof GoogleAccountType; 1512 if (isGoogleAccount1 && !isGoogleAccount2) { 1513 return -1; 1514 } else if (!isGoogleAccount1 && isGoogleAccount2) { 1515 return 1; 1516 } else if (isGoogleAccount1 && isGoogleAccount2){ 1517 skipAccountTypeCheck = true; 1518 } 1519 1520 int value; 1521 if (!skipAccountTypeCheck) { 1522 // Sort accounts with type before accounts without types. 1523 if (type1.accountType != null && type2.accountType == null) { 1524 return -1; 1525 } else if (type1.accountType == null && type2.accountType != null) { 1526 return 1; 1527 } 1528 1529 if (type1.accountType != null && type2.accountType != null) { 1530 value = type1.accountType.compareTo(type2.accountType); 1531 if (value != 0) { 1532 return value; 1533 } 1534 } 1535 1536 // Fall back to data set. Sort accounts with data sets before 1537 // those without. 1538 if (type1.dataSet != null && type2.dataSet == null) { 1539 return -1; 1540 } else if (type1.dataSet == null && type2.dataSet != null) { 1541 return 1; 1542 } 1543 1544 if (type1.dataSet != null && type2.dataSet != null) { 1545 value = type1.dataSet.compareTo(type2.dataSet); 1546 if (value != 0) { 1547 return value; 1548 } 1549 } 1550 } 1551 1552 // Check account name 1553 String oneAccount = one.getAccountName(); 1554 if (oneAccount == null) oneAccount = ""; 1555 String twoAccount = two.getAccountName(); 1556 if (twoAccount == null) twoAccount = ""; 1557 value = oneAccount.compareTo(twoAccount); 1558 if (value != 0) { 1559 return value; 1560 } 1561 1562 // Both are in the same account, fall back to contact ID 1563 Long oneId = one.getRawContactId(); 1564 Long twoId = two.getRawContactId(); 1565 if (oneId == null) { 1566 return -1; 1567 } else if (twoId == null) { 1568 return 1; 1569 } 1570 1571 return (int)(oneId - twoId); 1572 } 1573 } 1574 1575 /** 1576 * Returns the contact ID for the currently edited contact or 0 if the contact is new. 1577 */ 1578 protected long getContactId() { 1579 for (RawContactDelta rawContact : mState) { 1580 Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID); 1581 if (contactId != null) { 1582 return contactId; 1583 } 1584 } 1585 return 0; 1586 } 1587 1588 /** 1589 * Triggers an asynchronous search for aggregation suggestions. 1590 */ 1591 private void acquireAggregationSuggestions(Context context, 1592 RawContactEditorView rawContactEditor) { 1593 long rawContactId = rawContactEditor.getRawContactId(); 1594 if (mAggregationSuggestionsRawContactId != rawContactId 1595 && mAggregationSuggestionView != null) { 1596 mAggregationSuggestionView.setVisibility(View.GONE); 1597 mAggregationSuggestionView = null; 1598 mAggregationSuggestionEngine.reset(); 1599 } 1600 1601 mAggregationSuggestionsRawContactId = rawContactId; 1602 1603 if (mAggregationSuggestionEngine == null) { 1604 mAggregationSuggestionEngine = new AggregationSuggestionEngine(context); 1605 mAggregationSuggestionEngine.setListener(this); 1606 mAggregationSuggestionEngine.start(); 1607 } 1608 1609 mAggregationSuggestionEngine.setContactId(getContactId()); 1610 1611 LabeledEditorView nameEditor = rawContactEditor.getNameEditor(); 1612 mAggregationSuggestionEngine.onNameChange(nameEditor.getValues()); 1613 } 1614 1615 @Override 1616 public void onAggregationSuggestionChange() { 1617 Activity activity = getActivity(); 1618 if ((activity != null && activity.isFinishing()) 1619 || !isVisible() || mState.isEmpty() || mStatus != Status.EDITING) { 1620 return; 1621 } 1622 1623 UiClosables.closeQuietly(mAggregationSuggestionPopup); 1624 1625 if (mAggregationSuggestionEngine.getSuggestedContactCount() == 0) { 1626 return; 1627 } 1628 1629 final RawContactEditorView rawContactView = 1630 (RawContactEditorView)getRawContactEditorView(mAggregationSuggestionsRawContactId); 1631 if (rawContactView == null) { 1632 return; // Raw contact deleted? 1633 } 1634 final View anchorView = rawContactView.findViewById(R.id.anchor_view); 1635 mAggregationSuggestionPopup = new ListPopupWindow(mContext, null); 1636 mAggregationSuggestionPopup.setAnchorView(anchorView); 1637 mAggregationSuggestionPopup.setWidth(anchorView.getWidth()); 1638 mAggregationSuggestionPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); 1639 mAggregationSuggestionPopup.setAdapter( 1640 new AggregationSuggestionAdapter(getActivity(), 1641 mState.size() == 1 && mState.get(0).isContactInsert(), 1642 this, mAggregationSuggestionEngine.getSuggestions())); 1643 mAggregationSuggestionPopup.setOnItemClickListener(mAggregationSuggestionItemClickListener); 1644 mAggregationSuggestionPopup.show(); 1645 } 1646 1647 @Override 1648 public void onJoinAction(long contactId, List<Long> rawContactIdList) { 1649 long rawContactIds[] = new long[rawContactIdList.size()]; 1650 for (int i = 0; i < rawContactIds.length; i++) { 1651 rawContactIds[i] = rawContactIdList.get(i); 1652 } 1653 JoinSuggestedContactDialogFragment dialog = 1654 new JoinSuggestedContactDialogFragment(); 1655 Bundle args = new Bundle(); 1656 args.putLongArray("rawContactIds", rawContactIds); 1657 dialog.setArguments(args); 1658 dialog.setTargetFragment(this, 0); 1659 try { 1660 dialog.show(getFragmentManager(), "join"); 1661 } catch (Exception ex) { 1662 // No problem - the activity is no longer available to display the dialog 1663 } 1664 } 1665 1666 public static class JoinSuggestedContactDialogFragment extends DialogFragment { 1667 1668 @Override 1669 public Dialog onCreateDialog(Bundle savedInstanceState) { 1670 return new AlertDialog.Builder(getActivity()) 1671 .setIconAttribute(android.R.attr.alertDialogIcon) 1672 .setMessage(R.string.aggregation_suggestion_join_dialog_message) 1673 .setPositiveButton(android.R.string.yes, 1674 new DialogInterface.OnClickListener() { 1675 @Override 1676 public void onClick(DialogInterface dialog, int whichButton) { 1677 ContactEditorFragment targetFragment = 1678 (ContactEditorFragment) getTargetFragment(); 1679 long rawContactIds[] = 1680 getArguments().getLongArray("rawContactIds"); 1681 targetFragment.doJoinSuggestedContact(rawContactIds); 1682 } 1683 } 1684 ) 1685 .setNegativeButton(android.R.string.no, null) 1686 .create(); 1687 } 1688 } 1689 1690 /** 1691 * Joins the suggested contact (specified by the id's of constituent raw 1692 * contacts), save all changes, and stay in the editor. 1693 */ 1694 protected void doJoinSuggestedContact(long[] rawContactIds) { 1695 if (!hasValidState() || mStatus != Status.EDITING) { 1696 return; 1697 } 1698 1699 mState.setJoinWithRawContacts(rawContactIds); 1700 save(SaveMode.RELOAD); 1701 } 1702 1703 @Override 1704 public void onEditAction(Uri contactLookupUri) { 1705 SuggestionEditConfirmationDialogFragment dialog = 1706 new SuggestionEditConfirmationDialogFragment(); 1707 Bundle args = new Bundle(); 1708 args.putParcelable("contactUri", contactLookupUri); 1709 dialog.setArguments(args); 1710 dialog.setTargetFragment(this, 0); 1711 dialog.show(getFragmentManager(), "edit"); 1712 } 1713 1714 public static class SuggestionEditConfirmationDialogFragment extends DialogFragment { 1715 1716 @Override 1717 public Dialog onCreateDialog(Bundle savedInstanceState) { 1718 return new AlertDialog.Builder(getActivity()) 1719 .setIconAttribute(android.R.attr.alertDialogIcon) 1720 .setMessage(R.string.aggregation_suggestion_edit_dialog_message) 1721 .setPositiveButton(android.R.string.yes, 1722 new DialogInterface.OnClickListener() { 1723 @Override 1724 public void onClick(DialogInterface dialog, int whichButton) { 1725 ContactEditorFragment targetFragment = 1726 (ContactEditorFragment) getTargetFragment(); 1727 Uri contactUri = 1728 getArguments().getParcelable("contactUri"); 1729 targetFragment.doEditSuggestedContact(contactUri); 1730 } 1731 } 1732 ) 1733 .setNegativeButton(android.R.string.no, null) 1734 .create(); 1735 } 1736 } 1737 1738 /** 1739 * Abandons the currently edited contact and switches to editing the suggested 1740 * one, transferring all the data there 1741 */ 1742 protected void doEditSuggestedContact(Uri contactUri) { 1743 if (mListener != null) { 1744 // make sure we don't save this contact when closing down 1745 mStatus = Status.CLOSING; 1746 mListener.onEditOtherContactRequested( 1747 contactUri, mState.get(0).getContentValues()); 1748 } 1749 } 1750 1751 public void setAggregationSuggestionViewEnabled(boolean enabled) { 1752 if (mAggregationSuggestionView == null) { 1753 return; 1754 } 1755 1756 LinearLayout itemList = (LinearLayout) mAggregationSuggestionView.findViewById( 1757 R.id.aggregation_suggestions); 1758 int count = itemList.getChildCount(); 1759 for (int i = 0; i < count; i++) { 1760 itemList.getChildAt(i).setEnabled(enabled); 1761 } 1762 } 1763 1764 @Override 1765 public void onSaveInstanceState(Bundle outState) { 1766 outState.putParcelable(KEY_URI, mLookupUri); 1767 outState.putString(KEY_ACTION, mAction); 1768 1769 if (hasValidState()) { 1770 // Store entities with modifications 1771 outState.putParcelable(KEY_EDIT_STATE, mState); 1772 } 1773 outState.putLong(KEY_RAW_CONTACT_ID_REQUESTING_PHOTO, mRawContactIdRequestingPhoto); 1774 outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator); 1775 outState.putParcelable(KEY_CURRENT_PHOTO_URI, mCurrentPhotoUri); 1776 outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin); 1777 outState.putBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN, mContactWritableForJoin); 1778 outState.putLong(KEY_SHOW_JOIN_SUGGESTIONS, mAggregationSuggestionsRawContactId); 1779 outState.putBoolean(KEY_ENABLED, mEnabled); 1780 outState.putBoolean(KEY_NEW_LOCAL_PROFILE, mNewLocalProfile); 1781 outState.putBoolean(KEY_DISABLE_DELETE_MENU_OPTION, mDisableDeleteMenuOption); 1782 outState.putBoolean(KEY_IS_USER_PROFILE, mIsUserProfile); 1783 outState.putInt(KEY_STATUS, mStatus); 1784 outState.putParcelable(KEY_UPDATED_PHOTOS, mUpdatedPhotos); 1785 outState.putBoolean(KEY_HAS_NEW_CONTACT, mHasNewContact); 1786 outState.putBoolean(KEY_IS_EDIT, mIsEdit); 1787 outState.putBoolean(KEY_NEW_CONTACT_READY, mNewContactDataReady); 1788 outState.putBoolean(KEY_EXISTING_CONTACT_READY, mExistingContactDataReady); 1789 outState.putParcelableArrayList(KEY_RAW_CONTACTS, 1790 mRawContacts == null ? 1791 Lists.<RawContact> newArrayList() : Lists.newArrayList(mRawContacts)); 1792 outState.putBoolean(KEY_SEND_TO_VOICE_MAIL_STATE, mSendToVoicemailState); 1793 outState.putString(KEY_CUSTOM_RINGTONE, mCustomRingtone); 1794 outState.putBoolean(KEY_ARE_PHONE_OPTIONS_CHANGEABLE, mArePhoneOptionsChangable); 1795 1796 super.onSaveInstanceState(outState); 1797 } 1798 1799 @Override 1800 public void onActivityResult(int requestCode, int resultCode, Intent data) { 1801 if (mStatus == Status.SUB_ACTIVITY) { 1802 mStatus = Status.EDITING; 1803 } 1804 1805 // See if the photo selection handler handles this result. 1806 if (mCurrentPhotoHandler != null && mCurrentPhotoHandler.handlePhotoActivityResult( 1807 requestCode, resultCode, data)) { 1808 return; 1809 } 1810 1811 switch (requestCode) { 1812 case REQUEST_CODE_JOIN: { 1813 // Ignore failed requests 1814 if (resultCode != Activity.RESULT_OK) return; 1815 if (data != null) { 1816 final long contactId = ContentUris.parseId(data.getData()); 1817 joinAggregate(contactId); 1818 } 1819 break; 1820 } 1821 case REQUEST_CODE_ACCOUNTS_CHANGED: { 1822 // Bail if the account selector was not successful. 1823 if (resultCode != Activity.RESULT_OK) { 1824 mListener.onReverted(); 1825 return; 1826 } 1827 // If there's an account specified, use it. 1828 if (data != null) { 1829 AccountWithDataSet account = data.getParcelableExtra(Intents.Insert.ACCOUNT); 1830 if (account != null) { 1831 createContact(account); 1832 return; 1833 } 1834 } 1835 // If there isn't an account specified, then this is likely a phone-local 1836 // contact, so we should continue setting up the editor by automatically selecting 1837 // the most appropriate account. 1838 createContact(); 1839 break; 1840 } 1841 case REQUEST_CODE_PICK_RINGTONE: { 1842 if (data != null) { 1843 final Uri pickedUri = data.getParcelableExtra( 1844 RingtoneManager.EXTRA_RINGTONE_PICKED_URI); 1845 handleRingtonePicked(pickedUri); 1846 } 1847 break; 1848 } 1849 } 1850 } 1851 1852 /** 1853 * Sets the photo stored in mPhoto and writes it to the RawContact with the given id 1854 */ 1855 private void setPhoto(long rawContact, Bitmap photo, Uri photoUri) { 1856 BaseRawContactEditorView requestingEditor = getRawContactEditorView(rawContact); 1857 1858 if (photo == null || photo.getHeight() < 0 || photo.getWidth() < 0) { 1859 // This is unexpected. 1860 Log.w(TAG, "Invalid bitmap passed to setPhoto()"); 1861 } 1862 1863 if (requestingEditor != null) { 1864 requestingEditor.setPhotoBitmap(photo); 1865 } else { 1866 Log.w(TAG, "The contact that requested the photo is no longer present."); 1867 } 1868 1869 mUpdatedPhotos.putParcelable(String.valueOf(rawContact), photoUri); 1870 } 1871 1872 /** 1873 * Finds raw contact editor view for the given rawContactId. 1874 */ 1875 public BaseRawContactEditorView getRawContactEditorView(long rawContactId) { 1876 for (int i = 0; i < mContent.getChildCount(); i++) { 1877 final View childView = mContent.getChildAt(i); 1878 if (childView instanceof BaseRawContactEditorView) { 1879 final BaseRawContactEditorView editor = (BaseRawContactEditorView) childView; 1880 if (editor.getRawContactId() == rawContactId) { 1881 return editor; 1882 } 1883 } 1884 } 1885 return null; 1886 } 1887 1888 /** 1889 * Returns true if there is currently more than one photo on screen. 1890 */ 1891 private boolean hasMoreThanOnePhoto() { 1892 int countWithPicture = 0; 1893 final int numEntities = mState.size(); 1894 for (int i = 0; i < numEntities; i++) { 1895 final RawContactDelta entity = mState.get(i); 1896 if (entity.isVisible()) { 1897 final ValuesDelta primary = entity.getPrimaryEntry(Photo.CONTENT_ITEM_TYPE); 1898 if (primary != null && primary.getPhoto() != null) { 1899 countWithPicture++; 1900 } else { 1901 final long rawContactId = entity.getRawContactId(); 1902 final Uri uri = mUpdatedPhotos.getParcelable(String.valueOf(rawContactId)); 1903 if (uri != null) { 1904 try { 1905 mContext.getContentResolver().openInputStream(uri); 1906 countWithPicture++; 1907 } catch (FileNotFoundException e) { 1908 } 1909 } 1910 } 1911 1912 if (countWithPicture > 1) { 1913 return true; 1914 } 1915 } 1916 } 1917 return false; 1918 } 1919 1920 /** 1921 * The listener for the data loader 1922 */ 1923 private final LoaderManager.LoaderCallbacks<Contact> mDataLoaderListener = 1924 new LoaderCallbacks<Contact>() { 1925 @Override 1926 public Loader<Contact> onCreateLoader(int id, Bundle args) { 1927 mLoaderStartTime = SystemClock.elapsedRealtime(); 1928 return new ContactLoader(mContext, mLookupUri, true); 1929 } 1930 1931 @Override 1932 public void onLoadFinished(Loader<Contact> loader, Contact data) { 1933 final long loaderCurrentTime = SystemClock.elapsedRealtime(); 1934 Log.v(TAG, "Time needed for loading: " + (loaderCurrentTime-mLoaderStartTime)); 1935 if (!data.isLoaded()) { 1936 // Item has been deleted 1937 Log.i(TAG, "No contact found. Closing activity"); 1938 if (mListener != null) mListener.onContactNotFound(); 1939 return; 1940 } 1941 1942 mStatus = Status.EDITING; 1943 mLookupUri = data.getLookupUri(); 1944 final long setDataStartTime = SystemClock.elapsedRealtime(); 1945 setData(data); 1946 final long setDataEndTime = SystemClock.elapsedRealtime(); 1947 1948 Log.v(TAG, "Time needed for setting UI: " + (setDataEndTime-setDataStartTime)); 1949 } 1950 1951 @Override 1952 public void onLoaderReset(Loader<Contact> loader) { 1953 } 1954 }; 1955 1956 /** 1957 * The listener for the group meta data loader for all groups. 1958 */ 1959 private final LoaderManager.LoaderCallbacks<Cursor> mGroupLoaderListener = 1960 new LoaderCallbacks<Cursor>() { 1961 1962 @Override 1963 public CursorLoader onCreateLoader(int id, Bundle args) { 1964 return new GroupMetaDataLoader(mContext, Groups.CONTENT_URI); 1965 } 1966 1967 @Override 1968 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 1969 mGroupMetaData = data; 1970 bindGroupMetaData(); 1971 } 1972 1973 @Override 1974 public void onLoaderReset(Loader<Cursor> loader) { 1975 } 1976 }; 1977 1978 @Override 1979 public void onSplitContactConfirmed() { 1980 if (mState.isEmpty()) { 1981 // This may happen when this Fragment is recreated by the system during users 1982 // confirming the split action (and thus this method is called just before onCreate()), 1983 // for example. 1984 Log.e(TAG, "mState became null during the user's confirming split action. " + 1985 "Cannot perform the save action."); 1986 return; 1987 } 1988 1989 mState.markRawContactsForSplitting(); 1990 save(SaveMode.SPLIT); 1991 } 1992 1993 /** 1994 * Custom photo handler for the editor. The inner listener that this creates also has a 1995 * reference to the editor and acts as an {@link EditorListener}, and uses that editor to hold 1996 * state information in several of the listener methods. 1997 */ 1998 private final class PhotoHandler extends PhotoSelectionHandler { 1999 2000 final long mRawContactId; 2001 private final BaseRawContactEditorView mEditor; 2002 private final PhotoActionListener mPhotoEditorListener; 2003 2004 public PhotoHandler(Context context, BaseRawContactEditorView editor, int photoMode, 2005 RawContactDeltaList state) { 2006 super(context, editor.getPhotoEditor(), photoMode, false, state); 2007 mEditor = editor; 2008 mRawContactId = editor.getRawContactId(); 2009 mPhotoEditorListener = new PhotoEditorListener(); 2010 } 2011 2012 @Override 2013 public PhotoActionListener getListener() { 2014 return mPhotoEditorListener; 2015 } 2016 2017 @Override 2018 public void startPhotoActivity(Intent intent, int requestCode, Uri photoUri) { 2019 mRawContactIdRequestingPhoto = mEditor.getRawContactId(); 2020 mCurrentPhotoHandler = this; 2021 mStatus = Status.SUB_ACTIVITY; 2022 mCurrentPhotoUri = photoUri; 2023 ContactEditorFragment.this.startActivityForResult(intent, requestCode); 2024 } 2025 2026 private final class PhotoEditorListener extends PhotoSelectionHandler.PhotoActionListener 2027 implements EditorListener { 2028 2029 @Override 2030 public void onRequest(int request) { 2031 if (!hasValidState()) return; 2032 2033 if (request == EditorListener.REQUEST_PICK_PHOTO) { 2034 onClick(mEditor.getPhotoEditor()); 2035 } 2036 } 2037 2038 @Override 2039 public void onDeleteRequested(Editor removedEditor) { 2040 // The picture cannot be deleted, it can only be removed, which is handled by 2041 // onRemovePictureChosen() 2042 } 2043 2044 /** 2045 * User has chosen to set the selected photo as the (super) primary photo 2046 */ 2047 @Override 2048 public void onUseAsPrimaryChosen() { 2049 // Set the IsSuperPrimary for each editor 2050 int count = mContent.getChildCount(); 2051 for (int i = 0; i < count; i++) { 2052 final View childView = mContent.getChildAt(i); 2053 if (childView instanceof BaseRawContactEditorView) { 2054 final BaseRawContactEditorView editor = 2055 (BaseRawContactEditorView) childView; 2056 final PhotoEditorView photoEditor = editor.getPhotoEditor(); 2057 photoEditor.setSuperPrimary(editor == mEditor); 2058 } 2059 } 2060 bindEditors(); 2061 } 2062 2063 /** 2064 * User has chosen to remove a picture 2065 */ 2066 @Override 2067 public void onRemovePictureChosen() { 2068 mEditor.setPhotoBitmap(null); 2069 2070 // Prevent bitmap from being restored if rotate the device. 2071 // (only if we first chose a new photo before removing it) 2072 mUpdatedPhotos.remove(String.valueOf(mRawContactId)); 2073 bindEditors(); 2074 } 2075 2076 @Override 2077 public void onPhotoSelected(Uri uri) throws FileNotFoundException { 2078 final Bitmap bitmap = ContactPhotoUtils.getBitmapFromUri(mContext, uri); 2079 setPhoto(mRawContactId, bitmap, uri); 2080 mCurrentPhotoHandler = null; 2081 bindEditors(); 2082 } 2083 2084 @Override 2085 public Uri getCurrentPhotoUri() { 2086 return mCurrentPhotoUri; 2087 } 2088 2089 @Override 2090 public void onPhotoSelectionDismissed() { 2091 // Nothing to do. 2092 } 2093 } 2094 } 2095} 2096