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