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