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