ContactEditorFragment.java revision 2bbfae8f92d5c218c83a49621a4c8f98f4d7a394
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 && !isEditingUserProfile()) { 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 // Split only if more than one raw profile and not a user profile 807 menu.findItem(R.id.menu_split).setVisible(mState != null && mState.size() > 1 && 808 !isEditingUserProfile()); 809 // Cannot join a user profile 810 menu.findItem(R.id.menu_join).setVisible(!isEditingUserProfile()); 811 812 813 int size = menu.size(); 814 for (int i = 0; i < size; i++) { 815 menu.getItem(i).setEnabled(mEnabled); 816 } 817 } 818 819 @Override 820 public boolean onOptionsItemSelected(MenuItem item) { 821 switch (item.getItemId()) { 822 case R.id.menu_done: 823 return save(SaveMode.CLOSE); 824 case R.id.menu_discard: 825 return revert(); 826 case R.id.menu_split: 827 return doSplitContactAction(); 828 case R.id.menu_join: 829 return doJoinContactAction(); 830 } 831 return false; 832 } 833 834 private boolean doSplitContactAction() { 835 if (!hasValidState()) return false; 836 837 final SplitContactConfirmationDialogFragment dialog = 838 new SplitContactConfirmationDialogFragment(); 839 dialog.setTargetFragment(this, 0); 840 dialog.show(getFragmentManager(), SplitContactConfirmationDialogFragment.TAG); 841 return true; 842 } 843 844 private boolean doJoinContactAction() { 845 if (!hasValidState()) { 846 return false; 847 } 848 849 // If we just started creating a new contact and haven't added any data, it's too 850 // early to do a join 851 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 852 if (mState.size() == 1 && mState.get(0).isContactInsert() 853 && !EntityModifier.hasChanges(mState, accountTypes)) { 854 Toast.makeText(getActivity(), R.string.toast_join_with_empty_contact, 855 Toast.LENGTH_LONG).show(); 856 return true; 857 } 858 859 return save(SaveMode.JOIN); 860 } 861 862 private void loadPhotoPickSize() { 863 Cursor c = mContext.getContentResolver().query(DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI, 864 new String[]{DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null); 865 try { 866 c.moveToFirst(); 867 mPhotoPickSize = c.getInt(0); 868 } finally { 869 c.close(); 870 } 871 } 872 873 /** 874 * Constructs an intent for picking a photo from Gallery, cropping it and returning the bitmap. 875 */ 876 public Intent getPhotoPickIntent() { 877 Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null); 878 intent.setType("image/*"); 879 intent.putExtra("crop", "true"); 880 intent.putExtra("aspectX", 1); 881 intent.putExtra("aspectY", 1); 882 intent.putExtra("outputX", mPhotoPickSize); 883 intent.putExtra("outputY", mPhotoPickSize); 884 intent.putExtra("return-data", true); 885 return intent; 886 } 887 888 /** 889 * Check if our internal {@link #mState} is valid, usually checked before 890 * performing user actions. 891 */ 892 private boolean hasValidState() { 893 return mState != null && mState.size() > 0; 894 } 895 896 /** 897 * Create a file name for the icon photo using current time. 898 */ 899 private String getPhotoFileName() { 900 Date date = new Date(System.currentTimeMillis()); 901 SimpleDateFormat dateFormat = new SimpleDateFormat("'IMG'_yyyyMMdd_HHmmss"); 902 return dateFormat.format(date) + ".jpg"; 903 } 904 905 /** 906 * Constructs an intent for capturing a photo and storing it in a temporary file. 907 */ 908 public static Intent getTakePickIntent(File f) { 909 Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE, null); 910 intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(f)); 911 return intent; 912 } 913 914 /** 915 * Sends a newly acquired photo to Gallery for cropping 916 */ 917 protected void doCropPhoto(File f) { 918 try { 919 // Add the image to the media store 920 MediaScannerConnection.scanFile( 921 mContext, 922 new String[] { f.getAbsolutePath() }, 923 new String[] { null }, 924 null); 925 926 // Launch gallery to crop the photo 927 final Intent intent = getCropImageIntent(Uri.fromFile(f)); 928 mStatus = Status.SUB_ACTIVITY; 929 startActivityForResult(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA); 930 } catch (Exception e) { 931 Log.e(TAG, "Cannot crop image", e); 932 Toast.makeText(mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show(); 933 } 934 } 935 936 /** 937 * Constructs an intent for image cropping. 938 */ 939 public Intent getCropImageIntent(Uri photoUri) { 940 Intent intent = new Intent("com.android.camera.action.CROP"); 941 intent.setDataAndType(photoUri, "image/*"); 942 intent.putExtra("crop", "true"); 943 intent.putExtra("aspectX", 1); 944 intent.putExtra("aspectY", 1); 945 intent.putExtra("outputX", mPhotoPickSize); 946 intent.putExtra("outputY", mPhotoPickSize); 947 intent.putExtra("return-data", true); 948 return intent; 949 } 950 951 /** 952 * Saves or creates the contact based on the mode, and if successful 953 * finishes the activity. 954 */ 955 public boolean save(int saveMode) { 956 if (!hasValidState() || mStatus != Status.EDITING) { 957 return false; 958 } 959 960 // If we are about to close the editor - there is no need to refresh the data 961 if (saveMode == SaveMode.CLOSE || saveMode == SaveMode.SPLIT) { 962 getLoaderManager().destroyLoader(LOADER_DATA); 963 } 964 965 mStatus = Status.SAVING; 966 967 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 968 if (!EntityModifier.hasChanges(mState, accountTypes)) { 969 onSaveCompleted(false, saveMode, mLookupUri); 970 return true; 971 } 972 973 setEnabled(false); 974 975 Intent intent = ContactSaveService.createSaveContactIntent(getActivity(), mState, 976 SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(), 977 getActivity().getClass(), ContactEditorActivity.ACTION_SAVE_COMPLETED); 978 getActivity().startService(intent); 979 return true; 980 } 981 982 public static class CancelEditDialogFragment extends DialogFragment { 983 984 public static void show(ContactEditorFragment fragment) { 985 CancelEditDialogFragment dialog = new CancelEditDialogFragment(); 986 dialog.setTargetFragment(fragment, 0); 987 dialog.show(fragment.getFragmentManager(), "cancelEditor"); 988 } 989 990 @Override 991 public Dialog onCreateDialog(Bundle savedInstanceState) { 992 AlertDialog dialog = new AlertDialog.Builder(getActivity()) 993 .setIconAttribute(android.R.attr.alertDialogIcon) 994 .setTitle(R.string.cancel_confirmation_dialog_title) 995 .setMessage(R.string.cancel_confirmation_dialog_message) 996 .setPositiveButton(android.R.string.ok, 997 new DialogInterface.OnClickListener() { 998 @Override 999 public void onClick(DialogInterface dialog, int whichButton) { 1000 ((ContactEditorFragment)getTargetFragment()).doRevertAction(); 1001 } 1002 } 1003 ) 1004 .setNegativeButton(android.R.string.cancel, null) 1005 .create(); 1006 return dialog; 1007 } 1008 } 1009 1010 private boolean revert() { 1011 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 1012 if (mState == null || !EntityModifier.hasChanges(mState, accountTypes)) { 1013 doRevertAction(); 1014 } else { 1015 CancelEditDialogFragment.show(this); 1016 } 1017 return true; 1018 } 1019 1020 private void doRevertAction() { 1021 // When this Fragment is closed we don't want it to auto-save 1022 mStatus = Status.CLOSING; 1023 if (mListener != null) mListener.onReverted(); 1024 } 1025 1026 public void doSaveAction() { 1027 save(SaveMode.CLOSE); 1028 } 1029 1030 public void onJoinCompleted(Uri uri) { 1031 onSaveCompleted(false, SaveMode.RELOAD, uri); 1032 } 1033 1034 public void onSaveCompleted(boolean hadChanges, int saveMode, Uri contactLookupUri) { 1035 boolean success = contactLookupUri != null; 1036 Log.d(TAG, "onSaveCompleted(" + saveMode + ", " + contactLookupUri); 1037 if (hadChanges) { 1038 if (success) { 1039 if (saveMode != SaveMode.JOIN) { 1040 Toast.makeText(mContext, R.string.contactSavedToast, Toast.LENGTH_SHORT).show(); 1041 } 1042 } else { 1043 Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show(); 1044 } 1045 } 1046 switch (saveMode) { 1047 case SaveMode.CLOSE: 1048 case SaveMode.HOME: 1049 final Intent resultIntent; 1050 if (success && contactLookupUri != null) { 1051 final String requestAuthority = 1052 mLookupUri == null ? null : mLookupUri.getAuthority(); 1053 1054 final String legacyAuthority = "contacts"; 1055 1056 resultIntent = new Intent(); 1057 resultIntent.setAction(Intent.ACTION_VIEW); 1058 if (legacyAuthority.equals(requestAuthority)) { 1059 // Build legacy Uri when requested by caller 1060 final long contactId = ContentUris.parseId(Contacts.lookupContact( 1061 mContext.getContentResolver(), contactLookupUri)); 1062 final Uri legacyContentUri = Uri.parse("content://contacts/people"); 1063 final Uri legacyUri = ContentUris.withAppendedId( 1064 legacyContentUri, contactId); 1065 resultIntent.setData(legacyUri); 1066 } else { 1067 // Otherwise pass back a lookup-style Uri 1068 resultIntent.setData(contactLookupUri); 1069 } 1070 1071 } else { 1072 resultIntent = null; 1073 } 1074 // It is already saved, so prevent that it is saved again 1075 mStatus = Status.CLOSING; 1076 if (mListener != null) mListener.onSaveFinished(resultIntent); 1077 break; 1078 1079 case SaveMode.RELOAD: 1080 case SaveMode.JOIN: 1081 if (success && contactLookupUri != null) { 1082 // If it was a JOIN, we are now ready to bring up the join activity. 1083 if (saveMode == SaveMode.JOIN) { 1084 showJoinAggregateActivity(contactLookupUri); 1085 } 1086 1087 // If this was in INSERT, we are changing into an EDIT now. 1088 // If it already was an EDIT, we are changing to the new Uri now 1089 mState = null; 1090 load(Intent.ACTION_EDIT, contactLookupUri, null); 1091 mStatus = Status.LOADING; 1092 getLoaderManager().restartLoader(LOADER_DATA, null, mDataLoaderListener); 1093 } 1094 break; 1095 1096 case SaveMode.SPLIT: 1097 mStatus = Status.CLOSING; 1098 if (mListener != null) { 1099 mListener.onContactSplit(contactLookupUri); 1100 } else { 1101 Log.d(TAG, "No listener registered, can not call onSplitFinished"); 1102 } 1103 break; 1104 } 1105 } 1106 1107 /** 1108 * Shows a list of aggregates that can be joined into the currently viewed aggregate. 1109 * 1110 * @param contactLookupUri the fresh URI for the currently edited contact (after saving it) 1111 */ 1112 private void showJoinAggregateActivity(Uri contactLookupUri) { 1113 if (contactLookupUri == null || !isAdded()) { 1114 return; 1115 } 1116 1117 mContactIdForJoin = ContentUris.parseId(contactLookupUri); 1118 mContactWritableForJoin = isContactWritable(); 1119 final Intent intent = new Intent(JoinContactActivity.JOIN_CONTACT); 1120 intent.putExtra(JoinContactActivity.EXTRA_TARGET_CONTACT_ID, mContactIdForJoin); 1121 startActivityForResult(intent, REQUEST_CODE_JOIN); 1122 } 1123 1124 /** 1125 * Performs aggregation with the contact selected by the user from suggestions or A-Z list. 1126 */ 1127 private void joinAggregate(final long contactId) { 1128 Intent intent = ContactSaveService.createJoinContactsIntent(mContext, mContactIdForJoin, 1129 contactId, mContactWritableForJoin, 1130 ContactEditorActivity.class, ContactEditorActivity.ACTION_JOIN_COMPLETED); 1131 mContext.startService(intent); 1132 } 1133 1134 /** 1135 * Returns true if there is at least one writable raw contact in the current contact. 1136 */ 1137 private boolean isContactWritable() { 1138 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 1139 int size = mState.size(); 1140 for (int i = 0; i < size; i++) { 1141 ValuesDelta values = mState.get(i).getValues(); 1142 final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE); 1143 final String dataSet = values.getAsString(RawContacts.DATA_SET); 1144 final AccountType type = accountTypes.getAccountType(accountType, dataSet); 1145 if (!type.readOnly) { 1146 return true; 1147 } 1148 } 1149 return false; 1150 } 1151 1152 private boolean isEditingUserProfile() { 1153 return mNewLocalProfile || mIsUserProfile; 1154 } 1155 1156 public static interface Listener { 1157 /** 1158 * Contact was not found, so somehow close this fragment. This is raised after a contact 1159 * is removed via Menu/Delete (unless it was a new contact) 1160 */ 1161 void onContactNotFound(); 1162 1163 /** 1164 * Contact was split, so we can close now. 1165 * @param newLookupUri The lookup uri of the new contact that should be shown to the user. 1166 * The editor tries best to chose the most natural contact here. 1167 */ 1168 void onContactSplit(Uri newLookupUri); 1169 1170 /** 1171 * User has tapped Revert, close the fragment now. 1172 */ 1173 void onReverted(); 1174 1175 /** 1176 * Contact was saved and the Fragment can now be closed safely. 1177 */ 1178 void onSaveFinished(Intent resultIntent); 1179 1180 /** 1181 * User switched to editing a different contact (a suggestion from the 1182 * aggregation engine). 1183 */ 1184 void onEditOtherContactRequested( 1185 Uri contactLookupUri, ArrayList<ContentValues> contentValues); 1186 1187 /** 1188 * Contact is being created for an external account that provides its own 1189 * new contact activity. 1190 */ 1191 void onCustomCreateContactActivityRequested(AccountWithDataSet account, 1192 Bundle intentExtras); 1193 1194 /** 1195 * The edited raw contact belongs to an external account that provides 1196 * its own edit activity. 1197 * 1198 * @param redirect indicates that the current editor should be closed 1199 * before the custom editor is shown. 1200 */ 1201 void onCustomEditContactActivityRequested(AccountWithDataSet account, Uri rawContactUri, 1202 Bundle intentExtras, boolean redirect); 1203 } 1204 1205 private class EntityDeltaComparator implements Comparator<EntityDelta> { 1206 /** 1207 * Compare EntityDeltas for sorting the stack of editors. 1208 */ 1209 @Override 1210 public int compare(EntityDelta one, EntityDelta two) { 1211 // Check direct equality 1212 if (one.equals(two)) { 1213 return 0; 1214 } 1215 1216 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 1217 String accountType1 = one.getValues().getAsString(RawContacts.ACCOUNT_TYPE); 1218 String dataSet1 = one.getValues().getAsString(RawContacts.DATA_SET); 1219 final AccountType type1 = accountTypes.getAccountType(accountType1, dataSet1); 1220 String accountType2 = two.getValues().getAsString(RawContacts.ACCOUNT_TYPE); 1221 String dataSet2 = two.getValues().getAsString(RawContacts.DATA_SET); 1222 final AccountType type2 = accountTypes.getAccountType(accountType2, dataSet2); 1223 1224 // Check read-only 1225 if (type1.readOnly && !type2.readOnly) { 1226 return 1; 1227 } else if (!type1.readOnly && type2.readOnly) { 1228 return -1; 1229 } 1230 1231 // Check account type 1232 boolean skipAccountTypeCheck = false; 1233 boolean isGoogleAccount1 = type1 instanceof GoogleAccountType; 1234 boolean isGoogleAccount2 = type2 instanceof GoogleAccountType; 1235 if (isGoogleAccount1 && !isGoogleAccount2) { 1236 return -1; 1237 } else if (!isGoogleAccount1 && isGoogleAccount2) { 1238 return 1; 1239 } else if (isGoogleAccount1 && isGoogleAccount2){ 1240 skipAccountTypeCheck = true; 1241 } 1242 1243 int value; 1244 if (!skipAccountTypeCheck) { 1245 if (type1.accountType == null) { 1246 return 1; 1247 } 1248 value = type1.accountType.compareTo(type2.accountType); 1249 if (value != 0) { 1250 return value; 1251 } else { 1252 // Fall back to data set. 1253 if (type1.dataSet != null) { 1254 value = type1.dataSet.compareTo(type2.dataSet); 1255 if (value != 0) { 1256 return value; 1257 } 1258 } else if (type2.dataSet != null) { 1259 return 1; 1260 } 1261 } 1262 } 1263 1264 // Check account name 1265 ValuesDelta oneValues = one.getValues(); 1266 String oneAccount = oneValues.getAsString(RawContacts.ACCOUNT_NAME); 1267 if (oneAccount == null) oneAccount = ""; 1268 ValuesDelta twoValues = two.getValues(); 1269 String twoAccount = twoValues.getAsString(RawContacts.ACCOUNT_NAME); 1270 if (twoAccount == null) twoAccount = ""; 1271 value = oneAccount.compareTo(twoAccount); 1272 if (value != 0) { 1273 return value; 1274 } 1275 1276 // Both are in the same account, fall back to contact ID 1277 Long oneId = oneValues.getAsLong(RawContacts._ID); 1278 Long twoId = twoValues.getAsLong(RawContacts._ID); 1279 if (oneId == null) { 1280 return -1; 1281 } else if (twoId == null) { 1282 return 1; 1283 } 1284 1285 return (int)(oneId - twoId); 1286 } 1287 } 1288 1289 /** 1290 * Returns the contact ID for the currently edited contact or 0 if the contact is new. 1291 */ 1292 protected long getContactId() { 1293 if (mState != null) { 1294 for (EntityDelta rawContact : mState) { 1295 Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID); 1296 if (contactId != null) { 1297 return contactId; 1298 } 1299 } 1300 } 1301 return 0; 1302 } 1303 1304 /** 1305 * Triggers an asynchronous search for aggregation suggestions. 1306 */ 1307 public void acquireAggregationSuggestions(RawContactEditorView rawContactEditor) { 1308 long rawContactId = rawContactEditor.getRawContactId(); 1309 if (mAggregationSuggestionsRawContactId != rawContactId 1310 && mAggregationSuggestionView != null) { 1311 mAggregationSuggestionView.setVisibility(View.GONE); 1312 mAggregationSuggestionView = null; 1313 mAggregationSuggestionEngine.reset(); 1314 } 1315 1316 mAggregationSuggestionsRawContactId = rawContactId; 1317 1318 if (mAggregationSuggestionEngine == null) { 1319 mAggregationSuggestionEngine = new AggregationSuggestionEngine(getActivity()); 1320 mAggregationSuggestionEngine.setListener(this); 1321 mAggregationSuggestionEngine.start(); 1322 } 1323 1324 mAggregationSuggestionEngine.setContactId(getContactId()); 1325 1326 LabeledEditorView nameEditor = rawContactEditor.getNameEditor(); 1327 mAggregationSuggestionEngine.onNameChange(nameEditor.getValues()); 1328 } 1329 1330 @Override 1331 public void onAggregationSuggestionChange() { 1332 if (!isAdded() || mState == null || mStatus != Status.EDITING) { 1333 return; 1334 } 1335 1336 if (mAggregationSuggestionPopup != null && mAggregationSuggestionPopup.isShowing()) { 1337 mAggregationSuggestionPopup.dismiss(); 1338 } 1339 1340 if (mAggregationSuggestionEngine.getSuggestedContactCount() == 0) { 1341 return; 1342 } 1343 1344 final RawContactEditorView rawContactView = 1345 (RawContactEditorView)getRawContactEditorView(mAggregationSuggestionsRawContactId); 1346 final View anchorView = rawContactView.findViewById(R.id.anchor_view); 1347 mAggregationSuggestionPopup = new ListPopupWindow(mContext, null); 1348 mAggregationSuggestionPopup.setAnchorView(anchorView); 1349 mAggregationSuggestionPopup.setWidth(anchorView.getWidth()); 1350 mAggregationSuggestionPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); 1351 mAggregationSuggestionPopup.setModal(true); 1352 mAggregationSuggestionPopup.setAdapter( 1353 new AggregationSuggestionAdapter(getActivity(), 1354 mState.size() == 1 && mState.get(0).isContactInsert(), 1355 this, mAggregationSuggestionEngine.getSuggestions())); 1356 mAggregationSuggestionPopup.setOnItemClickListener(mAggregationSuggestionItemClickListener); 1357 mAggregationSuggestionPopup.show(); 1358 } 1359 1360 @Override 1361 public void onJoinAction(long contactId, List<Long> rawContactIdList) { 1362 long rawContactIds[] = new long[rawContactIdList.size()]; 1363 for (int i = 0; i < rawContactIds.length; i++) { 1364 rawContactIds[i] = rawContactIdList.get(i); 1365 } 1366 JoinSuggestedContactDialogFragment dialog = 1367 new JoinSuggestedContactDialogFragment(); 1368 Bundle args = new Bundle(); 1369 args.putLongArray("rawContactIds", rawContactIds); 1370 dialog.setArguments(args); 1371 dialog.setTargetFragment(this, 0); 1372 try { 1373 dialog.show(getFragmentManager(), "join"); 1374 } catch (Exception ex) { 1375 // No problem - the activity is no longer available to display the dialog 1376 } 1377 } 1378 1379 public static class JoinSuggestedContactDialogFragment extends DialogFragment { 1380 1381 @Override 1382 public Dialog onCreateDialog(Bundle savedInstanceState) { 1383 return new AlertDialog.Builder(getActivity()) 1384 .setIconAttribute(android.R.attr.alertDialogIcon) 1385 .setTitle(R.string.aggregation_suggestion_join_dialog_title) 1386 .setMessage(R.string.aggregation_suggestion_join_dialog_message) 1387 .setPositiveButton(android.R.string.yes, 1388 new DialogInterface.OnClickListener() { 1389 public void onClick(DialogInterface dialog, int whichButton) { 1390 ContactEditorFragment targetFragment = 1391 (ContactEditorFragment) getTargetFragment(); 1392 long rawContactIds[] = 1393 getArguments().getLongArray("rawContactIds"); 1394 targetFragment.doJoinSuggestedContact(rawContactIds); 1395 } 1396 } 1397 ) 1398 .setNegativeButton(android.R.string.no, null) 1399 .create(); 1400 } 1401 } 1402 1403 /** 1404 * Joins the suggested contact (specified by the id's of constituent raw 1405 * contacts), save all changes, and stay in the editor. 1406 */ 1407 protected void doJoinSuggestedContact(long[] rawContactIds) { 1408 if (!hasValidState() || mStatus != Status.EDITING) { 1409 return; 1410 } 1411 1412 mState.setJoinWithRawContacts(rawContactIds); 1413 save(SaveMode.RELOAD); 1414 } 1415 1416 @Override 1417 public void onEditAction(Uri contactLookupUri) { 1418 SuggestionEditConfirmationDialogFragment dialog = 1419 new SuggestionEditConfirmationDialogFragment(); 1420 Bundle args = new Bundle(); 1421 args.putParcelable("contactUri", contactLookupUri); 1422 dialog.setArguments(args); 1423 dialog.setTargetFragment(this, 0); 1424 dialog.show(getFragmentManager(), "edit"); 1425 } 1426 1427 public static class SuggestionEditConfirmationDialogFragment extends DialogFragment { 1428 1429 @Override 1430 public Dialog onCreateDialog(Bundle savedInstanceState) { 1431 return new AlertDialog.Builder(getActivity()) 1432 .setIconAttribute(android.R.attr.alertDialogIcon) 1433 .setTitle(R.string.aggregation_suggestion_edit_dialog_title) 1434 .setMessage(R.string.aggregation_suggestion_edit_dialog_message) 1435 .setPositiveButton(android.R.string.yes, 1436 new DialogInterface.OnClickListener() { 1437 public void onClick(DialogInterface dialog, int whichButton) { 1438 ContactEditorFragment targetFragment = 1439 (ContactEditorFragment) getTargetFragment(); 1440 Uri contactUri = 1441 getArguments().getParcelable("contactUri"); 1442 targetFragment.doEditSuggestedContact(contactUri); 1443 } 1444 } 1445 ) 1446 .setNegativeButton(android.R.string.no, null) 1447 .create(); 1448 } 1449 } 1450 1451 /** 1452 * Abandons the currently edited contact and switches to editing the suggested 1453 * one, transferring all the data there 1454 */ 1455 protected void doEditSuggestedContact(Uri contactUri) { 1456 if (mListener != null) { 1457 // make sure we don't save this contact when closing down 1458 mStatus = Status.CLOSING; 1459 mListener.onEditOtherContactRequested( 1460 contactUri, mState.get(0).getContentValues()); 1461 } 1462 } 1463 1464 public void setAggregationSuggestionViewEnabled(boolean enabled) { 1465 if (mAggregationSuggestionView == null) { 1466 return; 1467 } 1468 1469 LinearLayout itemList = (LinearLayout) mAggregationSuggestionView.findViewById( 1470 R.id.aggregation_suggestions); 1471 int count = itemList.getChildCount(); 1472 for (int i = 0; i < count; i++) { 1473 itemList.getChildAt(i).setEnabled(enabled); 1474 } 1475 } 1476 1477 /** 1478 * Computes bounds of the supplied view relative to its ascendant. 1479 */ 1480 private Rect getRelativeBounds(View ascendant, View view) { 1481 Rect rect = new Rect(); 1482 rect.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()); 1483 1484 View parent = (View) view.getParent(); 1485 while (parent != ascendant) { 1486 rect.offset(parent.getLeft(), parent.getTop()); 1487 parent = (View) parent.getParent(); 1488 } 1489 return rect; 1490 } 1491 1492 @Override 1493 public void onSaveInstanceState(Bundle outState) { 1494 outState.putParcelable(KEY_URI, mLookupUri); 1495 outState.putString(KEY_ACTION, mAction); 1496 1497 if (hasValidState()) { 1498 // Store entities with modifications 1499 outState.putParcelable(KEY_EDIT_STATE, mState); 1500 } 1501 1502 outState.putLong(KEY_RAW_CONTACT_ID_REQUESTING_PHOTO, mRawContactIdRequestingPhoto); 1503 outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator); 1504 if (mCurrentPhotoFile != null) { 1505 outState.putString(KEY_CURRENT_PHOTO_FILE, mCurrentPhotoFile.toString()); 1506 } 1507 outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin); 1508 outState.putBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN, mContactWritableForJoin); 1509 outState.putLong(KEY_SHOW_JOIN_SUGGESTIONS, mAggregationSuggestionsRawContactId); 1510 outState.putBoolean(KEY_ENABLED, mEnabled); 1511 outState.putBoolean(KEY_NEW_LOCAL_PROFILE, mNewLocalProfile); 1512 outState.putBoolean(KEY_IS_USER_PROFILE, mIsUserProfile); 1513 outState.putInt(KEY_STATUS, mStatus); 1514 super.onSaveInstanceState(outState); 1515 } 1516 1517 @Override 1518 public void onActivityResult(int requestCode, int resultCode, Intent data) { 1519 if (mStatus == Status.SUB_ACTIVITY) { 1520 mStatus = Status.EDITING; 1521 } 1522 1523 // Ignore failed requests 1524 if (resultCode != Activity.RESULT_OK) return; 1525 switch (requestCode) { 1526 case REQUEST_CODE_PHOTO_PICKED_WITH_DATA: { 1527 // As we are coming back to this view, the editor will be reloaded automatically, 1528 // which will cause the photo that is set here to disappear. To prevent this, 1529 // we remember to set a flag which is interpreted after loading. 1530 // This photo is set here already to reduce flickering. 1531 mPhoto = data.getParcelableExtra("data"); 1532 setPhoto(mRawContactIdRequestingPhoto, mPhoto); 1533 mRawContactIdRequestingPhotoAfterLoad = mRawContactIdRequestingPhoto; 1534 mRawContactIdRequestingPhoto = -1; 1535 1536 break; 1537 } 1538 case REQUEST_CODE_CAMERA_WITH_DATA: { 1539 doCropPhoto(mCurrentPhotoFile); 1540 break; 1541 } 1542 case REQUEST_CODE_JOIN: { 1543 if (data != null) { 1544 final long contactId = ContentUris.parseId(data.getData()); 1545 joinAggregate(contactId); 1546 } 1547 break; 1548 } 1549 } 1550 } 1551 1552 /** 1553 * Sets the photo stored in mPhoto and writes it to the RawContact with the given id 1554 */ 1555 private void setPhoto(long rawContact, Bitmap photo) { 1556 BaseRawContactEditorView requestingEditor = getRawContactEditorView(rawContact); 1557 if (requestingEditor != null) { 1558 requestingEditor.setPhotoBitmap(photo); 1559 } else { 1560 Log.w(TAG, "The contact that requested the photo is no longer present."); 1561 } 1562 } 1563 1564 /** 1565 * Finds raw contact editor view for the given rawContactId. 1566 */ 1567 public BaseRawContactEditorView getRawContactEditorView(long rawContactId) { 1568 for (int i = 0; i < mContent.getChildCount(); i++) { 1569 final View childView = mContent.getChildAt(i); 1570 if (childView instanceof BaseRawContactEditorView) { 1571 final BaseRawContactEditorView editor = (BaseRawContactEditorView) childView; 1572 if (editor.getRawContactId() == rawContactId) { 1573 return editor; 1574 } 1575 } 1576 } 1577 return null; 1578 } 1579 1580 /** 1581 * Returns true if there is currently more than one photo on screen. 1582 */ 1583 private boolean hasMoreThanOnePhoto() { 1584 int count = mContent.getChildCount(); 1585 int countWithPicture = 0; 1586 for (int i = 0; i < count; i++) { 1587 final View childView = mContent.getChildAt(i); 1588 if (childView instanceof BaseRawContactEditorView) { 1589 final BaseRawContactEditorView editor = (BaseRawContactEditorView) childView; 1590 if (editor.hasSetPhoto()) { 1591 countWithPicture++; 1592 if (countWithPicture > 1) return true; 1593 } 1594 } 1595 } 1596 1597 return false; 1598 } 1599 1600 /** 1601 * The listener for the data loader 1602 */ 1603 private final LoaderManager.LoaderCallbacks<ContactLoader.Result> mDataLoaderListener = 1604 new LoaderCallbacks<ContactLoader.Result>() { 1605 @Override 1606 public Loader<ContactLoader.Result> onCreateLoader(int id, Bundle args) { 1607 mLoaderStartTime = SystemClock.elapsedRealtime(); 1608 return new ContactLoader(mContext, mLookupUri); 1609 } 1610 1611 @Override 1612 public void onLoadFinished(Loader<ContactLoader.Result> loader, ContactLoader.Result data) { 1613 final long loaderCurrentTime = SystemClock.elapsedRealtime(); 1614 Log.v(TAG, "Time needed for loading: " + (loaderCurrentTime-mLoaderStartTime)); 1615 if (data == ContactLoader.Result.NOT_FOUND || data == ContactLoader.Result.ERROR) { 1616 // Item has been deleted 1617 Log.i(TAG, "No contact found. Closing activity"); 1618 if (mListener != null) mListener.onContactNotFound(); 1619 return; 1620 } 1621 1622 mStatus = Status.EDITING; 1623 mLookupUri = data.getLookupUri(); 1624 final long setDataStartTime = SystemClock.elapsedRealtime(); 1625 setData(data); 1626 final long setDataEndTime = SystemClock.elapsedRealtime(); 1627 1628 // If we are coming back from the photo trimmer, this will be set. 1629 if (mRawContactIdRequestingPhotoAfterLoad != -1) { 1630 setPhoto(mRawContactIdRequestingPhotoAfterLoad, mPhoto); 1631 mRawContactIdRequestingPhotoAfterLoad = -1; 1632 mPhoto = null; 1633 } 1634 Log.v(TAG, "Time needed for setting UI: " + (setDataEndTime-setDataStartTime)); 1635 } 1636 1637 @Override 1638 public void onLoaderReset(Loader<ContactLoader.Result> loader) { 1639 } 1640 }; 1641 1642 /** 1643 * The listener for the group meta data loader for all groups. 1644 */ 1645 private final LoaderManager.LoaderCallbacks<Cursor> mGroupLoaderListener = 1646 new LoaderCallbacks<Cursor>() { 1647 1648 @Override 1649 public CursorLoader onCreateLoader(int id, Bundle args) { 1650 return new GroupMetaDataLoader(mContext, Groups.CONTENT_URI); 1651 } 1652 1653 @Override 1654 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 1655 mGroupMetaData = data; 1656 bindGroupMetaData(); 1657 } 1658 1659 public void onLoaderReset(Loader<Cursor> loader) { 1660 } 1661 }; 1662 1663 @Override 1664 public void onSplitContactConfirmed() { 1665 mState.markRawContactsForSplitting(); 1666 save(SaveMode.SPLIT); 1667 } 1668 1669 private final class PhotoEditorListener 1670 implements EditorListener, PhotoActionPopup.Listener { 1671 private final BaseRawContactEditorView mEditor; 1672 private final boolean mAccountReadOnly; 1673 1674 private PhotoEditorListener(BaseRawContactEditorView editor, boolean accountReadOnly) { 1675 mEditor = editor; 1676 mAccountReadOnly = accountReadOnly; 1677 } 1678 1679 @Override 1680 public void onRequest(int request) { 1681 if (!hasValidState()) return; 1682 1683 if (request == EditorListener.REQUEST_PICK_PHOTO) { 1684 // Determine mode 1685 final int mode; 1686 if (mAccountReadOnly) { 1687 if (mEditor.hasSetPhoto() && hasMoreThanOnePhoto()) { 1688 mode = PhotoActionPopup.MODE_READ_ONLY_ALLOW_PRIMARY; 1689 } else { 1690 // Read-only and either no photo or the only photo ==> no options 1691 return; 1692 } 1693 } else { 1694 if (mEditor.hasSetPhoto()) { 1695 if (hasMoreThanOnePhoto()) { 1696 mode = PhotoActionPopup.MODE_PHOTO_ALLOW_PRIMARY; 1697 } else { 1698 mode = PhotoActionPopup.MODE_PHOTO_DISALLOW_PRIMARY; 1699 } 1700 } else { 1701 mode = PhotoActionPopup.MODE_NO_PHOTO; 1702 } 1703 } 1704 PhotoActionPopup.createPopupMenu(mContext, mEditor.getPhotoEditor(), this, mode) 1705 .show(); 1706 } 1707 } 1708 1709 @Override 1710 public void onDeleteRequested(Editor removedEditor) { 1711 // The picture cannot be deleted, it can only be removed, which is handled by 1712 // onRemovePictureChosen() 1713 } 1714 1715 /** 1716 * User has chosen to set the selected photo as the (super) primary photo 1717 */ 1718 @Override 1719 public void onUseAsPrimaryChosen() { 1720 // Set the IsSuperPrimary for each editor 1721 int count = mContent.getChildCount(); 1722 for (int i = 0; i < count; i++) { 1723 final View childView = mContent.getChildAt(i); 1724 if (childView instanceof BaseRawContactEditorView) { 1725 final BaseRawContactEditorView editor = (BaseRawContactEditorView) childView; 1726 final PhotoEditorView photoEditor = editor.getPhotoEditor(); 1727 photoEditor.setSuperPrimary(editor == mEditor); 1728 } 1729 } 1730 } 1731 1732 /** 1733 * User has chosen to remove a picture 1734 */ 1735 @Override 1736 public void onRemovePictureChosen() { 1737 mEditor.setPhotoBitmap(null); 1738 } 1739 1740 /** 1741 * Launches Camera to take a picture and store it in a file. 1742 */ 1743 @Override 1744 public void onTakePhotoChosen() { 1745 mRawContactIdRequestingPhoto = mEditor.getRawContactId(); 1746 try { 1747 // Launch camera to take photo for selected contact 1748 PHOTO_DIR.mkdirs(); 1749 mCurrentPhotoFile = new File(PHOTO_DIR, getPhotoFileName()); 1750 final Intent intent = getTakePickIntent(mCurrentPhotoFile); 1751 1752 mStatus = Status.SUB_ACTIVITY; 1753 startActivityForResult(intent, REQUEST_CODE_CAMERA_WITH_DATA); 1754 } catch (ActivityNotFoundException e) { 1755 Toast.makeText(mContext, R.string.photoPickerNotFoundText, 1756 Toast.LENGTH_LONG).show(); 1757 } 1758 } 1759 1760 /** 1761 * Launches Gallery to pick a photo. 1762 */ 1763 @Override 1764 public void onPickFromGalleryChosen() { 1765 mRawContactIdRequestingPhoto = mEditor.getRawContactId(); 1766 try { 1767 // Launch picker to choose photo for selected contact 1768 final Intent intent = getPhotoPickIntent(); 1769 mStatus = Status.SUB_ACTIVITY; 1770 startActivityForResult(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA); 1771 } catch (ActivityNotFoundException e) { 1772 Toast.makeText(mContext, R.string.photoPickerNotFoundText, 1773 Toast.LENGTH_LONG).show(); 1774 } 1775 } 1776 } 1777} 1778