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