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