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