ContactEditorFragment.java revision 2293e55d550fbc9974d1185960715e59acb14a85
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, Organization.CONTENT_ITEM_TYPE); 546 EntityModifier.ensureKindExists(insert, newAccountType, Event.CONTENT_ITEM_TYPE); 547 EntityModifier.ensureKindExists(insert, newAccountType, StructuredPostal.CONTENT_ITEM_TYPE); 548 549 if (mState == null) { 550 // Create state if none exists yet 551 mState = EntityDeltaList.fromSingle(insert); 552 } else { 553 // Add contact onto end of existing state 554 mState.add(insert); 555 } 556 557 mRequestFocus = true; 558 559 bindEditors(); 560 } 561 562 private void bindEditors() { 563 // Sort the editors 564 Collections.sort(mState, mComparator); 565 566 // Remove any existing editors and rebuild any visible 567 mContent.removeAllViews(); 568 569 final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( 570 Context.LAYOUT_INFLATER_SERVICE); 571 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 572 int numRawContacts = mState.size(); 573 for (int i = 0; i < numRawContacts; i++) { 574 // TODO ensure proper ordering of entities in the list 575 final EntityDelta entity = mState.get(i); 576 final ValuesDelta values = entity.getValues(); 577 if (!values.isVisible()) continue; 578 579 final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE); 580 final AccountType type = accountTypes.getAccountType(accountType); 581 final long rawContactId = values.getAsLong(RawContacts._ID); 582 583 final BaseRawContactEditorView editor; 584 if (type.isExternal()) { 585 editor = (BaseRawContactEditorView) inflater.inflate( 586 R.layout.external_raw_contact_editor_view, mContent, false); 587 ((ExternalRawContactEditorView) editor).setListener(this); 588 } else { 589 final RawContactEditorView rawContactEditor = (RawContactEditorView) 590 inflater.inflate(R.layout.raw_contact_editor_view, mContent, false); 591 // For existing contacts, only show the account header if there is more than 1 raw 592 // contact in the aggregate contact. 593 if (Intent.ACTION_EDIT.equals(mAction)) { 594 rawContactEditor.setAccountHeaderVisible(numRawContacts > 1); 595 } 596 editor = rawContactEditor; 597 } 598 if (Intent.ACTION_INSERT.equals(mAction) && numRawContacts == 1) { 599 final ArrayList<Account> accounts = 600 AccountTypeManager.getInstance(mContext).getAccounts(true); 601 if (accounts.size() > 1) { 602 addAccountSwitcher(mState.get(0), editor); 603 } else { 604 disableAccountSwitcher(editor); 605 } 606 } 607 608 editor.setEnabled(mEnabled); 609 610 mContent.addView(editor); 611 612 editor.setState(entity, type, mViewIdGenerator); 613 614 editor.getPhotoEditor().setEditorListener( 615 new PhotoEditorListener(editor, type.readOnly)); 616 if (editor instanceof RawContactEditorView) { 617 final RawContactEditorView rawContactEditor = (RawContactEditorView) editor; 618 EditorListener listener = new EditorListener() { 619 620 @Override 621 public void onRequest(int request) { 622 if (request == EditorListener.FIELD_CHANGED) { 623 acquireAggregationSuggestions(rawContactEditor); 624 } 625 } 626 627 @Override 628 public void onDeleteRequested(Editor removedEditor) { 629 } 630 }; 631 632 final TextFieldsEditorView nameEditor = rawContactEditor.getNameEditor(); 633 if (mRequestFocus) { 634 nameEditor.requestFocus(); 635 mRequestFocus = false; 636 } 637 nameEditor.setEditorListener(listener); 638 639 final TextFieldsEditorView phoneticNameEditor = 640 rawContactEditor.getPhoneticNameEditor(); 641 phoneticNameEditor.setEditorListener(listener); 642 rawContactEditor.setAutoAddToDefaultGroup(mAutoAddToDefaultGroup); 643 644 if (rawContactId == mAggregationSuggestionsRawContactId) { 645 acquireAggregationSuggestions(rawContactEditor); 646 } 647 } 648 } 649 650 mRequestFocus = false; 651 652 bindGroupMetaData(); 653 654 // Show editor now that we've loaded state 655 mContent.setVisibility(View.VISIBLE); 656 657 // Refresh Action Bar as the visibility of the join command 658 // Activity can be null if we have been detached from the Activity 659 final Activity activity = getActivity(); 660 if (activity != null) activity.invalidateOptionsMenu(); 661 662 } 663 664 private void bindGroupMetaData() { 665 if (mGroupMetaData == null) { 666 return; 667 } 668 669 int editorCount = mContent.getChildCount(); 670 for (int i = 0; i < editorCount; i++) { 671 BaseRawContactEditorView editor = (BaseRawContactEditorView) mContent.getChildAt(i); 672 editor.setGroupMetaData(mGroupMetaData); 673 } 674 } 675 676 private void addAccountSwitcher( 677 final EntityDelta currentState, BaseRawContactEditorView editor) { 678 ValuesDelta values = currentState.getValues(); 679 final Account currentAccount = new Account( 680 values.getAsString(RawContacts.ACCOUNT_NAME), 681 values.getAsString(RawContacts.ACCOUNT_TYPE)); 682 final View accountView = editor.findViewById(R.id.account); 683 final View anchorView = editor.findViewById(R.id.anchor_for_account_switcher); 684 accountView.setOnClickListener(new View.OnClickListener() { 685 @Override 686 public void onClick(View v) { 687 final ListPopupWindow popup = new ListPopupWindow(mContext, null); 688 final AccountsListAdapter adapter = 689 new AccountsListAdapter(mContext, true, currentAccount); 690 popup.setWidth(anchorView.getWidth()); 691 popup.setAnchorView(anchorView); 692 popup.setAdapter(adapter); 693 popup.setModal(true); 694 popup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); 695 popup.setOnItemClickListener(new AdapterView.OnItemClickListener() { 696 @Override 697 public void onItemClick(AdapterView<?> parent, View view, int position, 698 long id) { 699 popup.dismiss(); 700 Account newAccount = adapter.getItem(position); 701 if (!newAccount.equals(currentAccount)) { 702 rebindEditorsForNewContact(currentState, currentAccount, newAccount); 703 } 704 } 705 }); 706 popup.show(); 707 } 708 }); 709 } 710 711 private void disableAccountSwitcher(BaseRawContactEditorView editor) { 712 // Remove the pressed state from the account header because the user cannot switch accounts 713 // on an existing contact 714 final View accountView = editor.findViewById(R.id.account); 715 accountView.setBackgroundDrawable(null); 716 accountView.setEnabled(false); 717 } 718 719 @Override 720 public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) { 721 inflater.inflate(R.menu.edit_contact, menu); 722 } 723 724 @Override 725 public void onPrepareOptionsMenu(Menu menu) { 726 // This supports the keyboard shortcut to save changes to a contact but shouldn't be visible 727 // because the custom action bar contains the "save" button now (not the overflow menu). 728 // TODO: Find a better way to handle shortcuts, i.e. onKeyDown()? 729 menu.findItem(R.id.menu_done).setVisible(false); 730 731 menu.findItem(R.id.menu_split).setVisible(mState != null && mState.size() > 1); 732 int size = menu.size(); 733 for (int i = 0; i < size; i++) { 734 menu.getItem(i).setEnabled(mEnabled); 735 } 736 } 737 738 @Override 739 public boolean onOptionsItemSelected(MenuItem item) { 740 switch (item.getItemId()) { 741 case R.id.menu_done: 742 return save(SaveMode.CLOSE); 743 case R.id.menu_discard: 744 return revert(); 745 case R.id.menu_split: 746 return doSplitContactAction(); 747 case R.id.menu_join: 748 return doJoinContactAction(); 749 } 750 return false; 751 } 752 753 private boolean doSplitContactAction() { 754 if (!hasValidState()) return false; 755 756 final SplitContactConfirmationDialogFragment dialog = 757 new SplitContactConfirmationDialogFragment(); 758 dialog.setTargetFragment(this, 0); 759 dialog.show(getFragmentManager(), SplitContactConfirmationDialogFragment.TAG); 760 return true; 761 } 762 763 private boolean doJoinContactAction() { 764 if (!hasValidState()) { 765 return false; 766 } 767 768 // If we just started creating a new contact and haven't added any data, it's too 769 // early to do a join 770 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 771 if (mState.size() == 1 && mState.get(0).isContactInsert() 772 && !EntityModifier.hasChanges(mState, accountTypes)) { 773 Toast.makeText(getActivity(), R.string.toast_join_with_empty_contact, 774 Toast.LENGTH_LONG).show(); 775 return true; 776 } 777 778 return save(SaveMode.JOIN); 779 } 780 781 private void loadPhotoPickSize() { 782 Cursor c = mContext.getContentResolver().query(DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI, 783 new String[]{DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null); 784 try { 785 c.moveToFirst(); 786 mPhotoPickSize = c.getInt(0); 787 } finally { 788 c.close(); 789 } 790 } 791 792 /** 793 * Constructs an intent for picking a photo from Gallery, cropping it and returning the bitmap. 794 */ 795 public Intent getPhotoPickIntent() { 796 Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null); 797 intent.setType("image/*"); 798 intent.putExtra("crop", "true"); 799 intent.putExtra("aspectX", 1); 800 intent.putExtra("aspectY", 1); 801 intent.putExtra("outputX", mPhotoPickSize); 802 intent.putExtra("outputY", mPhotoPickSize); 803 intent.putExtra("return-data", true); 804 return intent; 805 } 806 807 /** 808 * Check if our internal {@link #mState} is valid, usually checked before 809 * performing user actions. 810 */ 811 private boolean hasValidState() { 812 return mState != null && mState.size() > 0; 813 } 814 815 /** 816 * Create a file name for the icon photo using current time. 817 */ 818 private String getPhotoFileName() { 819 Date date = new Date(System.currentTimeMillis()); 820 SimpleDateFormat dateFormat = new SimpleDateFormat("'IMG'_yyyyMMdd_HHmmss"); 821 return dateFormat.format(date) + ".jpg"; 822 } 823 824 /** 825 * Constructs an intent for capturing a photo and storing it in a temporary file. 826 */ 827 public static Intent getTakePickIntent(File f) { 828 Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE, null); 829 intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(f)); 830 return intent; 831 } 832 833 /** 834 * Sends a newly acquired photo to Gallery for cropping 835 */ 836 protected void doCropPhoto(File f) { 837 try { 838 // Add the image to the media store 839 MediaScannerConnection.scanFile( 840 mContext, 841 new String[] { f.getAbsolutePath() }, 842 new String[] { null }, 843 null); 844 845 // Launch gallery to crop the photo 846 final Intent intent = getCropImageIntent(Uri.fromFile(f)); 847 mStatus = Status.SUB_ACTIVITY; 848 startActivityForResult(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA); 849 } catch (Exception e) { 850 Log.e(TAG, "Cannot crop image", e); 851 Toast.makeText(mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show(); 852 } 853 } 854 855 /** 856 * Constructs an intent for image cropping. 857 */ 858 public Intent getCropImageIntent(Uri photoUri) { 859 Intent intent = new Intent("com.android.camera.action.CROP"); 860 intent.setDataAndType(photoUri, "image/*"); 861 intent.putExtra("crop", "true"); 862 intent.putExtra("aspectX", 1); 863 intent.putExtra("aspectY", 1); 864 intent.putExtra("outputX", mPhotoPickSize); 865 intent.putExtra("outputY", mPhotoPickSize); 866 intent.putExtra("return-data", true); 867 return intent; 868 } 869 870 /** 871 * Saves or creates the contact based on the mode, and if successful 872 * finishes the activity. 873 */ 874 public boolean save(int saveMode) { 875 if (!hasValidState() || mStatus != Status.EDITING) { 876 return false; 877 } 878 879 // If we are about to close the editor - there is no need to refresh the data 880 if (saveMode == SaveMode.CLOSE || saveMode == SaveMode.SPLIT) { 881 getLoaderManager().destroyLoader(LOADER_DATA); 882 } 883 884 mStatus = Status.SAVING; 885 886 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 887 if (!EntityModifier.hasChanges(mState, accountTypes)) { 888 onSaveCompleted(false, saveMode, mLookupUri); 889 return true; 890 } 891 892 setEnabled(false); 893 894 Intent intent = ContactSaveService.createSaveContactIntent(getActivity(), mState, 895 SAVE_MODE_EXTRA_KEY, saveMode, getActivity().getClass(), 896 ContactEditorActivity.ACTION_SAVE_COMPLETED); 897 getActivity().startService(intent); 898 return true; 899 } 900 901 public static class CancelEditDialogFragment extends DialogFragment { 902 903 public static void show(ContactEditorFragment fragment) { 904 CancelEditDialogFragment dialog = new CancelEditDialogFragment(); 905 dialog.setTargetFragment(fragment, 0); 906 dialog.show(fragment.getFragmentManager(), "cancelEditor"); 907 } 908 909 @Override 910 public Dialog onCreateDialog(Bundle savedInstanceState) { 911 AlertDialog dialog = new AlertDialog.Builder(getActivity()) 912 .setIconAttribute(android.R.attr.alertDialogIcon) 913 .setTitle(R.string.cancel_confirmation_dialog_title) 914 .setMessage(R.string.cancel_confirmation_dialog_message) 915 .setPositiveButton(R.string.discard, 916 new DialogInterface.OnClickListener() { 917 @Override 918 public void onClick(DialogInterface dialog, int whichButton) { 919 ((ContactEditorFragment)getTargetFragment()).doRevertAction(); 920 } 921 } 922 ) 923 .setNegativeButton(android.R.string.cancel, null) 924 .create(); 925 return dialog; 926 } 927 } 928 929 private boolean revert() { 930 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 931 if (mState == null || !EntityModifier.hasChanges(mState, accountTypes)) { 932 doRevertAction(); 933 } else { 934 CancelEditDialogFragment.show(this); 935 } 936 return true; 937 } 938 939 private void doRevertAction() { 940 // When this Fragment is closed we don't want it to auto-save 941 mStatus = Status.CLOSING; 942 if (mListener != null) mListener.onReverted(); 943 } 944 945 public void doSaveAction() { 946 save(SaveMode.CLOSE); 947 } 948 949 public void onJoinCompleted(Uri uri) { 950 onSaveCompleted(false, SaveMode.RELOAD, uri); 951 } 952 953 public void onSaveCompleted(boolean hadChanges, int saveMode, Uri contactLookupUri) { 954 boolean success = contactLookupUri != null; 955 Log.d(TAG, "onSaveCompleted(" + saveMode + ", " + contactLookupUri); 956 if (hadChanges) { 957 if (success) { 958 if (saveMode != SaveMode.JOIN) { 959 Toast.makeText(mContext, R.string.contactSavedToast, Toast.LENGTH_SHORT).show(); 960 } 961 } else { 962 Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show(); 963 } 964 } 965 switch (saveMode) { 966 case SaveMode.CLOSE: 967 case SaveMode.HOME: 968 final Intent resultIntent; 969 if (success && contactLookupUri != null) { 970 final String requestAuthority = 971 mLookupUri == null ? null : mLookupUri.getAuthority(); 972 973 final String legacyAuthority = "contacts"; 974 975 resultIntent = new Intent(); 976 resultIntent.setAction(Intent.ACTION_VIEW); 977 if (legacyAuthority.equals(requestAuthority)) { 978 // Build legacy Uri when requested by caller 979 final long contactId = ContentUris.parseId(Contacts.lookupContact( 980 mContext.getContentResolver(), contactLookupUri)); 981 final Uri legacyContentUri = Uri.parse("content://contacts/people"); 982 final Uri legacyUri = ContentUris.withAppendedId( 983 legacyContentUri, contactId); 984 resultIntent.setData(legacyUri); 985 } else { 986 // Otherwise pass back a lookup-style Uri 987 resultIntent.setData(contactLookupUri); 988 } 989 990 } else { 991 resultIntent = null; 992 } 993 // It is already saved, so prevent that it is saved again 994 mStatus = Status.CLOSING; 995 if (mListener != null) mListener.onSaveFinished(resultIntent); 996 break; 997 998 case SaveMode.RELOAD: 999 case SaveMode.JOIN: 1000 if (success && contactLookupUri != null) { 1001 // If it was a JOIN, we are now ready to bring up the join activity. 1002 if (saveMode == SaveMode.JOIN) { 1003 showJoinAggregateActivity(contactLookupUri); 1004 } 1005 1006 // If this was in INSERT, we are changing into an EDIT now. 1007 // If it already was an EDIT, we are changing to the new Uri now 1008 mState = null; 1009 load(Intent.ACTION_EDIT, contactLookupUri, null); 1010 mStatus = Status.LOADING; 1011 getLoaderManager().restartLoader(LOADER_DATA, null, mDataLoaderListener); 1012 } 1013 break; 1014 1015 case SaveMode.SPLIT: 1016 mStatus = Status.CLOSING; 1017 if (mListener != null) { 1018 mListener.onContactSplit(contactLookupUri); 1019 } else { 1020 Log.d(TAG, "No listener registered, can not call onSplitFinished"); 1021 } 1022 break; 1023 } 1024 } 1025 1026 /** 1027 * Shows a list of aggregates that can be joined into the currently viewed aggregate. 1028 * 1029 * @param contactLookupUri the fresh URI for the currently edited contact (after saving it) 1030 */ 1031 private void showJoinAggregateActivity(Uri contactLookupUri) { 1032 if (contactLookupUri == null || !isAdded()) { 1033 return; 1034 } 1035 1036 mContactIdForJoin = ContentUris.parseId(contactLookupUri); 1037 mContactWritableForJoin = isContactWritable(); 1038 final Intent intent = new Intent(JoinContactActivity.JOIN_CONTACT); 1039 intent.putExtra(JoinContactActivity.EXTRA_TARGET_CONTACT_ID, mContactIdForJoin); 1040 startActivityForResult(intent, REQUEST_CODE_JOIN); 1041 } 1042 1043 /** 1044 * Performs aggregation with the contact selected by the user from suggestions or A-Z list. 1045 */ 1046 private void joinAggregate(final long contactId) { 1047 Intent intent = ContactSaveService.createJoinContactsIntent(mContext, mContactIdForJoin, 1048 contactId, mContactWritableForJoin, 1049 ContactEditorActivity.class, ContactEditorActivity.ACTION_JOIN_COMPLETED); 1050 mContext.startService(intent); 1051 } 1052 1053 /** 1054 * Returns true if there is at least one writable raw contact in the current contact. 1055 */ 1056 private boolean isContactWritable() { 1057 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 1058 int size = mState.size(); 1059 for (int i = 0; i < size; i++) { 1060 ValuesDelta values = mState.get(i).getValues(); 1061 final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE); 1062 final AccountType type = accountTypes.getAccountType(accountType); 1063 if (!type.readOnly) { 1064 return true; 1065 } 1066 } 1067 return false; 1068 } 1069 1070 public static interface Listener { 1071 /** 1072 * Contact was not found, so somehow close this fragment. This is raised after a contact 1073 * is removed via Menu/Delete (unless it was a new contact) 1074 */ 1075 void onContactNotFound(); 1076 1077 /** 1078 * Contact was split, so we can close now. 1079 * @param newLookupUri The lookup uri of the new contact that should be shown to the user. 1080 * The editor tries best to chose the most natural contact here. 1081 */ 1082 void onContactSplit(Uri newLookupUri); 1083 1084 /** 1085 * User has tapped Revert, close the fragment now. 1086 */ 1087 void onReverted(); 1088 1089 /** 1090 * Set the Title (e.g. of the Activity) 1091 */ 1092 void setTitleTo(int resourceId); 1093 1094 /** 1095 * Contact was saved and the Fragment can now be closed safely. 1096 */ 1097 void onSaveFinished(Intent resultIntent); 1098 1099 /** 1100 * User switched to editing a different contact (a suggestion from the 1101 * aggregation engine). 1102 */ 1103 void onEditOtherContactRequested( 1104 Uri contactLookupUri, ArrayList<ContentValues> contentValues); 1105 1106 /** 1107 * Contact is being created for an external account that provides its own 1108 * new contact activity. 1109 */ 1110 void onCustomCreateContactActivityRequested(Account account, Bundle intentExtras); 1111 1112 /** 1113 * The edited raw contact belongs to an external account that provides 1114 * its own edit activity. 1115 * 1116 * @param redirect indicates that the current editor should be closed 1117 * before the custom editor is shown. 1118 */ 1119 void onCustomEditContactActivityRequested(Account account, Uri rawContactUri, 1120 Bundle intentExtras, boolean redirect); 1121 } 1122 1123 private class EntityDeltaComparator implements Comparator<EntityDelta> { 1124 /** 1125 * Compare EntityDeltas for sorting the stack of editors. 1126 */ 1127 @Override 1128 public int compare(EntityDelta one, EntityDelta two) { 1129 // Check direct equality 1130 if (one.equals(two)) { 1131 return 0; 1132 } 1133 1134 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 1135 String accountType2 = one.getValues().getAsString(RawContacts.ACCOUNT_TYPE); 1136 final AccountType type1 = accountTypes.getAccountType(accountType2); 1137 accountType2 = two.getValues().getAsString(RawContacts.ACCOUNT_TYPE); 1138 final AccountType type2 = accountTypes.getAccountType(accountType2); 1139 1140 // Check read-only 1141 if (type1.readOnly && !type2.readOnly) { 1142 return 1; 1143 } else if (!type1.readOnly && type2.readOnly) { 1144 return -1; 1145 } 1146 1147 // Check account type 1148 boolean skipAccountTypeCheck = false; 1149 boolean isGoogleAccount1 = type1 instanceof GoogleAccountType; 1150 boolean isGoogleAccount2 = type2 instanceof GoogleAccountType; 1151 if (isGoogleAccount1 && !isGoogleAccount2) { 1152 return -1; 1153 } else if (!isGoogleAccount1 && isGoogleAccount2) { 1154 return 1; 1155 } else if (isGoogleAccount1 && isGoogleAccount2){ 1156 skipAccountTypeCheck = true; 1157 } 1158 1159 int value; 1160 if (!skipAccountTypeCheck) { 1161 if (type1.accountType == null) { 1162 return 1; 1163 } 1164 value = type1.accountType.compareTo(type2.accountType); 1165 if (value != 0) { 1166 return value; 1167 } 1168 } 1169 1170 // Check account name 1171 ValuesDelta oneValues = one.getValues(); 1172 String oneAccount = oneValues.getAsString(RawContacts.ACCOUNT_NAME); 1173 if (oneAccount == null) oneAccount = ""; 1174 ValuesDelta twoValues = two.getValues(); 1175 String twoAccount = twoValues.getAsString(RawContacts.ACCOUNT_NAME); 1176 if (twoAccount == null) twoAccount = ""; 1177 value = oneAccount.compareTo(twoAccount); 1178 if (value != 0) { 1179 return value; 1180 } 1181 1182 // Both are in the same account, fall back to contact ID 1183 Long oneId = oneValues.getAsLong(RawContacts._ID); 1184 Long twoId = twoValues.getAsLong(RawContacts._ID); 1185 if (oneId == null) { 1186 return -1; 1187 } else if (twoId == null) { 1188 return 1; 1189 } 1190 1191 return (int)(oneId - twoId); 1192 } 1193 } 1194 1195 /** 1196 * Returns the contact ID for the currently edited contact or 0 if the contact is new. 1197 */ 1198 protected long getContactId() { 1199 if (mState != null) { 1200 for (EntityDelta rawContact : mState) { 1201 Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID); 1202 if (contactId != null) { 1203 return contactId; 1204 } 1205 } 1206 } 1207 return 0; 1208 } 1209 1210 /** 1211 * Triggers an asynchronous search for aggregation suggestions. 1212 */ 1213 public void acquireAggregationSuggestions(RawContactEditorView rawContactEditor) { 1214 long rawContactId = rawContactEditor.getRawContactId(); 1215 if (mAggregationSuggestionsRawContactId != rawContactId 1216 && mAggregationSuggestionView != null) { 1217 mAggregationSuggestionView.setVisibility(View.GONE); 1218 mAggregationSuggestionView = null; 1219 mAggregationSuggestionEngine.reset(); 1220 } 1221 1222 mAggregationSuggestionsRawContactId = rawContactId; 1223 1224 if (mAggregationSuggestionEngine == null) { 1225 mAggregationSuggestionEngine = new AggregationSuggestionEngine(getActivity()); 1226 mAggregationSuggestionEngine.setListener(this); 1227 mAggregationSuggestionEngine.start(); 1228 } 1229 1230 mAggregationSuggestionEngine.setContactId(getContactId()); 1231 1232 LabeledEditorView nameEditor = rawContactEditor.getNameEditor(); 1233 mAggregationSuggestionEngine.onNameChange(nameEditor.getValues()); 1234 } 1235 1236 @Override 1237 public void onAggregationSuggestionChange() { 1238 if (!isAdded() || mState == null || mStatus != Status.EDITING) { 1239 return; 1240 } 1241 1242 RawContactEditorView rawContactView = 1243 (RawContactEditorView)getRawContactEditorView(mAggregationSuggestionsRawContactId); 1244 if (rawContactView == null) { 1245 return; 1246 } 1247 1248 ViewStub stub = (ViewStub)rawContactView.findViewById(R.id.aggregation_suggestion_stub); 1249 if (stub != null) { 1250 stub.inflate(); 1251 } 1252 1253 // Only request the view on screen when it is first displayed 1254 boolean requestOnScreen = mAggregationSuggestionView == null; 1255 mAggregationSuggestionView = rawContactView.findViewById(R.id.aggregation_suggestion); 1256 1257 int count = mAggregationSuggestionEngine.getSuggestedContactCount(); 1258 if (count == 0) { 1259 mAggregationSuggestionView.setVisibility(View.GONE); 1260 return; 1261 } 1262 1263 List<Suggestion> suggestions = mAggregationSuggestionEngine.getSuggestions(); 1264 1265 LinearLayout itemList = (LinearLayout) mAggregationSuggestionView.findViewById( 1266 R.id.aggregation_suggestions); 1267 itemList.removeAllViews(); 1268 1269 LayoutInflater inflater = getActivity().getLayoutInflater(); 1270 1271 for (Suggestion suggestion : suggestions) { 1272 AggregationSuggestionView suggestionView = 1273 (AggregationSuggestionView) inflater.inflate( 1274 R.layout.aggregation_suggestions_item, null); 1275 suggestionView.setLayoutParams( 1276 new LinearLayout.LayoutParams( 1277 LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); 1278 suggestionView.setNewContact(mState.size() == 1 && mState.get(0).isContactInsert()); 1279 suggestionView.setListener(this); 1280 suggestionView.bindSuggestion(suggestion); 1281 itemList.addView(suggestionView); 1282 } 1283 1284 adjustAggregationSuggestionViewLayout(rawContactView); 1285 setAggregationSuggestionViewEnabled(mEnabled); 1286 mAggregationSuggestionView.setVisibility(View.VISIBLE); 1287 1288 if (requestOnScreen) { 1289 mContent.postDelayed(new Runnable() { 1290 1291 @Override 1292 public void run() { 1293 requestAggregationSuggestionOnScreen(mAggregationSuggestionView); 1294 } 1295 }, AGGREGATION_SUGGESTION_SCROLL_DELAY); 1296 } 1297 } 1298 1299 /** 1300 * Adjusts the layout of the aggregation suggestion view so that it is placed directly 1301 * underneath and have the same width as the last text editor of the contact name editor. 1302 */ 1303 private void adjustAggregationSuggestionViewLayout(RawContactEditorView rawContactView) { 1304 TextFieldsEditorView nameEditor = rawContactView.getNameEditor(); 1305 Rect rect = new Rect(); 1306 nameEditor.acquireEditorBounds(rect); 1307 MarginLayoutParams layoutParams = 1308 (MarginLayoutParams) mAggregationSuggestionView.getLayoutParams(); 1309 layoutParams.leftMargin = rect.left; 1310 layoutParams.width = rect.width(); 1311 mAggregationSuggestionView.setLayoutParams(layoutParams); 1312 } 1313 1314 @Override 1315 public void onJoinAction(long contactId, List<Long> rawContactIdList) { 1316 long rawContactIds[] = new long[rawContactIdList.size()]; 1317 for (int i = 0; i < rawContactIds.length; i++) { 1318 rawContactIds[i] = rawContactIdList.get(i); 1319 } 1320 JoinSuggestedContactDialogFragment dialog = 1321 new JoinSuggestedContactDialogFragment(); 1322 Bundle args = new Bundle(); 1323 args.putLongArray("rawContactIds", rawContactIds); 1324 dialog.setArguments(args); 1325 dialog.setTargetFragment(this, 0); 1326 try { 1327 dialog.show(getFragmentManager(), "join"); 1328 } catch (Exception ex) { 1329 // No problem - the activity is no longer available to display the dialog 1330 } 1331 } 1332 1333 public static class JoinSuggestedContactDialogFragment extends DialogFragment { 1334 1335 @Override 1336 public Dialog onCreateDialog(Bundle savedInstanceState) { 1337 return new AlertDialog.Builder(getActivity()) 1338 .setIconAttribute(android.R.attr.alertDialogIcon) 1339 .setTitle(R.string.aggregation_suggestion_join_dialog_title) 1340 .setMessage(R.string.aggregation_suggestion_join_dialog_message) 1341 .setPositiveButton(android.R.string.yes, 1342 new DialogInterface.OnClickListener() { 1343 public void onClick(DialogInterface dialog, int whichButton) { 1344 ContactEditorFragment targetFragment = 1345 (ContactEditorFragment) getTargetFragment(); 1346 long rawContactIds[] = 1347 getArguments().getLongArray("rawContactIds"); 1348 targetFragment.doJoinSuggestedContact(rawContactIds); 1349 } 1350 } 1351 ) 1352 .setNegativeButton(android.R.string.no, null) 1353 .create(); 1354 } 1355 } 1356 1357 /** 1358 * Joins the suggested contact (specified by the id's of constituent raw 1359 * contacts), save all changes, and stay in the editor. 1360 */ 1361 protected void doJoinSuggestedContact(long[] rawContactIds) { 1362 if (!hasValidState() || mStatus != Status.EDITING) { 1363 return; 1364 } 1365 1366 mState.setJoinWithRawContacts(rawContactIds); 1367 save(SaveMode.RELOAD); 1368 } 1369 1370 @Override 1371 public void onEditAction(Uri contactLookupUri) { 1372 SuggestionEditConfirmationDialogFragment dialog = 1373 new SuggestionEditConfirmationDialogFragment(); 1374 Bundle args = new Bundle(); 1375 args.putParcelable("contactUri", contactLookupUri); 1376 dialog.setArguments(args); 1377 dialog.setTargetFragment(this, 0); 1378 dialog.show(getFragmentManager(), "edit"); 1379 } 1380 1381 public static class SuggestionEditConfirmationDialogFragment extends DialogFragment { 1382 1383 @Override 1384 public Dialog onCreateDialog(Bundle savedInstanceState) { 1385 return new AlertDialog.Builder(getActivity()) 1386 .setIconAttribute(android.R.attr.alertDialogIcon) 1387 .setTitle(R.string.aggregation_suggestion_edit_dialog_title) 1388 .setMessage(R.string.aggregation_suggestion_edit_dialog_message) 1389 .setPositiveButton(android.R.string.yes, 1390 new DialogInterface.OnClickListener() { 1391 public void onClick(DialogInterface dialog, int whichButton) { 1392 ContactEditorFragment targetFragment = 1393 (ContactEditorFragment) getTargetFragment(); 1394 Uri contactUri = 1395 getArguments().getParcelable("contactUri"); 1396 targetFragment.doEditSuggestedContact(contactUri); 1397 } 1398 } 1399 ) 1400 .setNegativeButton(android.R.string.no, null) 1401 .create(); 1402 } 1403 } 1404 1405 /** 1406 * Abandons the currently edited contact and switches to editing the suggested 1407 * one, transferring all the data there 1408 */ 1409 protected void doEditSuggestedContact(Uri contactUri) { 1410 if (mListener != null) { 1411 // make sure we don't save this contact when closing down 1412 mStatus = Status.CLOSING; 1413 mListener.onEditOtherContactRequested( 1414 contactUri, mState.get(0).getContentValues()); 1415 } 1416 } 1417 1418 /** 1419 * Scrolls the editor if necessary to reveal the aggregation suggestion that is 1420 * shown below the name editor. Makes sure that the currently focused field 1421 * remains visible. 1422 */ 1423 private void requestAggregationSuggestionOnScreen(final View view) { 1424 Rect rect = getRelativeBounds(mContent, view); 1425 View focused = mContent.findFocus(); 1426 if (focused != null) { 1427 rect.union(getRelativeBounds(mContent, focused)); 1428 } 1429 mContent.requestRectangleOnScreen(rect); 1430 } 1431 1432 public void setAggregationSuggestionViewEnabled(boolean enabled) { 1433 if (mAggregationSuggestionView == null) { 1434 return; 1435 } 1436 1437 LinearLayout itemList = (LinearLayout) mAggregationSuggestionView.findViewById( 1438 R.id.aggregation_suggestions); 1439 int count = itemList.getChildCount(); 1440 for (int i = 0; i < count; i++) { 1441 itemList.getChildAt(i).setEnabled(enabled); 1442 } 1443 } 1444 1445 /** 1446 * Computes bounds of the supplied view relative to its ascendant. 1447 */ 1448 private Rect getRelativeBounds(View ascendant, View view) { 1449 Rect rect = new Rect(); 1450 rect.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()); 1451 1452 View parent = (View) view.getParent(); 1453 while (parent != ascendant) { 1454 rect.offset(parent.getLeft(), parent.getTop()); 1455 parent = (View) parent.getParent(); 1456 } 1457 return rect; 1458 } 1459 1460 @Override 1461 public void onSaveInstanceState(Bundle outState) { 1462 outState.putParcelable(KEY_URI, mLookupUri); 1463 outState.putString(KEY_ACTION, mAction); 1464 1465 if (hasValidState()) { 1466 // Store entities with modifications 1467 outState.putParcelable(KEY_EDIT_STATE, mState); 1468 } 1469 1470 outState.putLong(KEY_RAW_CONTACT_ID_REQUESTING_PHOTO, mRawContactIdRequestingPhoto); 1471 outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator); 1472 if (mCurrentPhotoFile != null) { 1473 outState.putString(KEY_CURRENT_PHOTO_FILE, mCurrentPhotoFile.toString()); 1474 } 1475 outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin); 1476 outState.putBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN, mContactWritableForJoin); 1477 outState.putLong(KEY_SHOW_JOIN_SUGGESTIONS, mAggregationSuggestionsRawContactId); 1478 outState.putBoolean(KEY_ENABLED, mEnabled); 1479 outState.putInt(KEY_STATUS, mStatus); 1480 super.onSaveInstanceState(outState); 1481 } 1482 1483 @Override 1484 public void onActivityResult(int requestCode, int resultCode, Intent data) { 1485 if (mStatus == Status.SUB_ACTIVITY) { 1486 mStatus = Status.EDITING; 1487 } 1488 1489 // Ignore failed requests 1490 if (resultCode != Activity.RESULT_OK) return; 1491 switch (requestCode) { 1492 case REQUEST_CODE_PHOTO_PICKED_WITH_DATA: { 1493 // As we are coming back to this view, the editor will be reloaded automatically, 1494 // which will cause the photo that is set here to disappear. To prevent this, 1495 // we remember to set a flag which is interpreted after loading. 1496 // This photo is set here already to reduce flickering. 1497 mPhoto = data.getParcelableExtra("data"); 1498 setPhoto(mRawContactIdRequestingPhoto, mPhoto); 1499 mRawContactIdRequestingPhotoAfterLoad = mRawContactIdRequestingPhoto; 1500 mRawContactIdRequestingPhoto = -1; 1501 1502 break; 1503 } 1504 case REQUEST_CODE_CAMERA_WITH_DATA: { 1505 doCropPhoto(mCurrentPhotoFile); 1506 break; 1507 } 1508 case REQUEST_CODE_JOIN: { 1509 if (data != null) { 1510 final long contactId = ContentUris.parseId(data.getData()); 1511 joinAggregate(contactId); 1512 } 1513 break; 1514 } 1515 } 1516 } 1517 1518 /** 1519 * Sets the photo stored in mPhoto and writes it to the RawContact with the given id 1520 */ 1521 private void setPhoto(long rawContact, Bitmap photo) { 1522 BaseRawContactEditorView requestingEditor = getRawContactEditorView(rawContact); 1523 if (requestingEditor != null) { 1524 requestingEditor.setPhotoBitmap(photo); 1525 } else { 1526 Log.w(TAG, "The contact that requested the photo is no longer present."); 1527 } 1528 } 1529 1530 /** 1531 * Finds raw contact editor view for the given rawContactId. 1532 */ 1533 public BaseRawContactEditorView getRawContactEditorView(long rawContactId) { 1534 for (int i = 0; i < mContent.getChildCount(); i++) { 1535 final View childView = mContent.getChildAt(i); 1536 if (childView instanceof BaseRawContactEditorView) { 1537 final BaseRawContactEditorView editor = (BaseRawContactEditorView) childView; 1538 if (editor.getRawContactId() == rawContactId) { 1539 return editor; 1540 } 1541 } 1542 } 1543 return null; 1544 } 1545 1546 /** 1547 * Returns true if there is currently more than one photo on screen. 1548 */ 1549 private boolean hasMoreThanOnePhoto() { 1550 int count = mContent.getChildCount(); 1551 int countWithPicture = 0; 1552 for (int i = 0; i < count; i++) { 1553 final View childView = mContent.getChildAt(i); 1554 if (childView instanceof BaseRawContactEditorView) { 1555 final BaseRawContactEditorView editor = (BaseRawContactEditorView) childView; 1556 if (editor.hasSetPhoto()) { 1557 countWithPicture++; 1558 if (countWithPicture > 1) return true; 1559 } 1560 } 1561 } 1562 1563 return false; 1564 } 1565 1566 /** 1567 * The listener for the data loader 1568 */ 1569 private final LoaderManager.LoaderCallbacks<ContactLoader.Result> mDataLoaderListener = 1570 new LoaderCallbacks<ContactLoader.Result>() { 1571 @Override 1572 public Loader<ContactLoader.Result> onCreateLoader(int id, Bundle args) { 1573 mLoaderStartTime = SystemClock.elapsedRealtime(); 1574 return new ContactLoader(mContext, mLookupUri); 1575 } 1576 1577 @Override 1578 public void onLoadFinished(Loader<ContactLoader.Result> loader, ContactLoader.Result data) { 1579 final long loaderCurrentTime = SystemClock.elapsedRealtime(); 1580 Log.v(TAG, "Time needed for loading: " + (loaderCurrentTime-mLoaderStartTime)); 1581 if (data == ContactLoader.Result.NOT_FOUND || data == ContactLoader.Result.ERROR) { 1582 // Item has been deleted 1583 Log.i(TAG, "No contact found. Closing activity"); 1584 if (mListener != null) mListener.onContactNotFound(); 1585 return; 1586 } 1587 1588 mStatus = Status.EDITING; 1589 mLookupUri = data.getLookupUri(); 1590 final long setDataStartTime = SystemClock.elapsedRealtime(); 1591 setData(data); 1592 final long setDataEndTime = SystemClock.elapsedRealtime(); 1593 1594 // If we are coming back from the photo trimmer, this will be set. 1595 if (mRawContactIdRequestingPhotoAfterLoad != -1) { 1596 setPhoto(mRawContactIdRequestingPhotoAfterLoad, mPhoto); 1597 mRawContactIdRequestingPhotoAfterLoad = -1; 1598 mPhoto = null; 1599 } 1600 Log.v(TAG, "Time needed for setting UI: " + (setDataEndTime-setDataStartTime)); 1601 } 1602 1603 @Override 1604 public void onLoaderReset(Loader<ContactLoader.Result> loader) { 1605 } 1606 }; 1607 1608 /** 1609 * The listener for the group meta data loader for all groups. 1610 */ 1611 private final LoaderManager.LoaderCallbacks<Cursor> mGroupLoaderListener = 1612 new LoaderCallbacks<Cursor>() { 1613 1614 @Override 1615 public CursorLoader onCreateLoader(int id, Bundle args) { 1616 return new GroupMetaDataLoader(mContext, Groups.CONTENT_URI); 1617 } 1618 1619 @Override 1620 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 1621 mGroupMetaData = data; 1622 bindGroupMetaData(); 1623 } 1624 1625 public void onLoaderReset(Loader<Cursor> loader) { 1626 } 1627 }; 1628 1629 @Override 1630 public void onSplitContactConfirmed() { 1631 mState.markRawContactsForSplitting(); 1632 save(SaveMode.SPLIT); 1633 } 1634 1635 private final class PhotoEditorListener 1636 implements EditorListener, PhotoActionPopup.Listener { 1637 private final BaseRawContactEditorView mEditor; 1638 private final boolean mAccountReadOnly; 1639 1640 private PhotoEditorListener(BaseRawContactEditorView editor, boolean accountReadOnly) { 1641 mEditor = editor; 1642 mAccountReadOnly = accountReadOnly; 1643 } 1644 1645 @Override 1646 public void onRequest(int request) { 1647 if (!hasValidState()) return; 1648 1649 if (request == EditorListener.REQUEST_PICK_PHOTO) { 1650 // Determine mode 1651 final int mode; 1652 if (mAccountReadOnly) { 1653 if (mEditor.hasSetPhoto() && hasMoreThanOnePhoto()) { 1654 mode = PhotoActionPopup.MODE_READ_ONLY_ALLOW_PRIMARY; 1655 } else { 1656 // Read-only and either no photo or the only photo ==> no options 1657 return; 1658 } 1659 } else { 1660 if (mEditor.hasSetPhoto()) { 1661 if (hasMoreThanOnePhoto()) { 1662 mode = PhotoActionPopup.MODE_PHOTO_ALLOW_PRIMARY; 1663 } else { 1664 mode = PhotoActionPopup.MODE_PHOTO_DISALLOW_PRIMARY; 1665 } 1666 } else { 1667 mode = PhotoActionPopup.MODE_NO_PHOTO; 1668 } 1669 } 1670 PhotoActionPopup.createPopupMenu(mContext, mEditor.getPhotoEditor(), this, mode) 1671 .show(); 1672 } 1673 } 1674 1675 @Override 1676 public void onDeleteRequested(Editor removedEditor) { 1677 // The picture cannot be deleted, it can only be removed, which is handled by 1678 // onRemovePictureChosen() 1679 } 1680 1681 /** 1682 * User has chosen to set the selected photo as the (super) primary photo 1683 */ 1684 @Override 1685 public void onUseAsPrimaryChosen() { 1686 // Set the IsSuperPrimary for each editor 1687 int count = mContent.getChildCount(); 1688 for (int i = 0; i < count; i++) { 1689 final View childView = mContent.getChildAt(i); 1690 if (childView instanceof BaseRawContactEditorView) { 1691 final BaseRawContactEditorView editor = (BaseRawContactEditorView) childView; 1692 final PhotoEditorView photoEditor = editor.getPhotoEditor(); 1693 photoEditor.setSuperPrimary(editor == mEditor); 1694 } 1695 } 1696 } 1697 1698 /** 1699 * User has chosen to remove a picture 1700 */ 1701 @Override 1702 public void onRemovePictureChosen() { 1703 mEditor.setPhotoBitmap(null); 1704 } 1705 1706 /** 1707 * Launches Camera to take a picture and store it in a file. 1708 */ 1709 @Override 1710 public void onTakePhotoChosen() { 1711 mRawContactIdRequestingPhoto = mEditor.getRawContactId(); 1712 try { 1713 // Launch camera to take photo for selected contact 1714 PHOTO_DIR.mkdirs(); 1715 mCurrentPhotoFile = new File(PHOTO_DIR, getPhotoFileName()); 1716 final Intent intent = getTakePickIntent(mCurrentPhotoFile); 1717 1718 mStatus = Status.SUB_ACTIVITY; 1719 startActivityForResult(intent, REQUEST_CODE_CAMERA_WITH_DATA); 1720 } catch (ActivityNotFoundException e) { 1721 Toast.makeText(mContext, R.string.photoPickerNotFoundText, 1722 Toast.LENGTH_LONG).show(); 1723 } 1724 } 1725 1726 /** 1727 * Launches Gallery to pick a photo. 1728 */ 1729 @Override 1730 public void onPickFromGalleryChosen() { 1731 mRawContactIdRequestingPhoto = mEditor.getRawContactId(); 1732 try { 1733 // Launch picker to choose photo for selected contact 1734 final Intent intent = getPhotoPickIntent(); 1735 mStatus = Status.SUB_ACTIVITY; 1736 startActivityForResult(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA); 1737 } catch (ActivityNotFoundException e) { 1738 Toast.makeText(mContext, R.string.photoPickerNotFoundText, 1739 Toast.LENGTH_LONG).show(); 1740 } 1741 } 1742 } 1743} 1744