/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.contacts.editor; import android.accounts.Account; import android.app.Activity; import android.app.Fragment; import android.app.LoaderManager; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.CursorLoader; import android.content.Intent; import android.content.Loader; import android.database.Cursor; import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.os.SystemClock; import android.provider.ContactsContract; import android.provider.ContactsContract.CommonDataKinds.Email; import android.provider.ContactsContract.CommonDataKinds.Event; import android.provider.ContactsContract.CommonDataKinds.Organization; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.CommonDataKinds.StructuredName; import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; import android.provider.ContactsContract.Intents; import android.provider.ContactsContract.RawContacts; import android.support.v7.widget.Toolbar; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.LinearLayout; import android.widget.ListPopupWindow; import android.widget.Toast; import com.android.contacts.ContactSaveService; import com.android.contacts.GroupMetaDataLoader; import com.android.contacts.R; import com.android.contacts.activities.ContactEditorAccountsChangedActivity; import com.android.contacts.activities.ContactEditorActivity; import com.android.contacts.activities.ContactEditorActivity.ContactEditor; import com.android.contacts.activities.ContactSelectionActivity; import com.android.contacts.activities.RequestPermissionsActivity; import com.android.contacts.editor.AggregationSuggestionEngine.Suggestion; import com.android.contacts.group.GroupUtil; import com.android.contacts.list.UiIntentActions; import com.android.contacts.logging.ScreenEvent.ScreenType; import com.android.contacts.model.AccountTypeManager; import com.android.contacts.model.Contact; import com.android.contacts.model.ContactLoader; import com.android.contacts.model.RawContact; import com.android.contacts.model.RawContactDelta; import com.android.contacts.model.RawContactDeltaList; import com.android.contacts.model.RawContactModifier; import com.android.contacts.model.ValuesDelta; import com.android.contacts.model.account.AccountInfo; import com.android.contacts.model.account.AccountType; import com.android.contacts.model.account.AccountWithDataSet; import com.android.contacts.model.account.AccountsLoader; import com.android.contacts.preference.ContactsPreferences; import com.android.contacts.quickcontact.InvisibleContactUtil; import com.android.contacts.quickcontact.QuickContactActivity; import com.android.contacts.util.ContactDisplayUtils; import com.android.contacts.util.ContactPhotoUtils; import com.android.contacts.util.ImplicitIntentsUtil; import com.android.contacts.util.MaterialColorMapUtils; import com.android.contacts.util.UiClosables; import com.android.contactsbind.HelpUtils; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; /** * Contact editor with only the most important fields displayed initially. */ public class ContactEditorFragment extends Fragment implements ContactEditor, SplitContactConfirmationDialogFragment.Listener, JoinContactConfirmationDialogFragment.Listener, AggregationSuggestionEngine.Listener, AggregationSuggestionView.Listener, CancelEditDialogFragment.Listener, RawContactEditorView.Listener, PhotoEditorView.Listener, AccountsLoader.AccountsListener { static final String TAG = "ContactEditor"; private static final int LOADER_CONTACT = 1; private static final int LOADER_GROUPS = 2; private static final int LOADER_ACCOUNTS = 3; private static final String KEY_PHOTO_RAW_CONTACT_ID = "photo_raw_contact_id"; private static final String KEY_UPDATED_PHOTOS = "updated_photos"; private static final List VALID_INTENT_ACTIONS = new ArrayList() {{ add(Intent.ACTION_EDIT); add(Intent.ACTION_INSERT); add(ContactEditorActivity.ACTION_SAVE_COMPLETED); }}; private static final String KEY_ACTION = "action"; private static final String KEY_URI = "uri"; private static final String KEY_AUTO_ADD_TO_DEFAULT_GROUP = "autoAddToDefaultGroup"; private static final String KEY_DISABLE_DELETE_MENU_OPTION = "disableDeleteMenuOption"; private static final String KEY_NEW_LOCAL_PROFILE = "newLocalProfile"; private static final String KEY_MATERIAL_PALETTE = "materialPalette"; private static final String KEY_ACCOUNT = "saveToAccount"; private static final String KEY_VIEW_ID_GENERATOR = "viewidgenerator"; private static final String KEY_RAW_CONTACTS = "rawContacts"; private static final String KEY_EDIT_STATE = "state"; private static final String KEY_STATUS = "status"; private static final String KEY_HAS_NEW_CONTACT = "hasNewContact"; private static final String KEY_NEW_CONTACT_READY = "newContactDataReady"; private static final String KEY_IS_EDIT = "isEdit"; private static final String KEY_EXISTING_CONTACT_READY = "existingContactDataReady"; private static final String KEY_IS_USER_PROFILE = "isUserProfile"; private static final String KEY_ENABLED = "enabled"; // Aggregation PopupWindow private static final String KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID = "aggregationSuggestionsRawContactId"; // Join Activity private static final String KEY_CONTACT_ID_FOR_JOIN = "contactidforjoin"; private static final String KEY_READ_ONLY_DISPLAY_NAME_ID = "readOnlyDisplayNameId"; private static final String KEY_COPY_READ_ONLY_DISPLAY_NAME = "copyReadOnlyDisplayName"; protected static final int REQUEST_CODE_JOIN = 0; protected static final int REQUEST_CODE_ACCOUNTS_CHANGED = 1; /** * An intent extra that forces the editor to add the edited contact * to the default group (e.g. "My Contacts"). */ public static final String INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY = "addToDefaultDirectory"; public static final String INTENT_EXTRA_NEW_LOCAL_PROFILE = "newLocalProfile"; public static final String INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION = "disableDeleteMenuOption"; /** * Intent key to pass the photo palette primary color calculated by * {@link com.android.contacts.quickcontact.QuickContactActivity} to the editor. */ public static final String INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR = "material_palette_primary_color"; /** * Intent key to pass the photo palette secondary color calculated by * {@link com.android.contacts.quickcontact.QuickContactActivity} to the editor. */ public static final String INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR = "material_palette_secondary_color"; /** * Intent key to pass the ID of the photo to display on the editor. */ // TODO: This can be cleaned up if we decide to not pass the photo id through // QuickContactActivity. public static final String INTENT_EXTRA_PHOTO_ID = "photo_id"; /** * Intent key to pass the ID of the raw contact id that should be displayed in the full editor * by itself. */ public static final String INTENT_EXTRA_RAW_CONTACT_ID_TO_DISPLAY_ALONE = "raw_contact_id_to_display_alone"; /** * Intent extra to specify a {@link ContactEditor.SaveMode}. */ public static final String SAVE_MODE_EXTRA_KEY = "saveMode"; /** * Intent extra key for the contact ID to join the current contact to after saving. */ public static final String JOIN_CONTACT_ID_EXTRA_KEY = "joinContactId"; /** * Callbacks for Activities that host contact editors Fragments. */ public interface Listener { /** * Contact was not found, so somehow close this fragment. This is raised after a contact * is removed via Menu/Delete */ void onContactNotFound(); /** * Contact was split, so we can close now. * * @param newLookupUri The lookup uri of the new contact that should be shown to the user. * The editor tries best to chose the most natural contact here. */ void onContactSplit(Uri newLookupUri); /** * User has tapped Revert, close the fragment now. */ void onReverted(); /** * Contact was saved and the Fragment can now be closed safely. */ void onSaveFinished(Intent resultIntent); /** * User switched to editing a different raw contact (a suggestion from the * aggregation engine). */ void onEditOtherRawContactRequested(Uri contactLookupUri, long rawContactId, ArrayList contentValues); /** * User has requested that contact be deleted. */ void onDeleteRequested(Uri contactUri); } /** * Adapter for aggregation suggestions displayed in a PopupWindow when * editor fields change. */ private static final class AggregationSuggestionAdapter extends BaseAdapter { private final LayoutInflater mLayoutInflater; private final AggregationSuggestionView.Listener mListener; private final List mSuggestions; public AggregationSuggestionAdapter(Activity activity, AggregationSuggestionView.Listener listener, List suggestions) { mLayoutInflater = activity.getLayoutInflater(); mListener = listener; mSuggestions = suggestions; } @Override public View getView(int position, View convertView, ViewGroup parent) { final Suggestion suggestion = (Suggestion) getItem(position); final AggregationSuggestionView suggestionView = (AggregationSuggestionView) mLayoutInflater.inflate( R.layout.aggregation_suggestions_item, null); suggestionView.setListener(mListener); suggestionView.bindSuggestion(suggestion); return suggestionView; } @Override public long getItemId(int position) { return position; } @Override public Object getItem(int position) { return mSuggestions.get(position); } @Override public int getCount() { return mSuggestions.size(); } } protected Context mContext; protected Listener mListener; // // Views // protected LinearLayout mContent; protected ListPopupWindow mAggregationSuggestionPopup; // // Parameters passed in on {@link #load} // protected String mAction; protected Uri mLookupUri; protected Bundle mIntentExtras; protected boolean mAutoAddToDefaultGroup; protected boolean mDisableDeleteMenuOption; protected boolean mNewLocalProfile; protected MaterialColorMapUtils.MaterialPalette mMaterialPalette; // // Helpers // protected ContactEditorUtils mEditorUtils; protected RawContactDeltaComparator mComparator; protected ViewIdGenerator mViewIdGenerator; private AggregationSuggestionEngine mAggregationSuggestionEngine; // // Loaded data // // Used to store existing contact data so it can be re-applied during a rebind call, // i.e. account switch. protected Contact mContact; protected ImmutableList mRawContacts; protected Cursor mGroupMetaData; // // Editor state // protected RawContactDeltaList mState; protected int mStatus; protected long mRawContactIdToDisplayAlone = -1; // Whether to show the new contact blank form and if it's corresponding delta is ready. protected boolean mHasNewContact; protected AccountWithDataSet mAccountWithDataSet; protected List mWritableAccounts = Collections.emptyList(); protected boolean mNewContactDataReady; protected boolean mNewContactAccountChanged; // Whether it's an edit of existing contact and if it's corresponding delta is ready. protected boolean mIsEdit; protected boolean mExistingContactDataReady; // Whether we are editing the "me" profile protected boolean mIsUserProfile; // Whether editor views and options menu items should be enabled private boolean mEnabled = true; // Aggregation PopupWindow private long mAggregationSuggestionsRawContactId; // Join Activity protected long mContactIdForJoin; // Used to pre-populate the editor with a display name when a user edits a read-only contact. protected long mReadOnlyDisplayNameId; protected boolean mCopyReadOnlyName; /** * The contact data loader listener. */ protected final LoaderManager.LoaderCallbacks mContactLoaderListener = new LoaderManager.LoaderCallbacks() { protected long mLoaderStartTime; @Override public Loader onCreateLoader(int id, Bundle args) { mLoaderStartTime = SystemClock.elapsedRealtime(); return new ContactLoader(mContext, mLookupUri, /* postViewNotification */ true, /* loadGroupMetaData */ true); } @Override public void onLoadFinished(Loader loader, Contact contact) { final long loaderCurrentTime = SystemClock.elapsedRealtime(); if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Time needed for loading: " + (loaderCurrentTime-mLoaderStartTime)); } if (!contact.isLoaded()) { // Item has been deleted. Close activity without saving again. Log.i(TAG, "No contact found. Closing activity"); mStatus = Status.CLOSING; if (mListener != null) mListener.onContactNotFound(); return; } mStatus = Status.EDITING; mLookupUri = contact.getLookupUri(); final long setDataStartTime = SystemClock.elapsedRealtime(); setState(contact); final long setDataEndTime = SystemClock.elapsedRealtime(); if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Time needed for setting UI: " + (setDataEndTime - setDataStartTime)); } } @Override public void onLoaderReset(Loader loader) { } }; /** * The groups meta data loader listener. */ protected final LoaderManager.LoaderCallbacks mGroupsLoaderListener = new LoaderManager.LoaderCallbacks() { @Override public CursorLoader onCreateLoader(int id, Bundle args) { return new GroupMetaDataLoader(mContext, ContactsContract.Groups.CONTENT_URI, GroupUtil.ALL_GROUPS_SELECTION); } @Override public void onLoadFinished(Loader loader, Cursor data) { mGroupMetaData = data; setGroupMetaData(); } @Override public void onLoaderReset(Loader loader) { } }; private long mPhotoRawContactId; private Bundle mUpdatedPhotos = new Bundle(); @Override public Context getContext() { return getActivity(); } @Override public void onAttach(Activity activity) { super.onAttach(activity); mContext = activity; mEditorUtils = ContactEditorUtils.create(mContext); mComparator = new RawContactDeltaComparator(mContext); } @Override public void onCreate(Bundle savedState) { if (savedState != null) { // Restore mUri before calling super.onCreate so that onInitializeLoaders // would already have a uri and an action to work with mAction = savedState.getString(KEY_ACTION); mLookupUri = savedState.getParcelable(KEY_URI); } super.onCreate(savedState); if (savedState == null) { mViewIdGenerator = new ViewIdGenerator(); // mState can still be null because it may not have have finished loading before // onSaveInstanceState was called. mState = new RawContactDeltaList(); } else { mViewIdGenerator = savedState.getParcelable(KEY_VIEW_ID_GENERATOR); mAutoAddToDefaultGroup = savedState.getBoolean(KEY_AUTO_ADD_TO_DEFAULT_GROUP); mDisableDeleteMenuOption = savedState.getBoolean(KEY_DISABLE_DELETE_MENU_OPTION); mNewLocalProfile = savedState.getBoolean(KEY_NEW_LOCAL_PROFILE); mMaterialPalette = savedState.getParcelable(KEY_MATERIAL_PALETTE); mAccountWithDataSet = savedState.getParcelable(KEY_ACCOUNT); mRawContacts = ImmutableList.copyOf(savedState.getParcelableArrayList( KEY_RAW_CONTACTS)); // NOTE: mGroupMetaData is not saved/restored // Read state from savedState. No loading involved here mState = savedState. getParcelable(KEY_EDIT_STATE); mStatus = savedState.getInt(KEY_STATUS); mHasNewContact = savedState.getBoolean(KEY_HAS_NEW_CONTACT); mNewContactDataReady = savedState.getBoolean(KEY_NEW_CONTACT_READY); mIsEdit = savedState.getBoolean(KEY_IS_EDIT); mExistingContactDataReady = savedState.getBoolean(KEY_EXISTING_CONTACT_READY); mIsUserProfile = savedState.getBoolean(KEY_IS_USER_PROFILE); mEnabled = savedState.getBoolean(KEY_ENABLED); // Aggregation PopupWindow mAggregationSuggestionsRawContactId = savedState.getLong( KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID); // Join Activity mContactIdForJoin = savedState.getLong(KEY_CONTACT_ID_FOR_JOIN); mReadOnlyDisplayNameId = savedState.getLong(KEY_READ_ONLY_DISPLAY_NAME_ID); mCopyReadOnlyName = savedState.getBoolean(KEY_COPY_READ_ONLY_DISPLAY_NAME, false); mPhotoRawContactId = savedState.getLong(KEY_PHOTO_RAW_CONTACT_ID); mUpdatedPhotos = savedState.getParcelable(KEY_UPDATED_PHOTOS); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { setHasOptionsMenu(true); final View view = inflater.inflate( R.layout.contact_editor_fragment, container, false); mContent = (LinearLayout) view.findViewById(R.id.raw_contacts_editor_view); return view; } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); validateAction(mAction); if (mState.isEmpty()) { // The delta list may not have finished loading before orientation change happens. // In this case, there will be a saved state but deltas will be missing. Reload from // database. if (Intent.ACTION_EDIT.equals(mAction)) { // Either // 1) orientation change but load never finished. // 2) not an orientation change so data needs to be loaded for first time. getLoaderManager().initLoader(LOADER_CONTACT, null, mContactLoaderListener); getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupsLoaderListener); } } else { // Orientation change, we already have mState, it was loaded by onCreate bindEditors(); } // Handle initial actions only when existing state missing if (savedInstanceState == null) { if (mIntentExtras != null) { final Account account = mIntentExtras == null ? null : (Account) mIntentExtras.getParcelable(Intents.Insert.EXTRA_ACCOUNT); final String dataSet = mIntentExtras == null ? null : mIntentExtras.getString(Intents.Insert.EXTRA_DATA_SET); mAccountWithDataSet = account != null ? new AccountWithDataSet(account.name, account.type, dataSet) : mIntentExtras.getParcelable( ContactEditorActivity.EXTRA_ACCOUNT_WITH_DATA_SET); } if (Intent.ACTION_EDIT.equals(mAction)) { mIsEdit = true; } else if (Intent.ACTION_INSERT.equals(mAction)) { mHasNewContact = true; if (mAccountWithDataSet != null) { createContact(mAccountWithDataSet); } // else wait for accounts to be loaded } } if (mHasNewContact) { AccountsLoader.loadAccounts(this, LOADER_ACCOUNTS, AccountTypeManager.writableFilter()); } } /** * Checks if the requested action is valid. * * @param action The action to test. * @throws IllegalArgumentException when the action is invalid. */ private static void validateAction(String action) { if (VALID_INTENT_ACTIONS.contains(action)) { return; } throw new IllegalArgumentException( "Unknown action " + action + "; Supported actions: " + VALID_INTENT_ACTIONS); } @Override public void onSaveInstanceState(Bundle outState) { outState.putString(KEY_ACTION, mAction); outState.putParcelable(KEY_URI, mLookupUri); outState.putBoolean(KEY_AUTO_ADD_TO_DEFAULT_GROUP, mAutoAddToDefaultGroup); outState.putBoolean(KEY_DISABLE_DELETE_MENU_OPTION, mDisableDeleteMenuOption); outState.putBoolean(KEY_NEW_LOCAL_PROFILE, mNewLocalProfile); if (mMaterialPalette != null) { outState.putParcelable(KEY_MATERIAL_PALETTE, mMaterialPalette); } outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator); outState.putParcelableArrayList(KEY_RAW_CONTACTS, mRawContacts == null ? Lists.newArrayList() : Lists.newArrayList(mRawContacts)); // NOTE: mGroupMetaData is not saved outState.putParcelable(KEY_EDIT_STATE, mState); outState.putInt(KEY_STATUS, mStatus); outState.putBoolean(KEY_HAS_NEW_CONTACT, mHasNewContact); outState.putBoolean(KEY_NEW_CONTACT_READY, mNewContactDataReady); outState.putBoolean(KEY_IS_EDIT, mIsEdit); outState.putBoolean(KEY_EXISTING_CONTACT_READY, mExistingContactDataReady); outState.putParcelable(KEY_ACCOUNT, mAccountWithDataSet); outState.putBoolean(KEY_IS_USER_PROFILE, mIsUserProfile); outState.putBoolean(KEY_ENABLED, mEnabled); // Aggregation PopupWindow outState.putLong(KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID, mAggregationSuggestionsRawContactId); // Join Activity outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin); outState.putLong(KEY_READ_ONLY_DISPLAY_NAME_ID, mReadOnlyDisplayNameId); outState.putBoolean(KEY_COPY_READ_ONLY_DISPLAY_NAME, mCopyReadOnlyName); outState.putLong(KEY_PHOTO_RAW_CONTACT_ID, mPhotoRawContactId); outState.putParcelable(KEY_UPDATED_PHOTOS, mUpdatedPhotos); super.onSaveInstanceState(outState); } @Override public void onStop() { super.onStop(); UiClosables.closeQuietly(mAggregationSuggestionPopup); } @Override public void onDestroy() { super.onDestroy(); if (mAggregationSuggestionEngine != null) { mAggregationSuggestionEngine.quit(); } } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case REQUEST_CODE_JOIN: { // Ignore failed requests if (resultCode != Activity.RESULT_OK) return; if (data != null) { final long contactId = ContentUris.parseId(data.getData()); if (hasPendingChanges()) { // Ask the user if they want to save changes before doing the join JoinContactConfirmationDialogFragment.show(this, contactId); } else { // Do the join immediately joinAggregate(contactId); } } break; } case REQUEST_CODE_ACCOUNTS_CHANGED: { // Bail if the account selector was not successful. if (resultCode != Activity.RESULT_OK || data == null || !data.hasExtra(Intents.Insert.EXTRA_ACCOUNT)) { if (mListener != null) { mListener.onReverted(); } return; } AccountWithDataSet account = data.getParcelableExtra( Intents.Insert.EXTRA_ACCOUNT); createContact(account); break; } } } @Override public void onAccountsLoaded(List data) { mWritableAccounts = data; // The user may need to select a new account to save to if (mAccountWithDataSet == null && mHasNewContact) { selectAccountAndCreateContact(); } final RawContactEditorView view = getContent(); if (view == null) { return; } view.setAccounts(data); if (mAccountWithDataSet == null && view.getCurrentRawContactDelta() == null) { return; } final AccountWithDataSet account = mAccountWithDataSet != null ? mAccountWithDataSet : view.getCurrentRawContactDelta().getAccountWithDataSet(); // The current account was removed if (!AccountInfo.contains(data, account) && !data.isEmpty()) { if (isReadyToBindEditors()) { onRebindEditorsForNewContact(getContent().getCurrentRawContactDelta(), account, data.get(0).getAccount()); } else { mAccountWithDataSet = data.get(0).getAccount(); } } } // // Options menu // @Override public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) { inflater.inflate(R.menu.edit_contact, menu); } @Override public void onPrepareOptionsMenu(Menu menu) { // This supports the keyboard shortcut to save changes to a contact but shouldn't be visible // because the custom action bar contains the "save" button now (not the overflow menu). // TODO: Find a better way to handle shortcuts, i.e. onKeyDown()? final MenuItem saveMenu = menu.findItem(R.id.menu_save); final MenuItem splitMenu = menu.findItem(R.id.menu_split); final MenuItem joinMenu = menu.findItem(R.id.menu_join); final MenuItem deleteMenu = menu.findItem(R.id.menu_delete); // TODO: b/30771904, b/31827701, temporarily disable these items until we get them to work // on a raw contact level. joinMenu.setVisible(false); splitMenu.setVisible(false); deleteMenu.setVisible(false); // Save menu is invisible when there's only one read only contact in the editor. saveMenu.setVisible(!isEditingReadOnlyRawContact()); if (saveMenu.isVisible()) { // Since we're using a custom action layout we have to manually hook up the handler. saveMenu.getActionView().setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { onOptionsItemSelected(saveMenu); } }); } int size = menu.size(); for (int i = 0; i < size; i++) { menu.getItem(i).setEnabled(mEnabled); } } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { return revert(); } final Activity activity = getActivity(); if (activity == null || activity.isFinishing() || activity.isDestroyed()) { // If we no longer are attached to a running activity want to // drain this event. return true; } final int id = item.getItemId(); if (id == R.id.menu_save) { return save(SaveMode.CLOSE); } else if (id == R.id.menu_delete) { if (mListener != null) mListener.onDeleteRequested(mLookupUri); return true; } else if (id == R.id.menu_split) { return doSplitContactAction(); } else if (id == R.id.menu_join) { return doJoinContactAction(); } else if (id == R.id.menu_help) { HelpUtils.launchHelpAndFeedbackForContactScreen(getActivity()); return true; } return false; } @Override public boolean revert() { if (mState.isEmpty() || !hasPendingChanges()) { onCancelEditConfirmed(); } else { CancelEditDialogFragment.show(this); } return true; } @Override public void onCancelEditConfirmed() { // When this Fragment is closed we don't want it to auto-save mStatus = Status.CLOSING; if (mListener != null) { mListener.onReverted(); } } @Override public void onSplitContactConfirmed(boolean hasPendingChanges) { if (mState.isEmpty()) { // This may happen when this Fragment is recreated by the system during users // confirming the split action (and thus this method is called just before onCreate()), // for example. Log.e(TAG, "mState became null during the user's confirming split action. " + "Cannot perform the save action."); return; } if (!hasPendingChanges && mHasNewContact) { // If the user didn't add anything new, we don't want to split out the newly created // raw contact into a name-only contact so remove them. final Iterator iterator = mState.iterator(); while (iterator.hasNext()) { final RawContactDelta rawContactDelta = iterator.next(); if (rawContactDelta.getRawContactId() < 0) { iterator.remove(); } } } mState.markRawContactsForSplitting(); save(SaveMode.SPLIT); } @Override public void onSplitContactCanceled() {} private boolean doSplitContactAction() { if (!hasValidState()) return false; SplitContactConfirmationDialogFragment.show(this, hasPendingChanges()); return true; } private boolean doJoinContactAction() { if (!hasValidState() || mLookupUri == null) { return false; } // If we just started creating a new contact and haven't added any data, it's too // early to do a join if (mState.size() == 1 && mState.get(0).isContactInsert() && !hasPendingChanges()) { Toast.makeText(mContext, R.string.toast_join_with_empty_contact, Toast.LENGTH_LONG).show(); return true; } showJoinAggregateActivity(mLookupUri); return true; } @Override public void onJoinContactConfirmed(long joinContactId) { doSaveAction(SaveMode.JOIN, joinContactId); } @Override public boolean save(int saveMode) { if (!hasValidState() || mStatus != Status.EDITING) { return false; } // If we are about to close the editor - there is no need to refresh the data if (saveMode == SaveMode.CLOSE || saveMode == SaveMode.EDITOR || saveMode == SaveMode.SPLIT) { getLoaderManager().destroyLoader(LOADER_CONTACT); } mStatus = Status.SAVING; if (!hasPendingChanges()) { if (mLookupUri == null && saveMode == SaveMode.RELOAD) { // We don't have anything to save and there isn't even an existing contact yet. // Nothing to do, simply go back to editing mode mStatus = Status.EDITING; return true; } onSaveCompleted(/* hadChanges =*/ false, saveMode, /* saveSucceeded =*/ mLookupUri != null, mLookupUri, /* joinContactId =*/ null); return true; } setEnabled(false); return doSaveAction(saveMode, /* joinContactId */ null); } // // State accessor methods // /** * Check if our internal {@link #mState} is valid, usually checked before * performing user actions. */ private boolean hasValidState() { return mState.size() > 0; } private boolean isEditingUserProfile() { return mNewLocalProfile || mIsUserProfile; } /** * Whether the contact being edited is composed of read-only raw contacts * aggregated with a newly created writable raw contact. */ private boolean isEditingReadOnlyRawContactWithNewContact() { return mHasNewContact && mState.size() > 1; } /** * @return true if the single raw contact we're looking at is read-only. */ private boolean isEditingReadOnlyRawContact() { return hasValidState() && mRawContactIdToDisplayAlone > 0 && !mState.getByRawContactId(mRawContactIdToDisplayAlone) .getAccountType(AccountTypeManager.getInstance(mContext)) .areContactsWritable(); } /** * Return true if there are any edits to the current contact which need to * be saved. */ private boolean hasPendingRawContactChanges(Set excludedMimeTypes) { final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); return RawContactModifier.hasChanges(mState, accountTypes, excludedMimeTypes); } /** * Determines if changes were made in the editor that need to be saved, while taking into * account that name changes are not real for read-only contacts. * See go/editing-read-only-contacts */ private boolean hasPendingChanges() { if (isEditingReadOnlyRawContactWithNewContact()) { // We created a new raw contact delta with a default display name. // We must test for pending changes while ignoring the default display name. final ValuesDelta beforeDelta = mState.getByRawContactId(mReadOnlyDisplayNameId) .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE); final ValuesDelta pendingDelta = mState .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE); if (structuredNamesAreEqual(beforeDelta, pendingDelta)) { final Set excludedMimeTypes = new HashSet<>(); excludedMimeTypes.add(StructuredName.CONTENT_ITEM_TYPE); return hasPendingRawContactChanges(excludedMimeTypes); } return true; } return hasPendingRawContactChanges(/* excludedMimeTypes =*/ null); } /** * Compares the two {@link ValuesDelta} to see if the structured name is changed. We made a copy * of a read only delta and now we want to check if the copied delta has changes. * * @param before original {@link ValuesDelta} * @param after copied {@link ValuesDelta} * @return true if the copied {@link ValuesDelta} has all the same values in the structured * name fields as the original. */ private boolean structuredNamesAreEqual(ValuesDelta before, ValuesDelta after) { if (before == after) return true; if (before == null || after == null) return false; final ContentValues original = before.getBefore(); final ContentValues pending = after.getAfter(); if (original != null && pending != null) { final String beforeDisplayName = original.getAsString(StructuredName.DISPLAY_NAME); final String afterDisplayName = pending.getAsString(StructuredName.DISPLAY_NAME); if (!TextUtils.equals(beforeDisplayName, afterDisplayName)) return false; final String beforePrefix = original.getAsString(StructuredName.PREFIX); final String afterPrefix = pending.getAsString(StructuredName.PREFIX); if (!TextUtils.equals(beforePrefix, afterPrefix)) return false; final String beforeFirstName = original.getAsString(StructuredName.GIVEN_NAME); final String afterFirstName = pending.getAsString(StructuredName.GIVEN_NAME); if (!TextUtils.equals(beforeFirstName, afterFirstName)) return false; final String beforeMiddleName = original.getAsString(StructuredName.MIDDLE_NAME); final String afterMiddleName = pending.getAsString(StructuredName.MIDDLE_NAME); if (!TextUtils.equals(beforeMiddleName, afterMiddleName)) return false; final String beforeLastName = original.getAsString(StructuredName.FAMILY_NAME); final String afterLastName = pending.getAsString(StructuredName.FAMILY_NAME); if (!TextUtils.equals(beforeLastName, afterLastName)) return false; final String beforeSuffix = original.getAsString(StructuredName.SUFFIX); final String afterSuffix = pending.getAsString(StructuredName.SUFFIX); return TextUtils.equals(beforeSuffix, afterSuffix); } return false; } // // Account creation // private void selectAccountAndCreateContact() { Preconditions.checkNotNull(mWritableAccounts, "Accounts must be loaded first"); // If this is a local profile, then skip the logic about showing the accounts changed // activity and create a phone-local contact. if (mNewLocalProfile) { createContact(null); return; } final List accounts = AccountInfo.extractAccounts(mWritableAccounts); // If there is no default account or the accounts have changed such that we need to // prompt the user again, then launch the account prompt. if (mEditorUtils.shouldShowAccountChangedNotification(accounts)) { Intent intent = new Intent(mContext, ContactEditorAccountsChangedActivity.class); // Prevent a second instance from being started on rotates intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); mStatus = Status.SUB_ACTIVITY; startActivityForResult(intent, REQUEST_CODE_ACCOUNTS_CHANGED); } else { // Make sure the default account is automatically set if there is only one non-device // account. mEditorUtils.maybeUpdateDefaultAccount(accounts); // Otherwise, there should be a default account. Then either create a local contact // (if default account is null) or create a contact with the specified account. AccountWithDataSet defaultAccount = mEditorUtils.getOnlyOrDefaultAccount(accounts); createContact(defaultAccount); } } /** * Shows account creation screen associated with a given account. * * @param account may be null to signal a device-local contact should be created. */ private void createContact(AccountWithDataSet account) { final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); final AccountType accountType = accountTypes.getAccountTypeForAccount(account); setStateForNewContact(account, accountType, isEditingUserProfile()); } // // Data binding // private void setState(Contact contact) { // If we have already loaded data, we do not want to change it here to not confuse the user if (!mState.isEmpty()) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Ignoring background change. This will have to be rebased later"); } return; } mContact = contact; mRawContacts = contact.getRawContacts(); // Check for writable raw contacts. If there are none, then we need to create one so user // can edit. For the user profile case, there is already an editable contact. if (!contact.isUserProfile() && !contact.isWritableContact(mContext)) { mHasNewContact = true; mReadOnlyDisplayNameId = contact.getNameRawContactId(); mCopyReadOnlyName = true; // This is potentially an asynchronous call and will add deltas to list. selectAccountAndCreateContact(); } else { mHasNewContact = false; } setStateForExistingContact(contact.isUserProfile(), mRawContacts); if (mAutoAddToDefaultGroup && InvisibleContactUtil.isInvisibleAndAddable(contact, getContext())) { InvisibleContactUtil.markAddToDefaultGroup(contact, mState, getContext()); } } /** * Prepare {@link #mState} for a newly created phone-local contact. */ private void setStateForNewContact(AccountWithDataSet account, AccountType accountType, boolean isUserProfile) { setStateForNewContact(account, accountType, /* oldState =*/ null, /* oldAccountType =*/ null, isUserProfile); } /** * Prepare {@link #mState} for a newly created phone-local contact, migrating the state * specified by oldState and oldAccountType. */ private void setStateForNewContact(AccountWithDataSet account, AccountType accountType, RawContactDelta oldState, AccountType oldAccountType, boolean isUserProfile) { mStatus = Status.EDITING; mAccountWithDataSet = account; mState.add(createNewRawContactDelta(account, accountType, oldState, oldAccountType)); mIsUserProfile = isUserProfile; mNewContactDataReady = true; bindEditors(); } /** * Returns a {@link RawContactDelta} for a new contact suitable for addition into * {@link #mState}. * * If oldState and oldAccountType are specified, the state specified by those parameters * is migrated to the result {@link RawContactDelta}. */ private RawContactDelta createNewRawContactDelta(AccountWithDataSet account, AccountType accountType, RawContactDelta oldState, AccountType oldAccountType) { final RawContact rawContact = new RawContact(); if (account != null) { rawContact.setAccount(account); } else { rawContact.setAccountToLocal(); } final RawContactDelta result = new RawContactDelta( ValuesDelta.fromAfter(rawContact.getValues())); if (oldState == null) { // Parse any values from incoming intent RawContactModifier.parseExtras(mContext, accountType, result, mIntentExtras); } else { RawContactModifier.migrateStateForNewContact( mContext, oldState, result, oldAccountType, accountType); } // Ensure we have some default fields (if the account type does not support a field, // ensureKind will not add it, so it is safe to add e.g. Event) RawContactModifier.ensureKindExists(result, accountType, StructuredName.CONTENT_ITEM_TYPE); RawContactModifier.ensureKindExists(result, accountType, Phone.CONTENT_ITEM_TYPE); RawContactModifier.ensureKindExists(result, accountType, Email.CONTENT_ITEM_TYPE); RawContactModifier.ensureKindExists(result, accountType, Organization.CONTENT_ITEM_TYPE); RawContactModifier.ensureKindExists(result, accountType, Event.CONTENT_ITEM_TYPE); RawContactModifier.ensureKindExists(result, accountType, StructuredPostal.CONTENT_ITEM_TYPE); // Set the correct URI for saving the contact as a profile if (mNewLocalProfile) { result.setProfileQueryUri(); } return result; } /** * Prepare {@link #mState} for an existing contact. */ private void setStateForExistingContact(boolean isUserProfile, ImmutableList rawContacts) { setEnabled(true); mState.addAll(rawContacts.iterator()); setIntentExtras(mIntentExtras); mIntentExtras = null; // For user profile, change the contacts query URI mIsUserProfile = isUserProfile; boolean localProfileExists = false; if (mIsUserProfile) { for (RawContactDelta rawContactDelta : mState) { // For profile contacts, we need a different query URI rawContactDelta.setProfileQueryUri(); // Try to find a local profile contact if (rawContactDelta.getValues().getAsString(RawContacts.ACCOUNT_TYPE) == null) { localProfileExists = true; } } // Editor should always present a local profile for editing // TODO(wjang): Need to figure out when this case comes up. We can't do this if we're // going to prune all but the one raw contact that we're trying to display by itself. if (!localProfileExists && mRawContactIdToDisplayAlone <= 0) { mState.add(createLocalRawContactDelta()); } } mExistingContactDataReady = true; bindEditors(); } /** * Set the enabled state of editors. */ private void setEnabled(boolean enabled) { if (mEnabled != enabled) { mEnabled = enabled; // Enable/disable editors if (mContent != null) { int count = mContent.getChildCount(); for (int i = 0; i < count; i++) { mContent.getChildAt(i).setEnabled(enabled); } } // Maybe invalidate the options menu final Activity activity = getActivity(); if (activity != null) activity.invalidateOptionsMenu(); } } /** * Returns a {@link RawContactDelta} for a local contact suitable for addition into * {@link #mState}. */ private static RawContactDelta createLocalRawContactDelta() { final RawContact rawContact = new RawContact(); rawContact.setAccountToLocal(); final RawContactDelta result = new RawContactDelta( ValuesDelta.fromAfter(rawContact.getValues())); result.setProfileQueryUri(); return result; } private void copyReadOnlyName() { // We should only ever be doing this if we're creating a new writable contact to attach to // a read only contact. if (!isEditingReadOnlyRawContactWithNewContact()) { return; } final int writableIndex = mState.indexOfFirstWritableRawContact(getContext()); final RawContactDelta writable = mState.get(writableIndex); final RawContactDelta readOnly = mState.getByRawContactId(mContact.getNameRawContactId()); final ValuesDelta writeNameDelta = writable .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE); final ValuesDelta readNameDelta = readOnly .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE); mCopyReadOnlyName = false; if (writeNameDelta == null || readNameDelta == null) { return; } writeNameDelta.copyStructuredNameFieldsFrom(readNameDelta); } /** * Bind editors using {@link #mState} and other members initialized from the loaded (or new) * Contact. */ protected void bindEditors() { if (!isReadyToBindEditors()) { return; } // Add input fields for the loaded Contact final RawContactEditorView editorView = getContent(); editorView.setListener(this); if (mCopyReadOnlyName) { copyReadOnlyName(); } editorView.setState(mState, mMaterialPalette, mViewIdGenerator, mHasNewContact, mIsUserProfile, mAccountWithDataSet, mRawContactIdToDisplayAlone); if (isEditingReadOnlyRawContact()) { final Toolbar toolbar = getEditorActivity().getToolbar(); if (toolbar != null) { toolbar.setTitle(R.string.contact_editor_title_read_only_contact); // Set activity title for Talkback getEditorActivity().setTitle(R.string.contact_editor_title_read_only_contact); toolbar.setNavigationIcon(R.drawable.quantum_ic_arrow_back_vd_theme_24); toolbar.setNavigationContentDescription(R.string.back_arrow_content_description); toolbar.getNavigationIcon().setAutoMirrored(true); } } // Set up the photo widget editorView.setPhotoListener(this); mPhotoRawContactId = editorView.getPhotoRawContactId(); // If there is an updated full resolution photo apply it now, this will be the case if // the user selects or takes a new photo, then rotates the device. final Uri uri = (Uri) mUpdatedPhotos.get(String.valueOf(mPhotoRawContactId)); if (uri != null) { editorView.setFullSizePhoto(uri); } // The editor is ready now so make it visible editorView.setEnabled(mEnabled); editorView.setVisibility(View.VISIBLE); // Refresh the ActionBar as the visibility of the join command // Activity can be null if we have been detached from the Activity. invalidateOptionsMenu(); } /** * Invalidates the options menu if we are still associated with an Activity. */ private void invalidateOptionsMenu() { final Activity activity = getActivity(); if (activity != null) { activity.invalidateOptionsMenu(); } } private boolean isReadyToBindEditors() { if (mState.isEmpty()) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "No data to bind editors"); } return false; } if (mIsEdit && !mExistingContactDataReady) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Existing contact data is not ready to bind editors."); } return false; } if (mHasNewContact && !mNewContactDataReady) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "New contact data is not ready to bind editors."); } return false; } // Don't attempt to bind anything if we have no permissions. return RequestPermissionsActivity.hasRequiredPermissions(mContext); } /** * Removes a current editor ({@link #mState}) and rebinds new editor for a new account. * Some of old data are reused with new restriction enforced by the new account. * * @param oldState Old data being edited. * @param oldAccount Old account associated with oldState. * @param newAccount New account to be used. */ private void rebindEditorsForNewContact( RawContactDelta oldState, AccountWithDataSet oldAccount, AccountWithDataSet newAccount) { AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); AccountType oldAccountType = accountTypes.getAccountTypeForAccount(oldAccount); AccountType newAccountType = accountTypes.getAccountTypeForAccount(newAccount); mExistingContactDataReady = false; mNewContactDataReady = false; mState = new RawContactDeltaList(); setStateForNewContact(newAccount, newAccountType, oldState, oldAccountType, isEditingUserProfile()); if (mIsEdit) { setStateForExistingContact(isEditingUserProfile(), mRawContacts); } } // // ContactEditor // @Override public void setListener(Listener listener) { mListener = listener; } @Override public void load(String action, Uri lookupUri, Bundle intentExtras) { mAction = action; mLookupUri = lookupUri; mIntentExtras = intentExtras; if (mIntentExtras != null) { mAutoAddToDefaultGroup = mIntentExtras.containsKey(INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY); mNewLocalProfile = mIntentExtras.getBoolean(INTENT_EXTRA_NEW_LOCAL_PROFILE); mDisableDeleteMenuOption = mIntentExtras.getBoolean(INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION); if (mIntentExtras.containsKey(INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR) && mIntentExtras.containsKey(INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR)) { mMaterialPalette = new MaterialColorMapUtils.MaterialPalette( mIntentExtras.getInt(INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR), mIntentExtras.getInt(INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR)); } mRawContactIdToDisplayAlone = mIntentExtras .getLong(INTENT_EXTRA_RAW_CONTACT_ID_TO_DISPLAY_ALONE); } } @Override public void setIntentExtras(Bundle extras) { getContent().setIntentExtras(extras); } @Override public void onJoinCompleted(Uri uri) { onSaveCompleted(false, SaveMode.RELOAD, uri != null, uri, /* joinContactId */ null); } private String getNameToDisplay(Uri contactUri) { // The contact has been deleted or the uri is otherwise no longer right. if (contactUri == null) { return null; } final ContentResolver resolver = mContext.getContentResolver(); final Cursor cursor = resolver.query(contactUri, new String[]{ ContactsContract.Contacts.DISPLAY_NAME, ContactsContract.Contacts.DISPLAY_NAME_ALTERNATIVE}, null, null, null); if (cursor != null) { try { if (cursor.moveToFirst()) { final String displayName = cursor.getString(0); final String displayNameAlt = cursor.getString(1); cursor.close(); return ContactDisplayUtils.getPreferredDisplayName(displayName, displayNameAlt, new ContactsPreferences(mContext)); } } finally { cursor.close(); } } return null; } @Override public void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded, Uri contactLookupUri, Long joinContactId) { if (hadChanges) { if (saveSucceeded) { switch (saveMode) { case SaveMode.JOIN: break; case SaveMode.SPLIT: Toast.makeText(mContext, R.string.contactUnlinkedToast, Toast.LENGTH_SHORT) .show(); break; default: final String displayName = getNameToDisplay(contactLookupUri); final String toastMessage; if (!TextUtils.isEmpty(displayName)) { toastMessage = getResources().getString( R.string.contactSavedNamedToast, displayName); } else { toastMessage = getResources().getString(R.string.contactSavedToast); } Toast.makeText(mContext, toastMessage, Toast.LENGTH_SHORT).show(); } } else { Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show(); } } switch (saveMode) { case SaveMode.CLOSE: { final Intent resultIntent; if (saveSucceeded && contactLookupUri != null) { final Uri lookupUri = ContactEditorUtils.maybeConvertToLegacyLookupUri( mContext, contactLookupUri, mLookupUri); resultIntent = ImplicitIntentsUtil.composeQuickContactIntent( mContext, lookupUri, ScreenType.EDITOR); resultIntent.putExtra(QuickContactActivity.EXTRA_CONTACT_EDITED, true); } else { resultIntent = null; } // It is already saved, so prevent it from being saved again mStatus = Status.CLOSING; if (mListener != null) mListener.onSaveFinished(resultIntent); break; } case SaveMode.EDITOR: { // It is already saved, so prevent it from being saved again mStatus = Status.CLOSING; if (mListener != null) mListener.onSaveFinished(/* resultIntent= */ null); break; } case SaveMode.JOIN: if (saveSucceeded && contactLookupUri != null && joinContactId != null) { joinAggregate(joinContactId); } break; case SaveMode.RELOAD: if (saveSucceeded && contactLookupUri != null) { // If this was in INSERT, we are changing into an EDIT now. // If it already was an EDIT, we are changing to the new Uri now mState = new RawContactDeltaList(); load(Intent.ACTION_EDIT, contactLookupUri, null); mStatus = Status.LOADING; getLoaderManager().restartLoader(LOADER_CONTACT, null, mContactLoaderListener); } break; case SaveMode.SPLIT: mStatus = Status.CLOSING; if (mListener != null) { mListener.onContactSplit(contactLookupUri); } else if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "No listener registered, can not call onSplitFinished"); } break; } } /** * Shows a list of aggregates that can be joined into the currently viewed aggregate. * * @param contactLookupUri the fresh URI for the currently edited contact (after saving it) */ private void showJoinAggregateActivity(Uri contactLookupUri) { if (contactLookupUri == null || !isAdded()) { return; } mContactIdForJoin = ContentUris.parseId(contactLookupUri); final Intent intent = new Intent(mContext, ContactSelectionActivity.class); intent.setAction(UiIntentActions.PICK_JOIN_CONTACT_ACTION); intent.putExtra(UiIntentActions.TARGET_CONTACT_ID_EXTRA_KEY, mContactIdForJoin); startActivityForResult(intent, REQUEST_CODE_JOIN); } // // Aggregation PopupWindow // /** * Triggers an asynchronous search for aggregation suggestions. */ protected void acquireAggregationSuggestions(Context context, long rawContactId, ValuesDelta valuesDelta) { mAggregationSuggestionsRawContactId = rawContactId; if (mAggregationSuggestionEngine == null) { mAggregationSuggestionEngine = new AggregationSuggestionEngine(context); mAggregationSuggestionEngine.setListener(this); mAggregationSuggestionEngine.start(); } mAggregationSuggestionEngine.setContactId(getContactId()); mAggregationSuggestionEngine.setAccountFilter( getContent().getCurrentRawContactDelta().getAccountWithDataSet()); mAggregationSuggestionEngine.onNameChange(valuesDelta); } /** * Returns the contact ID for the currently edited contact or 0 if the contact is new. */ private long getContactId() { for (RawContactDelta rawContact : mState) { Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID); if (contactId != null) { return contactId; } } return 0; } @Override public void onAggregationSuggestionChange() { final Activity activity = getActivity(); if ((activity != null && activity.isFinishing()) || !isVisible() || mState.isEmpty() || mStatus != Status.EDITING) { return; } UiClosables.closeQuietly(mAggregationSuggestionPopup); if (mAggregationSuggestionEngine.getSuggestedContactCount() == 0) { return; } final View anchorView = getAggregationAnchorView(); if (anchorView == null) { return; // Raw contact deleted? } mAggregationSuggestionPopup = new ListPopupWindow(mContext, null); mAggregationSuggestionPopup.setAnchorView(anchorView); mAggregationSuggestionPopup.setWidth(anchorView.getWidth()); mAggregationSuggestionPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); mAggregationSuggestionPopup.setAdapter( new AggregationSuggestionAdapter( getActivity(), /* listener =*/ this, mAggregationSuggestionEngine.getSuggestions())); mAggregationSuggestionPopup.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { final AggregationSuggestionView suggestionView = (AggregationSuggestionView) view; suggestionView.handleItemClickEvent(); UiClosables.closeQuietly(mAggregationSuggestionPopup); mAggregationSuggestionPopup = null; } }); mAggregationSuggestionPopup.show(); } /** * Returns the editor view that should be used as the anchor for aggregation suggestions. */ protected View getAggregationAnchorView() { return getContent().getAggregationAnchorView(); } /** * Joins the suggested contact (specified by the id's of constituent raw * contacts), save all changes, and stay in the editor. */ public void doJoinSuggestedContact(long[] rawContactIds) { if (!hasValidState() || mStatus != Status.EDITING) { return; } mState.setJoinWithRawContacts(rawContactIds); save(SaveMode.RELOAD); } @Override public void onEditAction(Uri contactLookupUri, long rawContactId) { SuggestionEditConfirmationDialogFragment.show(this, contactLookupUri, rawContactId); } /** * Abandons the currently edited contact and switches to editing the selected raw contact, * transferring all the data there */ public void doEditSuggestedContact(Uri contactUri, long rawContactId) { if (mListener != null) { // make sure we don't save this contact when closing down mStatus = Status.CLOSING; mListener.onEditOtherRawContactRequested(contactUri, rawContactId, getContent().getCurrentRawContactDelta().getContentValues()); } } /** * Sets group metadata on all bound editors. */ protected void setGroupMetaData() { if (mGroupMetaData != null) { getContent().setGroupMetaData(mGroupMetaData); } } /** * Persist the accumulated editor deltas. * * @param joinContactId the raw contact ID to join the contact being saved to after the save, * may be null. */ protected boolean doSaveAction(int saveMode, Long joinContactId) { final Intent intent = ContactSaveService.createSaveContactIntent(mContext, mState, SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(), ((Activity) mContext).getClass(), ContactEditorActivity.ACTION_SAVE_COMPLETED, mUpdatedPhotos, JOIN_CONTACT_ID_EXTRA_KEY, joinContactId); return startSaveService(mContext, intent, saveMode); } private boolean startSaveService(Context context, Intent intent, int saveMode) { final boolean result = ContactSaveService.startService( context, intent, saveMode); if (!result) { onCancelEditConfirmed(); } return result; } // // Join Activity // /** * Performs aggregation with the contact selected by the user from suggestions or A-Z list. */ protected void joinAggregate(final long contactId) { final Intent intent = ContactSaveService.createJoinContactsIntent( mContext, mContactIdForJoin, contactId, ContactEditorActivity.class, ContactEditorActivity.ACTION_JOIN_COMPLETED); mContext.startService(intent); } public void removePhoto() { getContent().removePhoto(); mUpdatedPhotos.remove(String.valueOf(mPhotoRawContactId)); } public void updatePhoto(Uri uri) throws FileNotFoundException { final Bitmap bitmap = ContactPhotoUtils.getBitmapFromUri(getActivity(), uri); if (bitmap == null || bitmap.getHeight() <= 0 || bitmap.getWidth() <= 0) { Toast.makeText(mContext, R.string.contactPhotoSavedErrorToast, Toast.LENGTH_SHORT).show(); return; } mUpdatedPhotos.putParcelable(String.valueOf(mPhotoRawContactId), uri); getContent().updatePhoto(uri); } public void setPrimaryPhoto() { getContent().setPrimaryPhoto(); } @Override public void onNameFieldChanged(long rawContactId, ValuesDelta valuesDelta) { final Activity activity = getActivity(); if (activity == null || activity.isFinishing()) { return; } acquireAggregationSuggestions(activity, rawContactId, valuesDelta); } @Override public void onRebindEditorsForNewContact(RawContactDelta oldState, AccountWithDataSet oldAccount, AccountWithDataSet newAccount) { mNewContactAccountChanged = true; rebindEditorsForNewContact(oldState, oldAccount, newAccount); } @Override public void onBindEditorsFailed() { final Activity activity = getActivity(); if (activity != null && !activity.isFinishing()) { Toast.makeText(activity, R.string.editor_failed_to_load, Toast.LENGTH_SHORT).show(); activity.setResult(Activity.RESULT_CANCELED); activity.finish(); } } @Override public void onEditorsBound() { final Activity activity = getActivity(); if (activity == null || activity.isFinishing()) { return; } getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupsLoaderListener); } @Override public void onPhotoEditorViewClicked() { // For contacts composed of a single writable raw contact, or raw contacts have no more // than 1 photo, clicking the photo view simply opens the source photo dialog getEditorActivity().changePhoto(getPhotoMode()); } private int getPhotoMode() { return getContent().isWritablePhotoSet() ? PhotoActionPopup.Modes.WRITE_ABLE_PHOTO : PhotoActionPopup.Modes.NO_PHOTO; } private ContactEditorActivity getEditorActivity() { return (ContactEditorActivity) getActivity(); } private RawContactEditorView getContent() { return (RawContactEditorView) mContent; } }