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