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