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