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