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