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