ContactEditorFragment.java revision ced983d7a816256d93fdea1f81e63e4598c18875
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.Rect; 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 115 public static final String SAVE_MODE_EXTRA_KEY = "saveMode"; 116 117 /** 118 * An intent extra that forces the editor to add the edited contact 119 * to the default group (e.g. "My Contacts"). 120 */ 121 public static final String INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY = "addToDefaultDirectory"; 122 123 public static final String INTENT_EXTRA_NEW_LOCAL_PROFILE = "newLocalProfile"; 124 125 /** 126 * Modes that specify what the AsyncTask has to perform after saving 127 */ 128 // TODO: Move this into a common utils class or the save service because the contact and 129 // group editors need to use this interface definition 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 Bitmap mPhoto = null; 192 private long mRawContactIdRequestingPhoto = -1; 193 private long mRawContactIdRequestingPhotoAfterLoad = -1; 194 private PhotoSelectionHandler mPhotoSelectionHandler; 195 196 private final EntityDeltaComparator mComparator = new EntityDeltaComparator(); 197 198 private Cursor mGroupMetaData; 199 200 private File mCurrentPhotoFile; 201 202 private Context mContext; 203 private String mAction; 204 private Uri mLookupUri; 205 private Bundle mIntentExtras; 206 private Listener mListener; 207 208 private long mContactIdForJoin; 209 private boolean mContactWritableForJoin; 210 211 private ContactEditorUtils mEditorUtils; 212 213 private LinearLayout mContent; 214 private EntityDeltaList mState; 215 216 private ViewIdGenerator mViewIdGenerator; 217 218 private long mLoaderStartTime; 219 220 private int mStatus; 221 222 private AggregationSuggestionEngine mAggregationSuggestionEngine; 223 private long mAggregationSuggestionsRawContactId; 224 private View mAggregationSuggestionView; 225 226 private ListPopupWindow mAggregationSuggestionPopup; 227 228 private static final class AggregationSuggestionAdapter extends BaseAdapter { 229 private final Activity mActivity; 230 private final boolean mSetNewContact; 231 private final AggregationSuggestionView.Listener mListener; 232 private final List<Suggestion> mSuggestions; 233 234 public AggregationSuggestionAdapter(Activity activity, boolean setNewContact, 235 AggregationSuggestionView.Listener listener, List<Suggestion> suggestions) { 236 mActivity = activity; 237 mSetNewContact = setNewContact; 238 mListener = listener; 239 mSuggestions = suggestions; 240 } 241 242 @Override 243 public View getView(int position, View convertView, ViewGroup parent) { 244 Suggestion suggestion = (Suggestion) getItem(position); 245 LayoutInflater inflater = mActivity.getLayoutInflater(); 246 AggregationSuggestionView suggestionView = 247 (AggregationSuggestionView) inflater.inflate( 248 R.layout.aggregation_suggestions_item, null); 249 suggestionView.setNewContact(mSetNewContact); 250 suggestionView.setListener(mListener); 251 suggestionView.bindSuggestion(suggestion); 252 return suggestionView; 253 } 254 255 @Override 256 public long getItemId(int position) { 257 return position; 258 } 259 260 @Override 261 public Object getItem(int position) { 262 return mSuggestions.get(position); 263 } 264 265 @Override 266 public int getCount() { 267 return mSuggestions.size(); 268 } 269 } 270 271 private OnItemClickListener mAggregationSuggestionItemClickListener = 272 new OnItemClickListener() { 273 @Override 274 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 275 final AggregationSuggestionView suggestionView = (AggregationSuggestionView) view; 276 suggestionView.handleItemClickEvent(); 277 mAggregationSuggestionPopup.dismiss(); 278 mAggregationSuggestionPopup = null; 279 } 280 }; 281 282 private boolean mAutoAddToDefaultGroup; 283 284 private boolean mEnabled = true; 285 private boolean mRequestFocus; 286 private boolean mNewLocalProfile = false; 287 private boolean mIsUserProfile = false; 288 289 public ContactEditorFragment() { 290 } 291 292 public void setEnabled(boolean enabled) { 293 if (mEnabled != enabled) { 294 mEnabled = enabled; 295 if (mContent != null) { 296 int count = mContent.getChildCount(); 297 for (int i = 0; i < count; i++) { 298 mContent.getChildAt(i).setEnabled(enabled); 299 } 300 } 301 setAggregationSuggestionViewEnabled(enabled); 302 final Activity activity = getActivity(); 303 if (activity != null) activity.invalidateOptionsMenu(); 304 } 305 } 306 307 @Override 308 public void onAttach(Activity activity) { 309 super.onAttach(activity); 310 mContext = activity; 311 mEditorUtils = ContactEditorUtils.getInstance(mContext); 312 } 313 314 @Override 315 public void onStop() { 316 super.onStop(); 317 if (mAggregationSuggestionEngine != null) { 318 mAggregationSuggestionEngine.quit(); 319 } 320 321 // If anything was left unsaved, save it now but keep the editor open. 322 if (!getActivity().isChangingConfigurations() && mStatus == Status.EDITING) { 323 save(SaveMode.RELOAD); 324 } 325 } 326 327 @Override 328 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { 329 final View view = inflater.inflate(R.layout.contact_editor_fragment, container, false); 330 331 mContent = (LinearLayout) view.findViewById(R.id.editors); 332 333 setHasOptionsMenu(true); 334 335 // If we are in an orientation change, we already have mState (it was loaded by onCreate) 336 if (mState != null) { 337 bindEditors(); 338 } 339 340 return view; 341 } 342 343 @Override 344 public void onActivityCreated(Bundle savedInstanceState) { 345 super.onActivityCreated(savedInstanceState); 346 347 // Handle initial actions only when existing state missing 348 final boolean hasIncomingState = savedInstanceState != null; 349 350 if (!hasIncomingState) { 351 if (Intent.ACTION_EDIT.equals(mAction)) { 352 getLoaderManager().initLoader(LOADER_DATA, null, mDataLoaderListener); 353 } else if (Intent.ACTION_INSERT.equals(mAction)) { 354 final Account account = mIntentExtras == null ? null : 355 (Account) mIntentExtras.getParcelable(Intents.Insert.ACCOUNT); 356 final String dataSet = mIntentExtras == null ? null : 357 mIntentExtras.getString(Intents.Insert.DATA_SET); 358 359 if (account != null) { 360 // Account specified in Intent 361 createContact(new AccountWithDataSet(account.name, account.type, dataSet)); 362 } else { 363 // No Account specified. Let the user choose 364 // Load Accounts async so that we can present them 365 selectAccountAndCreateContact(); 366 } 367 } else if (ContactEditorActivity.ACTION_SAVE_COMPLETED.equals(mAction)) { 368 // do nothing 369 } else throw new IllegalArgumentException("Unknown Action String " + mAction + 370 ". Only support " + Intent.ACTION_EDIT + " or " + Intent.ACTION_INSERT); 371 } 372 } 373 374 @Override 375 public void onStart() { 376 getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupLoaderListener); 377 super.onStart(); 378 } 379 380 public void load(String action, Uri lookupUri, Bundle intentExtras) { 381 mAction = action; 382 mLookupUri = lookupUri; 383 mIntentExtras = intentExtras; 384 mAutoAddToDefaultGroup = mIntentExtras != null 385 && mIntentExtras.containsKey(INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY); 386 mNewLocalProfile = mIntentExtras != null 387 && mIntentExtras.getBoolean(INTENT_EXTRA_NEW_LOCAL_PROFILE); 388 } 389 390 public void setListener(Listener value) { 391 mListener = value; 392 } 393 394 @Override 395 public void onCreate(Bundle savedState) { 396 if (savedState != null) { 397 // Restore mUri before calling super.onCreate so that onInitializeLoaders 398 // would already have a uri and an action to work with 399 mLookupUri = savedState.getParcelable(KEY_URI); 400 mAction = savedState.getString(KEY_ACTION); 401 } 402 403 super.onCreate(savedState); 404 405 if (savedState == null) { 406 // If savedState is non-null, onRestoreInstanceState() will restore the generator. 407 mViewIdGenerator = new ViewIdGenerator(); 408 } else { 409 // Read state from savedState. No loading involved here 410 mState = savedState.<EntityDeltaList> getParcelable(KEY_EDIT_STATE); 411 mRawContactIdRequestingPhoto = savedState.getLong( 412 KEY_RAW_CONTACT_ID_REQUESTING_PHOTO); 413 mViewIdGenerator = savedState.getParcelable(KEY_VIEW_ID_GENERATOR); 414 String fileName = savedState.getString(KEY_CURRENT_PHOTO_FILE); 415 if (fileName != null) { 416 mCurrentPhotoFile = new File(fileName); 417 } 418 mContactIdForJoin = savedState.getLong(KEY_CONTACT_ID_FOR_JOIN); 419 mContactWritableForJoin = savedState.getBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN); 420 mAggregationSuggestionsRawContactId = savedState.getLong(KEY_SHOW_JOIN_SUGGESTIONS); 421 mEnabled = savedState.getBoolean(KEY_ENABLED); 422 mStatus = savedState.getInt(KEY_STATUS); 423 mNewLocalProfile = savedState.getBoolean(KEY_NEW_LOCAL_PROFILE); 424 mIsUserProfile = savedState.getBoolean(KEY_IS_USER_PROFILE); 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 data) { 467 setEnabled(true); 468 469 mState = EntityDeltaList.fromIterator(data.getEntities().iterator()); 470 setIntentExtras(mIntentExtras); 471 mIntentExtras = null; 472 473 // For user profile, change the contacts query URI 474 mIsUserProfile = data.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 (editor instanceof RawContactEditorView) { 722 final RawContactEditorView rawContactEditor = (RawContactEditorView) editor; 723 EditorListener listener = new EditorListener() { 724 725 @Override 726 public void onRequest(int request) { 727 if (request == EditorListener.FIELD_CHANGED && !isEditingUserProfile()) { 728 acquireAggregationSuggestions(rawContactEditor); 729 } 730 } 731 732 @Override 733 public void onDeleteRequested(Editor removedEditor) { 734 } 735 }; 736 737 final TextFieldsEditorView nameEditor = rawContactEditor.getNameEditor(); 738 if (mRequestFocus) { 739 nameEditor.requestFocus(); 740 mRequestFocus = false; 741 } 742 nameEditor.setEditorListener(listener); 743 744 final TextFieldsEditorView phoneticNameEditor = 745 rawContactEditor.getPhoneticNameEditor(); 746 phoneticNameEditor.setEditorListener(listener); 747 rawContactEditor.setAutoAddToDefaultGroup(mAutoAddToDefaultGroup); 748 749 if (rawContactId == mAggregationSuggestionsRawContactId) { 750 acquireAggregationSuggestions(rawContactEditor); 751 } 752 } 753 } 754 755 mRequestFocus = false; 756 757 bindGroupMetaData(); 758 759 // Show editor now that we've loaded state 760 mContent.setVisibility(View.VISIBLE); 761 762 // Refresh Action Bar as the visibility of the join command 763 // Activity can be null if we have been detached from the Activity 764 final Activity activity = getActivity(); 765 if (activity != null) activity.invalidateOptionsMenu(); 766 } 767 768 private void bindPhotoHandler(BaseRawContactEditorView editor, AccountType type, 769 EntityDeltaList state) { 770 final int mode; 771 if (type.areContactsWritable()) { 772 if (editor.hasSetPhoto()) { 773 if (hasMoreThanOnePhoto()) { 774 mode = PhotoActionPopup.Modes.PHOTO_ALLOW_PRIMARY; 775 } else { 776 mode = PhotoActionPopup.Modes.PHOTO_DISALLOW_PRIMARY; 777 } 778 } else { 779 mode = PhotoActionPopup.Modes.NO_PHOTO; 780 } 781 } else { 782 if (editor.hasSetPhoto() && hasMoreThanOnePhoto()) { 783 mode = PhotoActionPopup.Modes.READ_ONLY_ALLOW_PRIMARY; 784 } else { 785 // Read-only and either no photo or the only photo ==> no options 786 return; 787 } 788 } 789 mPhotoSelectionHandler = new PhotoHandler(mContext, editor, mode, state); 790 editor.getPhotoEditor().setEditorListener( 791 (PhotoHandler.PhotoEditorListener) mPhotoSelectionHandler.getListener()); 792 } 793 794 private void bindGroupMetaData() { 795 if (mGroupMetaData == null) { 796 return; 797 } 798 799 int editorCount = mContent.getChildCount(); 800 for (int i = 0; i < editorCount; i++) { 801 BaseRawContactEditorView editor = (BaseRawContactEditorView) mContent.getChildAt(i); 802 editor.setGroupMetaData(mGroupMetaData); 803 } 804 } 805 806 private void saveDefaultAccountIfNecessary() { 807 // Verify that this is a newly created contact, that the contact is composed of only 808 // 1 raw contact, and that the contact is not a user profile. 809 if (!Intent.ACTION_INSERT.equals(mAction) && mState.size() == 1 && 810 !isEditingUserProfile()) { 811 return; 812 } 813 814 // Find the associated account for this contact (retrieve it here because there are 815 // multiple paths to creating a contact and this ensures we always have the correct 816 // account). 817 final EntityDelta entity = mState.get(0); 818 final ValuesDelta values = entity.getValues(); 819 String name = values.getAsString(RawContacts.ACCOUNT_NAME); 820 String type = values.getAsString(RawContacts.ACCOUNT_TYPE); 821 String dataSet = values.getAsString(RawContacts.DATA_SET); 822 823 AccountWithDataSet account = (name == null || type == null) ? null : 824 new AccountWithDataSet(name, type, dataSet); 825 mEditorUtils.saveDefaultAndAllAccounts(account); 826 } 827 828 private void addAccountSwitcher( 829 final EntityDelta currentState, BaseRawContactEditorView editor) { 830 ValuesDelta values = currentState.getValues(); 831 final AccountWithDataSet currentAccount = new AccountWithDataSet( 832 values.getAsString(RawContacts.ACCOUNT_NAME), 833 values.getAsString(RawContacts.ACCOUNT_TYPE), 834 values.getAsString(RawContacts.DATA_SET)); 835 final View accountView = editor.findViewById(R.id.account); 836 final View anchorView = editor.findViewById(R.id.account_container); 837 accountView.setOnClickListener(new View.OnClickListener() { 838 @Override 839 public void onClick(View v) { 840 final ListPopupWindow popup = new ListPopupWindow(mContext, null); 841 final AccountsListAdapter adapter = 842 new AccountsListAdapter(mContext, 843 AccountListFilter.ACCOUNTS_CONTACT_WRITABLE, currentAccount); 844 popup.setWidth(anchorView.getWidth()); 845 popup.setAnchorView(anchorView); 846 popup.setAdapter(adapter); 847 popup.setModal(true); 848 popup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); 849 popup.setOnItemClickListener(new AdapterView.OnItemClickListener() { 850 @Override 851 public void onItemClick(AdapterView<?> parent, View view, int position, 852 long id) { 853 popup.dismiss(); 854 AccountWithDataSet newAccount = adapter.getItem(position); 855 if (!newAccount.equals(currentAccount)) { 856 rebindEditorsForNewContact(currentState, currentAccount, newAccount); 857 } 858 } 859 }); 860 popup.show(); 861 } 862 }); 863 } 864 865 private void disableAccountSwitcher(BaseRawContactEditorView editor) { 866 // Remove the pressed state from the account header because the user cannot switch accounts 867 // on an existing contact 868 final View accountView = editor.findViewById(R.id.account); 869 accountView.setBackgroundDrawable(null); 870 accountView.setEnabled(false); 871 } 872 873 @Override 874 public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) { 875 inflater.inflate(R.menu.edit_contact, menu); 876 } 877 878 @Override 879 public void onPrepareOptionsMenu(Menu menu) { 880 // This supports the keyboard shortcut to save changes to a contact but shouldn't be visible 881 // because the custom action bar contains the "save" button now (not the overflow menu). 882 // TODO: Find a better way to handle shortcuts, i.e. onKeyDown()? 883 menu.findItem(R.id.menu_done).setVisible(false); 884 885 // Split only if more than one raw profile and not a user profile 886 menu.findItem(R.id.menu_split).setVisible(mState != null && mState.size() > 1 && 887 !isEditingUserProfile()); 888 // Cannot join a user profile 889 menu.findItem(R.id.menu_join).setVisible(!isEditingUserProfile()); 890 891 892 int size = menu.size(); 893 for (int i = 0; i < size; i++) { 894 menu.getItem(i).setEnabled(mEnabled); 895 } 896 } 897 898 @Override 899 public boolean onOptionsItemSelected(MenuItem item) { 900 switch (item.getItemId()) { 901 case R.id.menu_done: 902 return save(SaveMode.CLOSE); 903 case R.id.menu_discard: 904 return revert(); 905 case R.id.menu_split: 906 return doSplitContactAction(); 907 case R.id.menu_join: 908 return doJoinContactAction(); 909 } 910 return false; 911 } 912 913 private boolean doSplitContactAction() { 914 if (!hasValidState()) return false; 915 916 final SplitContactConfirmationDialogFragment dialog = 917 new SplitContactConfirmationDialogFragment(); 918 dialog.setTargetFragment(this, 0); 919 dialog.show(getFragmentManager(), SplitContactConfirmationDialogFragment.TAG); 920 return true; 921 } 922 923 private boolean doJoinContactAction() { 924 if (!hasValidState()) { 925 return false; 926 } 927 928 // If we just started creating a new contact and haven't added any data, it's too 929 // early to do a join 930 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 931 if (mState.size() == 1 && mState.get(0).isContactInsert() 932 && !EntityModifier.hasChanges(mState, accountTypes)) { 933 Toast.makeText(getActivity(), R.string.toast_join_with_empty_contact, 934 Toast.LENGTH_LONG).show(); 935 return true; 936 } 937 938 return save(SaveMode.JOIN); 939 } 940 941 /** 942 * Check if our internal {@link #mState} is valid, usually checked before 943 * performing user actions. 944 */ 945 private boolean hasValidState() { 946 return mState != null && mState.size() > 0; 947 } 948 949 /** 950 * Saves or creates the contact based on the mode, and if successful 951 * finishes the activity. 952 */ 953 public boolean save(int saveMode) { 954 if (!hasValidState() || mStatus != Status.EDITING) { 955 return false; 956 } 957 958 // If we are about to close the editor - there is no need to refresh the data 959 if (saveMode == SaveMode.CLOSE || saveMode == SaveMode.SPLIT) { 960 getLoaderManager().destroyLoader(LOADER_DATA); 961 } 962 963 mStatus = Status.SAVING; 964 965 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 966 if (!EntityModifier.hasChanges(mState, accountTypes)) { 967 onSaveCompleted(false, saveMode, mLookupUri != null, mLookupUri); 968 return true; 969 } 970 971 setEnabled(false); 972 973 // Store account as default account, only if this is a new contact 974 saveDefaultAccountIfNecessary(); 975 976 // Save contact 977 Intent intent = ContactSaveService.createSaveContactIntent(getActivity(), mState, 978 SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(), 979 getActivity().getClass(), ContactEditorActivity.ACTION_SAVE_COMPLETED); 980 getActivity().startService(intent); 981 return true; 982 } 983 984 public static class CancelEditDialogFragment extends DialogFragment { 985 986 public static void show(ContactEditorFragment fragment) { 987 CancelEditDialogFragment dialog = new CancelEditDialogFragment(); 988 dialog.setTargetFragment(fragment, 0); 989 dialog.show(fragment.getFragmentManager(), "cancelEditor"); 990 } 991 992 @Override 993 public Dialog onCreateDialog(Bundle savedInstanceState) { 994 AlertDialog dialog = new AlertDialog.Builder(getActivity()) 995 .setIconAttribute(android.R.attr.alertDialogIcon) 996 .setMessage(R.string.cancel_confirmation_dialog_message) 997 .setPositiveButton(android.R.string.ok, 998 new DialogInterface.OnClickListener() { 999 @Override 1000 public void onClick(DialogInterface dialog, int whichButton) { 1001 ((ContactEditorFragment)getTargetFragment()).doRevertAction(); 1002 } 1003 } 1004 ) 1005 .setNegativeButton(android.R.string.cancel, null) 1006 .create(); 1007 return dialog; 1008 } 1009 } 1010 1011 private boolean revert() { 1012 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 1013 if (mState == null || !EntityModifier.hasChanges(mState, accountTypes)) { 1014 doRevertAction(); 1015 } else { 1016 CancelEditDialogFragment.show(this); 1017 } 1018 return true; 1019 } 1020 1021 private void doRevertAction() { 1022 // When this Fragment is closed we don't want it to auto-save 1023 mStatus = Status.CLOSING; 1024 if (mListener != null) mListener.onReverted(); 1025 } 1026 1027 public void doSaveAction() { 1028 save(SaveMode.CLOSE); 1029 } 1030 1031 public void onJoinCompleted(Uri uri) { 1032 onSaveCompleted(false, SaveMode.RELOAD, uri != null, uri); 1033 } 1034 1035 public void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded, 1036 Uri contactLookupUri) { 1037 Log.d(TAG, "onSaveCompleted(" + saveMode + ", " + contactLookupUri); 1038 if (hadChanges) { 1039 if (saveSucceeded) { 1040 if (saveMode != SaveMode.JOIN) { 1041 Toast.makeText(mContext, R.string.contactSavedToast, Toast.LENGTH_SHORT).show(); 1042 } 1043 } else { 1044 Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show(); 1045 } 1046 } 1047 switch (saveMode) { 1048 case SaveMode.CLOSE: 1049 case SaveMode.HOME: 1050 final Intent resultIntent; 1051 if (saveSucceeded && contactLookupUri != null) { 1052 final String requestAuthority = 1053 mLookupUri == null ? null : mLookupUri.getAuthority(); 1054 1055 final String legacyAuthority = "contacts"; 1056 1057 resultIntent = new Intent(); 1058 resultIntent.setAction(Intent.ACTION_VIEW); 1059 if (legacyAuthority.equals(requestAuthority)) { 1060 // Build legacy Uri when requested by caller 1061 final long contactId = ContentUris.parseId(Contacts.lookupContact( 1062 mContext.getContentResolver(), contactLookupUri)); 1063 final Uri legacyContentUri = Uri.parse("content://contacts/people"); 1064 final Uri legacyUri = ContentUris.withAppendedId( 1065 legacyContentUri, contactId); 1066 resultIntent.setData(legacyUri); 1067 } else { 1068 // Otherwise pass back a lookup-style Uri 1069 resultIntent.setData(contactLookupUri); 1070 } 1071 1072 } else { 1073 resultIntent = null; 1074 } 1075 // It is already saved, so prevent that it is saved again 1076 mStatus = Status.CLOSING; 1077 if (mListener != null) mListener.onSaveFinished(resultIntent); 1078 break; 1079 1080 case SaveMode.RELOAD: 1081 case SaveMode.JOIN: 1082 if (saveSucceeded && contactLookupUri != null) { 1083 // If it was a JOIN, we are now ready to bring up the join activity. 1084 if (saveMode == SaveMode.JOIN) { 1085 showJoinAggregateActivity(contactLookupUri); 1086 } 1087 1088 // If this was in INSERT, we are changing into an EDIT now. 1089 // If it already was an EDIT, we are changing to the new Uri now 1090 mState = null; 1091 load(Intent.ACTION_EDIT, contactLookupUri, null); 1092 mStatus = Status.LOADING; 1093 getLoaderManager().restartLoader(LOADER_DATA, null, mDataLoaderListener); 1094 } 1095 break; 1096 1097 case SaveMode.SPLIT: 1098 mStatus = Status.CLOSING; 1099 if (mListener != null) { 1100 mListener.onContactSplit(contactLookupUri); 1101 } else { 1102 Log.d(TAG, "No listener registered, can not call onSplitFinished"); 1103 } 1104 break; 1105 } 1106 } 1107 1108 /** 1109 * Shows a list of aggregates that can be joined into the currently viewed aggregate. 1110 * 1111 * @param contactLookupUri the fresh URI for the currently edited contact (after saving it) 1112 */ 1113 private void showJoinAggregateActivity(Uri contactLookupUri) { 1114 if (contactLookupUri == null || !isAdded()) { 1115 return; 1116 } 1117 1118 mContactIdForJoin = ContentUris.parseId(contactLookupUri); 1119 mContactWritableForJoin = isContactWritable(); 1120 final Intent intent = new Intent(JoinContactActivity.JOIN_CONTACT); 1121 intent.putExtra(JoinContactActivity.EXTRA_TARGET_CONTACT_ID, mContactIdForJoin); 1122 startActivityForResult(intent, REQUEST_CODE_JOIN); 1123 } 1124 1125 /** 1126 * Performs aggregation with the contact selected by the user from suggestions or A-Z list. 1127 */ 1128 private void joinAggregate(final long contactId) { 1129 Intent intent = ContactSaveService.createJoinContactsIntent(mContext, mContactIdForJoin, 1130 contactId, mContactWritableForJoin, 1131 ContactEditorActivity.class, ContactEditorActivity.ACTION_JOIN_COMPLETED); 1132 mContext.startService(intent); 1133 } 1134 1135 /** 1136 * Returns true if there is at least one writable raw contact in the current contact. 1137 */ 1138 private boolean isContactWritable() { 1139 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 1140 int size = mState.size(); 1141 for (int i = 0; i < size; i++) { 1142 ValuesDelta values = mState.get(i).getValues(); 1143 final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE); 1144 final String dataSet = values.getAsString(RawContacts.DATA_SET); 1145 final AccountType type = accountTypes.getAccountType(accountType, dataSet); 1146 if (type.areContactsWritable()) { 1147 return true; 1148 } 1149 } 1150 return false; 1151 } 1152 1153 private boolean isEditingUserProfile() { 1154 return mNewLocalProfile || mIsUserProfile; 1155 } 1156 1157 public static interface Listener { 1158 /** 1159 * Contact was not found, so somehow close this fragment. This is raised after a contact 1160 * is removed via Menu/Delete (unless it was a new contact) 1161 */ 1162 void onContactNotFound(); 1163 1164 /** 1165 * Contact was split, so we can close now. 1166 * @param newLookupUri The lookup uri of the new contact that should be shown to the user. 1167 * The editor tries best to chose the most natural contact here. 1168 */ 1169 void onContactSplit(Uri newLookupUri); 1170 1171 /** 1172 * User has tapped Revert, close the fragment now. 1173 */ 1174 void onReverted(); 1175 1176 /** 1177 * Contact was saved and the Fragment can now be closed safely. 1178 */ 1179 void onSaveFinished(Intent resultIntent); 1180 1181 /** 1182 * User switched to editing a different contact (a suggestion from the 1183 * aggregation engine). 1184 */ 1185 void onEditOtherContactRequested( 1186 Uri contactLookupUri, ArrayList<ContentValues> contentValues); 1187 1188 /** 1189 * Contact is being created for an external account that provides its own 1190 * new contact activity. 1191 */ 1192 void onCustomCreateContactActivityRequested(AccountWithDataSet account, 1193 Bundle intentExtras); 1194 1195 /** 1196 * The edited raw contact belongs to an external account that provides 1197 * its own edit activity. 1198 * 1199 * @param redirect indicates that the current editor should be closed 1200 * before the custom editor is shown. 1201 */ 1202 void onCustomEditContactActivityRequested(AccountWithDataSet account, Uri rawContactUri, 1203 Bundle intentExtras, boolean redirect); 1204 } 1205 1206 private class EntityDeltaComparator implements Comparator<EntityDelta> { 1207 /** 1208 * Compare EntityDeltas for sorting the stack of editors. 1209 */ 1210 @Override 1211 public int compare(EntityDelta one, EntityDelta two) { 1212 // Check direct equality 1213 if (one.equals(two)) { 1214 return 0; 1215 } 1216 1217 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 1218 String accountType1 = one.getValues().getAsString(RawContacts.ACCOUNT_TYPE); 1219 String dataSet1 = one.getValues().getAsString(RawContacts.DATA_SET); 1220 final AccountType type1 = accountTypes.getAccountType(accountType1, dataSet1); 1221 String accountType2 = two.getValues().getAsString(RawContacts.ACCOUNT_TYPE); 1222 String dataSet2 = two.getValues().getAsString(RawContacts.DATA_SET); 1223 final AccountType type2 = accountTypes.getAccountType(accountType2, dataSet2); 1224 1225 // Check read-only 1226 if (!type1.areContactsWritable() && type2.areContactsWritable()) { 1227 return 1; 1228 } else if (type1.areContactsWritable() && !type2.areContactsWritable()) { 1229 return -1; 1230 } 1231 1232 // Check account type 1233 boolean skipAccountTypeCheck = false; 1234 boolean isGoogleAccount1 = type1 instanceof GoogleAccountType; 1235 boolean isGoogleAccount2 = type2 instanceof GoogleAccountType; 1236 if (isGoogleAccount1 && !isGoogleAccount2) { 1237 return -1; 1238 } else if (!isGoogleAccount1 && isGoogleAccount2) { 1239 return 1; 1240 } else if (isGoogleAccount1 && isGoogleAccount2){ 1241 skipAccountTypeCheck = true; 1242 } 1243 1244 int value; 1245 if (!skipAccountTypeCheck) { 1246 if (type1.accountType == null) { 1247 return 1; 1248 } 1249 value = type1.accountType.compareTo(type2.accountType); 1250 if (value != 0) { 1251 return value; 1252 } else { 1253 // Fall back to data set. 1254 if (type1.dataSet != null) { 1255 value = type1.dataSet.compareTo(type2.dataSet); 1256 if (value != 0) { 1257 return value; 1258 } 1259 } else if (type2.dataSet != null) { 1260 return 1; 1261 } 1262 } 1263 } 1264 1265 // Check account name 1266 ValuesDelta oneValues = one.getValues(); 1267 String oneAccount = oneValues.getAsString(RawContacts.ACCOUNT_NAME); 1268 if (oneAccount == null) oneAccount = ""; 1269 ValuesDelta twoValues = two.getValues(); 1270 String twoAccount = twoValues.getAsString(RawContacts.ACCOUNT_NAME); 1271 if (twoAccount == null) twoAccount = ""; 1272 value = oneAccount.compareTo(twoAccount); 1273 if (value != 0) { 1274 return value; 1275 } 1276 1277 // Both are in the same account, fall back to contact ID 1278 Long oneId = oneValues.getAsLong(RawContacts._ID); 1279 Long twoId = twoValues.getAsLong(RawContacts._ID); 1280 if (oneId == null) { 1281 return -1; 1282 } else if (twoId == null) { 1283 return 1; 1284 } 1285 1286 return (int)(oneId - twoId); 1287 } 1288 } 1289 1290 /** 1291 * Returns the contact ID for the currently edited contact or 0 if the contact is new. 1292 */ 1293 protected long getContactId() { 1294 if (mState != null) { 1295 for (EntityDelta rawContact : mState) { 1296 Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID); 1297 if (contactId != null) { 1298 return contactId; 1299 } 1300 } 1301 } 1302 return 0; 1303 } 1304 1305 /** 1306 * Triggers an asynchronous search for aggregation suggestions. 1307 */ 1308 public void acquireAggregationSuggestions(RawContactEditorView rawContactEditor) { 1309 long rawContactId = rawContactEditor.getRawContactId(); 1310 if (mAggregationSuggestionsRawContactId != rawContactId 1311 && mAggregationSuggestionView != null) { 1312 mAggregationSuggestionView.setVisibility(View.GONE); 1313 mAggregationSuggestionView = null; 1314 mAggregationSuggestionEngine.reset(); 1315 } 1316 1317 mAggregationSuggestionsRawContactId = rawContactId; 1318 1319 if (mAggregationSuggestionEngine == null) { 1320 mAggregationSuggestionEngine = new AggregationSuggestionEngine(getActivity()); 1321 mAggregationSuggestionEngine.setListener(this); 1322 mAggregationSuggestionEngine.start(); 1323 } 1324 1325 mAggregationSuggestionEngine.setContactId(getContactId()); 1326 1327 LabeledEditorView nameEditor = rawContactEditor.getNameEditor(); 1328 mAggregationSuggestionEngine.onNameChange(nameEditor.getValues()); 1329 } 1330 1331 @Override 1332 public void onAggregationSuggestionChange() { 1333 if (!isAdded() || mState == null || mStatus != Status.EDITING) { 1334 return; 1335 } 1336 1337 if (mAggregationSuggestionPopup != null && mAggregationSuggestionPopup.isShowing()) { 1338 mAggregationSuggestionPopup.dismiss(); 1339 } 1340 1341 if (mAggregationSuggestionEngine.getSuggestedContactCount() == 0) { 1342 return; 1343 } 1344 1345 final RawContactEditorView rawContactView = 1346 (RawContactEditorView)getRawContactEditorView(mAggregationSuggestionsRawContactId); 1347 if (rawContactView == null) { 1348 return; // Raw contact deleted? 1349 } 1350 final View anchorView = rawContactView.findViewById(R.id.anchor_view); 1351 mAggregationSuggestionPopup = new ListPopupWindow(mContext, null); 1352 mAggregationSuggestionPopup.setAnchorView(anchorView); 1353 mAggregationSuggestionPopup.setWidth(anchorView.getWidth()); 1354 mAggregationSuggestionPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); 1355 mAggregationSuggestionPopup.setModal(true); 1356 mAggregationSuggestionPopup.setAdapter( 1357 new AggregationSuggestionAdapter(getActivity(), 1358 mState.size() == 1 && mState.get(0).isContactInsert(), 1359 this, mAggregationSuggestionEngine.getSuggestions())); 1360 mAggregationSuggestionPopup.setOnItemClickListener(mAggregationSuggestionItemClickListener); 1361 mAggregationSuggestionPopup.show(); 1362 } 1363 1364 @Override 1365 public void onJoinAction(long contactId, List<Long> rawContactIdList) { 1366 long rawContactIds[] = new long[rawContactIdList.size()]; 1367 for (int i = 0; i < rawContactIds.length; i++) { 1368 rawContactIds[i] = rawContactIdList.get(i); 1369 } 1370 JoinSuggestedContactDialogFragment dialog = 1371 new JoinSuggestedContactDialogFragment(); 1372 Bundle args = new Bundle(); 1373 args.putLongArray("rawContactIds", rawContactIds); 1374 dialog.setArguments(args); 1375 dialog.setTargetFragment(this, 0); 1376 try { 1377 dialog.show(getFragmentManager(), "join"); 1378 } catch (Exception ex) { 1379 // No problem - the activity is no longer available to display the dialog 1380 } 1381 } 1382 1383 public static class JoinSuggestedContactDialogFragment extends DialogFragment { 1384 1385 @Override 1386 public Dialog onCreateDialog(Bundle savedInstanceState) { 1387 return new AlertDialog.Builder(getActivity()) 1388 .setIconAttribute(android.R.attr.alertDialogIcon) 1389 .setMessage(R.string.aggregation_suggestion_join_dialog_message) 1390 .setPositiveButton(android.R.string.yes, 1391 new DialogInterface.OnClickListener() { 1392 public void onClick(DialogInterface dialog, int whichButton) { 1393 ContactEditorFragment targetFragment = 1394 (ContactEditorFragment) getTargetFragment(); 1395 long rawContactIds[] = 1396 getArguments().getLongArray("rawContactIds"); 1397 targetFragment.doJoinSuggestedContact(rawContactIds); 1398 } 1399 } 1400 ) 1401 .setNegativeButton(android.R.string.no, null) 1402 .create(); 1403 } 1404 } 1405 1406 /** 1407 * Joins the suggested contact (specified by the id's of constituent raw 1408 * contacts), save all changes, and stay in the editor. 1409 */ 1410 protected void doJoinSuggestedContact(long[] rawContactIds) { 1411 if (!hasValidState() || mStatus != Status.EDITING) { 1412 return; 1413 } 1414 1415 mState.setJoinWithRawContacts(rawContactIds); 1416 save(SaveMode.RELOAD); 1417 } 1418 1419 @Override 1420 public void onEditAction(Uri contactLookupUri) { 1421 SuggestionEditConfirmationDialogFragment dialog = 1422 new SuggestionEditConfirmationDialogFragment(); 1423 Bundle args = new Bundle(); 1424 args.putParcelable("contactUri", contactLookupUri); 1425 dialog.setArguments(args); 1426 dialog.setTargetFragment(this, 0); 1427 dialog.show(getFragmentManager(), "edit"); 1428 } 1429 1430 public static class SuggestionEditConfirmationDialogFragment extends DialogFragment { 1431 1432 @Override 1433 public Dialog onCreateDialog(Bundle savedInstanceState) { 1434 return new AlertDialog.Builder(getActivity()) 1435 .setIconAttribute(android.R.attr.alertDialogIcon) 1436 .setMessage(R.string.aggregation_suggestion_edit_dialog_message) 1437 .setPositiveButton(android.R.string.yes, 1438 new DialogInterface.OnClickListener() { 1439 public void onClick(DialogInterface dialog, int whichButton) { 1440 ContactEditorFragment targetFragment = 1441 (ContactEditorFragment) getTargetFragment(); 1442 Uri contactUri = 1443 getArguments().getParcelable("contactUri"); 1444 targetFragment.doEditSuggestedContact(contactUri); 1445 } 1446 } 1447 ) 1448 .setNegativeButton(android.R.string.no, null) 1449 .create(); 1450 } 1451 } 1452 1453 /** 1454 * Abandons the currently edited contact and switches to editing the suggested 1455 * one, transferring all the data there 1456 */ 1457 protected void doEditSuggestedContact(Uri contactUri) { 1458 if (mListener != null) { 1459 // make sure we don't save this contact when closing down 1460 mStatus = Status.CLOSING; 1461 mListener.onEditOtherContactRequested( 1462 contactUri, mState.get(0).getContentValues()); 1463 } 1464 } 1465 1466 public void setAggregationSuggestionViewEnabled(boolean enabled) { 1467 if (mAggregationSuggestionView == null) { 1468 return; 1469 } 1470 1471 LinearLayout itemList = (LinearLayout) mAggregationSuggestionView.findViewById( 1472 R.id.aggregation_suggestions); 1473 int count = itemList.getChildCount(); 1474 for (int i = 0; i < count; i++) { 1475 itemList.getChildAt(i).setEnabled(enabled); 1476 } 1477 } 1478 1479 /** 1480 * Computes bounds of the supplied view relative to its ascendant. 1481 */ 1482 private Rect getRelativeBounds(View ascendant, View view) { 1483 Rect rect = new Rect(); 1484 rect.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()); 1485 1486 View parent = (View) view.getParent(); 1487 while (parent != ascendant) { 1488 rect.offset(parent.getLeft(), parent.getTop()); 1489 parent = (View) parent.getParent(); 1490 } 1491 return rect; 1492 } 1493 1494 @Override 1495 public void onSaveInstanceState(Bundle outState) { 1496 outState.putParcelable(KEY_URI, mLookupUri); 1497 outState.putString(KEY_ACTION, mAction); 1498 1499 if (hasValidState()) { 1500 // Store entities with modifications 1501 outState.putParcelable(KEY_EDIT_STATE, mState); 1502 } 1503 1504 outState.putLong(KEY_RAW_CONTACT_ID_REQUESTING_PHOTO, mRawContactIdRequestingPhoto); 1505 outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator); 1506 if (mCurrentPhotoFile != null) { 1507 outState.putString(KEY_CURRENT_PHOTO_FILE, mCurrentPhotoFile.toString()); 1508 } 1509 outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin); 1510 outState.putBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN, mContactWritableForJoin); 1511 outState.putLong(KEY_SHOW_JOIN_SUGGESTIONS, mAggregationSuggestionsRawContactId); 1512 outState.putBoolean(KEY_ENABLED, mEnabled); 1513 outState.putBoolean(KEY_NEW_LOCAL_PROFILE, mNewLocalProfile); 1514 outState.putBoolean(KEY_IS_USER_PROFILE, mIsUserProfile); 1515 outState.putInt(KEY_STATUS, mStatus); 1516 super.onSaveInstanceState(outState); 1517 } 1518 1519 @Override 1520 public void onActivityResult(int requestCode, int resultCode, Intent data) { 1521 if (mStatus == Status.SUB_ACTIVITY) { 1522 mStatus = Status.EDITING; 1523 } 1524 1525 // See if the photo selection handler handles this result. 1526 if (mPhotoSelectionHandler != null && mPhotoSelectionHandler.handlePhotoActivityResult( 1527 requestCode, resultCode, data)) { 1528 return; 1529 } 1530 1531 switch (requestCode) { 1532 case REQUEST_CODE_JOIN: { 1533 // Ignore failed requests 1534 if (resultCode != Activity.RESULT_OK) return; 1535 if (data != null) { 1536 final long contactId = ContentUris.parseId(data.getData()); 1537 joinAggregate(contactId); 1538 } 1539 break; 1540 } 1541 case REQUEST_CODE_ACCOUNTS_CHANGED: { 1542 // Bail if the account selector was not successful. 1543 if (resultCode != Activity.RESULT_OK) { 1544 mListener.onReverted(); 1545 return; 1546 } 1547 // If there's an account specified, use it. 1548 if (data != null) { 1549 AccountWithDataSet account = data.getParcelableExtra(Intents.Insert.ACCOUNT); 1550 if (account != null) { 1551 createContact(account); 1552 return; 1553 } 1554 } 1555 // If there isn't an account specified, then this is likely a phone-local 1556 // contact, so we should continue setting up the editor by automatically selecting 1557 // the most appropriate account. 1558 createContact(); 1559 break; 1560 } 1561 } 1562 } 1563 1564 /** 1565 * Sets the photo stored in mPhoto and writes it to the RawContact with the given id 1566 */ 1567 private void setPhoto(long rawContact, Bitmap photo) { 1568 BaseRawContactEditorView requestingEditor = getRawContactEditorView(rawContact); 1569 if (requestingEditor != null) { 1570 requestingEditor.setPhotoBitmap(photo); 1571 } else { 1572 Log.w(TAG, "The contact that requested the photo is no longer present."); 1573 } 1574 } 1575 1576 /** 1577 * Finds raw contact editor view for the given rawContactId. 1578 */ 1579 public BaseRawContactEditorView getRawContactEditorView(long rawContactId) { 1580 for (int i = 0; i < mContent.getChildCount(); i++) { 1581 final View childView = mContent.getChildAt(i); 1582 if (childView instanceof BaseRawContactEditorView) { 1583 final BaseRawContactEditorView editor = (BaseRawContactEditorView) childView; 1584 if (editor.getRawContactId() == rawContactId) { 1585 return editor; 1586 } 1587 } 1588 } 1589 return null; 1590 } 1591 1592 /** 1593 * Returns true if there is currently more than one photo on screen. 1594 */ 1595 private boolean hasMoreThanOnePhoto() { 1596 int count = mContent.getChildCount(); 1597 int countWithPicture = 0; 1598 for (int i = 0; i < count; i++) { 1599 final View childView = mContent.getChildAt(i); 1600 if (childView instanceof BaseRawContactEditorView) { 1601 final BaseRawContactEditorView editor = (BaseRawContactEditorView) childView; 1602 if (editor.hasSetPhoto()) { 1603 countWithPicture++; 1604 if (countWithPicture > 1) return true; 1605 } 1606 } 1607 } 1608 1609 return false; 1610 } 1611 1612 /** 1613 * The listener for the data loader 1614 */ 1615 private final LoaderManager.LoaderCallbacks<ContactLoader.Result> mDataLoaderListener = 1616 new LoaderCallbacks<ContactLoader.Result>() { 1617 @Override 1618 public Loader<ContactLoader.Result> onCreateLoader(int id, Bundle args) { 1619 mLoaderStartTime = SystemClock.elapsedRealtime(); 1620 return new ContactLoader(mContext, mLookupUri); 1621 } 1622 1623 @Override 1624 public void onLoadFinished(Loader<ContactLoader.Result> loader, ContactLoader.Result data) { 1625 final long loaderCurrentTime = SystemClock.elapsedRealtime(); 1626 Log.v(TAG, "Time needed for loading: " + (loaderCurrentTime-mLoaderStartTime)); 1627 if (!data.isLoaded()) { 1628 // Item has been deleted 1629 Log.i(TAG, "No contact found. Closing activity"); 1630 if (mListener != null) mListener.onContactNotFound(); 1631 return; 1632 } 1633 1634 mStatus = Status.EDITING; 1635 mLookupUri = data.getLookupUri(); 1636 final long setDataStartTime = SystemClock.elapsedRealtime(); 1637 setData(data); 1638 final long setDataEndTime = SystemClock.elapsedRealtime(); 1639 1640 // If we are coming back from the photo trimmer, this will be set. 1641 if (mRawContactIdRequestingPhotoAfterLoad != -1) { 1642 setPhoto(mRawContactIdRequestingPhotoAfterLoad, mPhoto); 1643 mRawContactIdRequestingPhotoAfterLoad = -1; 1644 mPhoto = null; 1645 } 1646 Log.v(TAG, "Time needed for setting UI: " + (setDataEndTime-setDataStartTime)); 1647 } 1648 1649 @Override 1650 public void onLoaderReset(Loader<ContactLoader.Result> loader) { 1651 } 1652 }; 1653 1654 /** 1655 * The listener for the group meta data loader for all groups. 1656 */ 1657 private final LoaderManager.LoaderCallbacks<Cursor> mGroupLoaderListener = 1658 new LoaderCallbacks<Cursor>() { 1659 1660 @Override 1661 public CursorLoader onCreateLoader(int id, Bundle args) { 1662 return new GroupMetaDataLoader(mContext, Groups.CONTENT_URI); 1663 } 1664 1665 @Override 1666 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 1667 mGroupMetaData = data; 1668 bindGroupMetaData(); 1669 } 1670 1671 public void onLoaderReset(Loader<Cursor> loader) { 1672 } 1673 }; 1674 1675 @Override 1676 public void onSplitContactConfirmed() { 1677 if (mState == null) { 1678 // This may happen when this Fragment is recreated by the system during users 1679 // confirming the split action (and thus this method is called just before onCreate()), 1680 // for example. 1681 Log.e(TAG, "mState became null during the user's confirming split action. " + 1682 "Cannot perform the save action."); 1683 return; 1684 } 1685 1686 mState.markRawContactsForSplitting(); 1687 save(SaveMode.SPLIT); 1688 } 1689 1690 /** 1691 * Custom photo handler for the editor. The inner listener that this creates also has a 1692 * reference to the editor and acts as an {@link EditorListener}, and uses that editor to hold 1693 * state information in several of the listener methods. 1694 */ 1695 private final class PhotoHandler extends PhotoSelectionHandler { 1696 public PhotoHandler(Context context, BaseRawContactEditorView editor, int photoMode, 1697 EntityDeltaList state) { 1698 super(context, editor.getPhotoEditor(), photoMode, false, state); 1699 setListener(new PhotoEditorListener(editor)); 1700 } 1701 1702 private final class PhotoEditorListener extends PhotoSelectionHandler.PhotoActionListener 1703 implements EditorListener { 1704 private final BaseRawContactEditorView mEditor; 1705 1706 private PhotoEditorListener(BaseRawContactEditorView editor) { 1707 mEditor = editor; 1708 } 1709 1710 @Override 1711 public void onRequest(int request) { 1712 if (!hasValidState()) return; 1713 1714 if (request == EditorListener.REQUEST_PICK_PHOTO) { 1715 onClick(mEditor.getPhotoEditor()); 1716 } 1717 } 1718 1719 @Override 1720 public void onDeleteRequested(Editor removedEditor) { 1721 // The picture cannot be deleted, it can only be removed, which is handled by 1722 // onRemovePictureChosen() 1723 } 1724 1725 /** 1726 * User has chosen to set the selected photo as the (super) primary photo 1727 */ 1728 @Override 1729 public void onUseAsPrimaryChosen() { 1730 // Set the IsSuperPrimary for each editor 1731 int count = mContent.getChildCount(); 1732 for (int i = 0; i < count; i++) { 1733 final View childView = mContent.getChildAt(i); 1734 if (childView instanceof BaseRawContactEditorView) { 1735 final BaseRawContactEditorView editor = 1736 (BaseRawContactEditorView) childView; 1737 final PhotoEditorView photoEditor = editor.getPhotoEditor(); 1738 photoEditor.setSuperPrimary(editor == mEditor); 1739 } 1740 } 1741 } 1742 1743 /** 1744 * User has chosen to remove a picture 1745 */ 1746 @Override 1747 public void onRemovePictureChosen() { 1748 mEditor.setPhotoBitmap(null); 1749 } 1750 1751 @Override 1752 public void startTakePhotoActivity(Intent intent, int requestCode, File photoFile) { 1753 mRawContactIdRequestingPhoto = mEditor.getRawContactId(); 1754 mStatus = Status.SUB_ACTIVITY; 1755 mCurrentPhotoFile = photoFile; 1756 startActivityForResult(intent, requestCode); 1757 } 1758 1759 @Override 1760 public void startPickFromGalleryActivity(Intent intent, int requestCode) { 1761 mRawContactIdRequestingPhoto = mEditor.getRawContactId(); 1762 mStatus = Status.SUB_ACTIVITY; 1763 startActivityForResult(intent, requestCode); 1764 } 1765 1766 @Override 1767 public void onPhotoSelected(Bitmap bitmap) { 1768 setPhoto(mRawContactIdRequestingPhoto, bitmap); 1769 mRawContactIdRequestingPhotoAfterLoad = mRawContactIdRequestingPhoto; 1770 mRawContactIdRequestingPhoto = -1; 1771 } 1772 1773 @Override 1774 public File getCurrentPhotoFile() { 1775 return mCurrentPhotoFile; 1776 } 1777 1778 @Override 1779 public void onPhotoSelectionDismissed() { 1780 // Nothing to do. 1781 } 1782 } 1783 } 1784} 1785