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