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