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