| /* |
| * Copyright (C) 2010 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.AlertDialog; |
| import android.app.Dialog; |
| import android.app.DialogFragment; |
| import android.app.Fragment; |
| import android.app.LoaderManager; |
| import android.app.LoaderManager.LoaderCallbacks; |
| import android.content.ContentUris; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.CursorLoader; |
| import android.content.DialogInterface; |
| import android.content.Intent; |
| import android.content.Loader; |
| import android.database.Cursor; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.SystemClock; |
| 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.Photo; |
| import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; |
| import android.provider.ContactsContract.Contacts; |
| import android.provider.ContactsContract.Groups; |
| import android.provider.ContactsContract.Intents; |
| import android.provider.ContactsContract.RawContacts; |
| 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.AdapterView.OnItemClickListener; |
| 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.JoinContactActivity; |
| import com.android.contacts.detail.PhotoSelectionHandler; |
| import com.android.contacts.editor.AggregationSuggestionEngine.Suggestion; |
| import com.android.contacts.editor.Editor.EditorListener; |
| 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.RawContactDelta.ValuesDelta; |
| import com.android.contacts.model.RawContactDeltaList; |
| import com.android.contacts.model.RawContactModifier; |
| import com.android.contacts.model.account.AccountType; |
| import com.android.contacts.model.account.AccountWithDataSet; |
| import com.android.contacts.model.account.GoogleAccountType; |
| import com.android.contacts.util.AccountsListAdapter; |
| import com.android.contacts.util.AccountsListAdapter.AccountListFilter; |
| import com.android.contacts.util.ContactPhotoUtils; |
| import com.android.contacts.util.HelpUtils; |
| import com.google.common.collect.ImmutableList; |
| |
| import java.io.File; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.List; |
| |
| public class ContactEditorFragment extends Fragment implements |
| SplitContactConfirmationDialogFragment.Listener, |
| AggregationSuggestionEngine.Listener, AggregationSuggestionView.Listener, |
| RawContactReadOnlyEditorView.Listener { |
| |
| private static final String TAG = ContactEditorFragment.class.getSimpleName(); |
| |
| private static final int LOADER_DATA = 1; |
| private static final int LOADER_GROUPS = 2; |
| |
| private static final String KEY_URI = "uri"; |
| private static final String KEY_ACTION = "action"; |
| private static final String KEY_EDIT_STATE = "state"; |
| private static final String KEY_RAW_CONTACT_ID_REQUESTING_PHOTO = "photorequester"; |
| private static final String KEY_VIEW_ID_GENERATOR = "viewidgenerator"; |
| private static final String KEY_CURRENT_PHOTO_FILE = "currentphotofile"; |
| private static final String KEY_CONTACT_ID_FOR_JOIN = "contactidforjoin"; |
| private static final String KEY_CONTACT_WRITABLE_FOR_JOIN = "contactwritableforjoin"; |
| private static final String KEY_SHOW_JOIN_SUGGESTIONS = "showJoinSuggestions"; |
| private static final String KEY_ENABLED = "enabled"; |
| private static final String KEY_STATUS = "status"; |
| private static final String KEY_NEW_LOCAL_PROFILE = "newLocalProfile"; |
| private static final String KEY_IS_USER_PROFILE = "isUserProfile"; |
| private static final String KEY_UPDATED_PHOTOS = "updatedPhotos"; |
| |
| private static final String[] VALID_ACTIONS = {Intent.ACTION_EDIT, Intent.ACTION_INSERT, |
| ContactEditorActivity.ACTION_SAVE_COMPLETED}; |
| |
| public static final String SAVE_MODE_EXTRA_KEY = "saveMode"; |
| |
| |
| /** |
| * 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"; |
| |
| /** |
| * Modes that specify what the AsyncTask has to perform after saving |
| */ |
| public interface SaveMode { |
| /** |
| * Close the editor after saving |
| */ |
| public static final int CLOSE = 0; |
| |
| /** |
| * Reload the data so that the user can continue editing |
| */ |
| public static final int RELOAD = 1; |
| |
| /** |
| * Split the contact after saving |
| */ |
| public static final int SPLIT = 2; |
| |
| /** |
| * Join another contact after saving |
| */ |
| public static final int JOIN = 3; |
| |
| /** |
| * Navigate to Contacts Home activity after saving. |
| */ |
| public static final int HOME = 4; |
| } |
| |
| private interface Status { |
| /** |
| * The loader is fetching data |
| */ |
| public static final int LOADING = 0; |
| |
| /** |
| * Not currently busy. We are waiting for the user to enter data |
| */ |
| public static final int EDITING = 1; |
| |
| /** |
| * The data is currently being saved. This is used to prevent more |
| * auto-saves (they shouldn't overlap) |
| */ |
| public static final int SAVING = 2; |
| |
| /** |
| * Prevents any more saves. This is used if in the following cases: |
| * - After Save/Close |
| * - After Revert |
| * - After the user has accepted an edit suggestion |
| */ |
| public static final int CLOSING = 3; |
| |
| /** |
| * Prevents saving while running a child activity. |
| */ |
| public static final int SUB_ACTIVITY = 4; |
| } |
| |
| private static final int REQUEST_CODE_JOIN = 0; |
| private static final int REQUEST_CODE_ACCOUNTS_CHANGED = 1; |
| |
| /** |
| * The raw contact for which we started "take photo" or "choose photo from gallery" most |
| * recently. Used to restore {@link #mCurrentPhotoHandler} after orientation change. |
| */ |
| private long mRawContactIdRequestingPhoto; |
| /** |
| * The {@link PhotoHandler} for the photo editor for the {@link #mRawContactIdRequestingPhoto} |
| * raw contact. |
| * |
| * A {@link PhotoHandler} is created for each photo editor in {@link #bindPhotoHandler}, but |
| * the only "active" one should get the activity result. This member represents the active |
| * one. |
| */ |
| private PhotoHandler mCurrentPhotoHandler; |
| |
| private final EntityDeltaComparator mComparator = new EntityDeltaComparator(); |
| |
| private Cursor mGroupMetaData; |
| |
| private String mCurrentPhotoFile; |
| private Bundle mUpdatedPhotos = new Bundle(); |
| |
| private Context mContext; |
| private String mAction; |
| private Uri mLookupUri; |
| private Bundle mIntentExtras; |
| private Listener mListener; |
| |
| private long mContactIdForJoin; |
| private boolean mContactWritableForJoin; |
| |
| private ContactEditorUtils mEditorUtils; |
| |
| private LinearLayout mContent; |
| private RawContactDeltaList mState; |
| |
| private ViewIdGenerator mViewIdGenerator; |
| |
| private long mLoaderStartTime; |
| |
| private int mStatus; |
| |
| private AggregationSuggestionEngine mAggregationSuggestionEngine; |
| private long mAggregationSuggestionsRawContactId; |
| private View mAggregationSuggestionView; |
| |
| private ListPopupWindow mAggregationSuggestionPopup; |
| |
| private static final class AggregationSuggestionAdapter extends BaseAdapter { |
| private final Activity mActivity; |
| private final boolean mSetNewContact; |
| private final AggregationSuggestionView.Listener mListener; |
| private final List<Suggestion> mSuggestions; |
| |
| public AggregationSuggestionAdapter(Activity activity, boolean setNewContact, |
| AggregationSuggestionView.Listener listener, List<Suggestion> suggestions) { |
| mActivity = activity; |
| mSetNewContact = setNewContact; |
| mListener = listener; |
| mSuggestions = suggestions; |
| } |
| |
| @Override |
| public View getView(int position, View convertView, ViewGroup parent) { |
| Suggestion suggestion = (Suggestion) getItem(position); |
| LayoutInflater inflater = mActivity.getLayoutInflater(); |
| AggregationSuggestionView suggestionView = |
| (AggregationSuggestionView) inflater.inflate( |
| R.layout.aggregation_suggestions_item, null); |
| suggestionView.setNewContact(mSetNewContact); |
| 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(); |
| } |
| } |
| |
| private OnItemClickListener mAggregationSuggestionItemClickListener = |
| new OnItemClickListener() { |
| @Override |
| public void onItemClick(AdapterView<?> parent, View view, int position, long id) { |
| final AggregationSuggestionView suggestionView = (AggregationSuggestionView) view; |
| suggestionView.handleItemClickEvent(); |
| mAggregationSuggestionPopup.dismiss(); |
| mAggregationSuggestionPopup = null; |
| } |
| }; |
| |
| private boolean mAutoAddToDefaultGroup; |
| |
| private boolean mEnabled = true; |
| private boolean mRequestFocus; |
| private boolean mNewLocalProfile = false; |
| private boolean mIsUserProfile = false; |
| |
| public ContactEditorFragment() { |
| } |
| |
| public void setEnabled(boolean enabled) { |
| if (mEnabled != enabled) { |
| mEnabled = enabled; |
| if (mContent != null) { |
| int count = mContent.getChildCount(); |
| for (int i = 0; i < count; i++) { |
| mContent.getChildAt(i).setEnabled(enabled); |
| } |
| } |
| setAggregationSuggestionViewEnabled(enabled); |
| final Activity activity = getActivity(); |
| if (activity != null) activity.invalidateOptionsMenu(); |
| } |
| } |
| |
| @Override |
| public void onAttach(Activity activity) { |
| super.onAttach(activity); |
| mContext = activity; |
| mEditorUtils = ContactEditorUtils.getInstance(mContext); |
| } |
| |
| @Override |
| public void onStop() { |
| super.onStop(); |
| if (mAggregationSuggestionEngine != null) { |
| mAggregationSuggestionEngine.quit(); |
| } |
| |
| // If anything was left unsaved, save it now but keep the editor open. |
| if (!getActivity().isChangingConfigurations() && mStatus == Status.EDITING) { |
| save(SaveMode.RELOAD); |
| } |
| } |
| |
| @Override |
| public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { |
| final View view = inflater.inflate(R.layout.contact_editor_fragment, container, false); |
| |
| mContent = (LinearLayout) view.findViewById(R.id.editors); |
| |
| setHasOptionsMenu(true); |
| |
| return view; |
| } |
| |
| @Override |
| public void onActivityCreated(Bundle savedInstanceState) { |
| super.onActivityCreated(savedInstanceState); |
| |
| validateAction(mAction); |
| |
| // Handle initial actions only when existing state missing |
| final boolean hasIncomingState = savedInstanceState != null; |
| |
| if (mState == null) { |
| // 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. |
| // or |
| // 2) not an orientation change. data needs to be loaded for first time. |
| getLoaderManager().initLoader(LOADER_DATA, null, mDataLoaderListener); |
| } |
| } else { |
| // Orientation change, we already have mState, it was loaded by onCreate |
| bindEditors(); |
| } |
| |
| if (!hasIncomingState) { |
| if (Intent.ACTION_INSERT.equals(mAction)) { |
| final Account account = mIntentExtras == null ? null : |
| (Account) mIntentExtras.getParcelable(Intents.Insert.ACCOUNT); |
| final String dataSet = mIntentExtras == null ? null : |
| mIntentExtras.getString(Intents.Insert.DATA_SET); |
| |
| if (account != null) { |
| // Account specified in Intent |
| createContact(new AccountWithDataSet(account.name, account.type, dataSet)); |
| } else { |
| // No Account specified. Let the user choose |
| // Load Accounts async so that we can present them |
| selectAccountAndCreateContact(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Checks if the requested action is valid. |
| * |
| * @param action The action to test. |
| * @throws IllegalArgumentException when the action is invalid. |
| */ |
| private void validateAction(String action) { |
| for (String validAction : VALID_ACTIONS) { |
| if (validAction.equals(action)) { |
| return; |
| } |
| } |
| throw new IllegalArgumentException("Unknown Action String " + mAction + |
| ". Only support " + Intent.ACTION_EDIT + " or " + Intent.ACTION_INSERT); |
| } |
| |
| @Override |
| public void onStart() { |
| getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupLoaderListener); |
| super.onStart(); |
| } |
| |
| public void load(String action, Uri lookupUri, Bundle intentExtras) { |
| mAction = action; |
| mLookupUri = lookupUri; |
| mIntentExtras = intentExtras; |
| mAutoAddToDefaultGroup = mIntentExtras != null |
| && mIntentExtras.containsKey(INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY); |
| mNewLocalProfile = mIntentExtras != null |
| && mIntentExtras.getBoolean(INTENT_EXTRA_NEW_LOCAL_PROFILE); |
| } |
| |
| public void setListener(Listener value) { |
| mListener = value; |
| } |
| |
| @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 |
| mLookupUri = savedState.getParcelable(KEY_URI); |
| mAction = savedState.getString(KEY_ACTION); |
| } |
| |
| super.onCreate(savedState); |
| |
| if (savedState == null) { |
| // If savedState is non-null, onRestoreInstanceState() will restore the generator. |
| mViewIdGenerator = new ViewIdGenerator(); |
| } else { |
| // Read state from savedState. No loading involved here |
| mState = savedState.<RawContactDeltaList> getParcelable(KEY_EDIT_STATE); |
| mRawContactIdRequestingPhoto = savedState.getLong( |
| KEY_RAW_CONTACT_ID_REQUESTING_PHOTO); |
| mViewIdGenerator = savedState.getParcelable(KEY_VIEW_ID_GENERATOR); |
| mCurrentPhotoFile = savedState.getString(KEY_CURRENT_PHOTO_FILE); |
| mContactIdForJoin = savedState.getLong(KEY_CONTACT_ID_FOR_JOIN); |
| mContactWritableForJoin = savedState.getBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN); |
| mAggregationSuggestionsRawContactId = savedState.getLong(KEY_SHOW_JOIN_SUGGESTIONS); |
| mEnabled = savedState.getBoolean(KEY_ENABLED); |
| mStatus = savedState.getInt(KEY_STATUS); |
| mNewLocalProfile = savedState.getBoolean(KEY_NEW_LOCAL_PROFILE); |
| mIsUserProfile = savedState.getBoolean(KEY_IS_USER_PROFILE); |
| mUpdatedPhotos = savedState.getParcelable(KEY_UPDATED_PHOTOS); |
| } |
| } |
| |
| public void setData(Contact data) { |
| // If we have already loaded data, we do not want to change it here to not confuse the user |
| if (mState != null) { |
| Log.v(TAG, "Ignoring background change. This will have to be rebased later"); |
| return; |
| } |
| |
| // See if this edit operation needs to be redirected to a custom editor |
| ImmutableList<RawContact> rawContacts = data.getRawContacts(); |
| if (rawContacts.size() == 1) { |
| RawContact rawContact = rawContacts.get(0); |
| String type = rawContact.getAccountTypeString(); |
| String dataSet = rawContact.getDataSet(); |
| AccountType accountType = rawContact.getAccountType(); |
| if (accountType.getEditContactActivityClassName() != null && |
| !accountType.areContactsWritable()) { |
| if (mListener != null) { |
| String name = rawContact.getAccountName(); |
| long rawContactId = rawContact.getId(); |
| mListener.onCustomEditContactActivityRequested( |
| new AccountWithDataSet(name, type, dataSet), |
| ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId), |
| mIntentExtras, true); |
| } |
| return; |
| } |
| } |
| |
| bindEditorsForExistingContact(data); |
| } |
| |
| @Override |
| public void onExternalEditorRequest(AccountWithDataSet account, Uri uri) { |
| mListener.onCustomEditContactActivityRequested(account, uri, null, false); |
| } |
| |
| private void bindEditorsForExistingContact(Contact contact) { |
| setEnabled(true); |
| |
| mState = contact.createRawContactDeltaList(); |
| setIntentExtras(mIntentExtras); |
| mIntentExtras = null; |
| |
| // For user profile, change the contacts query URI |
| mIsUserProfile = contact.isUserProfile(); |
| boolean localProfileExists = false; |
| |
| if (mIsUserProfile) { |
| for (RawContactDelta state : mState) { |
| // For profile contacts, we need a different query URI |
| state.setProfileQueryUri(); |
| // Try to find a local profile contact |
| if (state.getValues().getAsString(RawContacts.ACCOUNT_TYPE) == null) { |
| localProfileExists = true; |
| } |
| } |
| // Editor should always present a local profile for editing |
| if (!localProfileExists) { |
| final RawContact rawContact = new RawContact(mContext); |
| rawContact.setAccountToLocal(); |
| |
| RawContactDelta insert = new RawContactDelta(ValuesDelta.fromAfter( |
| rawContact.getValues())); |
| insert.setProfileQueryUri(); |
| mState.add(insert); |
| } |
| } |
| mRequestFocus = true; |
| |
| bindEditors(); |
| } |
| |
| /** |
| * Merges extras from the intent. |
| */ |
| public void setIntentExtras(Bundle extras) { |
| if (extras == null || extras.size() == 0) { |
| return; |
| } |
| |
| final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); |
| for (RawContactDelta state : mState) { |
| final AccountType type = state.getAccountType(accountTypes); |
| if (type.areContactsWritable()) { |
| // Apply extras to the first writable raw contact only |
| RawContactModifier.parseExtras(mContext, type, state, extras); |
| break; |
| } |
| } |
| } |
| |
| private void selectAccountAndCreateContact() { |
| // 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; |
| } |
| |
| // 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()) { |
| Intent intent = new Intent(mContext, ContactEditorAccountsChangedActivity.class); |
| mStatus = Status.SUB_ACTIVITY; |
| startActivityForResult(intent, REQUEST_CODE_ACCOUNTS_CHANGED); |
| } else { |
| // 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.getDefaultAccount(); |
| if (defaultAccount == null) { |
| createContact(null); |
| } else { |
| createContact(defaultAccount); |
| } |
| } |
| } |
| |
| /** |
| * Create a contact by automatically selecting the first account. If there's no available |
| * account, a device-local contact should be created. |
| */ |
| private void createContact() { |
| final List<AccountWithDataSet> accounts = |
| AccountTypeManager.getInstance(mContext).getAccounts(true); |
| // No Accounts available. Create a phone-local contact. |
| if (accounts.isEmpty()) { |
| createContact(null); |
| return; |
| } |
| |
| // We have an account switcher in "create-account" screen, so don't need to ask a user to |
| // select an account here. |
| createContact(accounts.get(0)); |
| } |
| |
| /** |
| * 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.getAccountType(account != null ? account.type : null, |
| account != null ? account.dataSet : null); |
| |
| if (accountType.getCreateContactActivityClassName() != null) { |
| if (mListener != null) { |
| mListener.onCustomCreateContactActivityRequested(account, mIntentExtras); |
| } |
| } else { |
| bindEditorsForNewContact(account, accountType); |
| } |
| } |
| |
| /** |
| * 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.getAccountType( |
| oldAccount.type, oldAccount.dataSet); |
| AccountType newAccountType = accountTypes.getAccountType( |
| newAccount.type, newAccount.dataSet); |
| |
| if (newAccountType.getCreateContactActivityClassName() != null) { |
| Log.w(TAG, "external activity called in rebind situation"); |
| if (mListener != null) { |
| mListener.onCustomCreateContactActivityRequested(newAccount, mIntentExtras); |
| } |
| } else { |
| mState = null; |
| bindEditorsForNewContact(newAccount, newAccountType, oldState, oldAccountType); |
| } |
| } |
| |
| private void bindEditorsForNewContact(AccountWithDataSet account, |
| final AccountType accountType) { |
| bindEditorsForNewContact(account, accountType, null, null); |
| } |
| |
| private void bindEditorsForNewContact(AccountWithDataSet newAccount, |
| final AccountType newAccountType, RawContactDelta oldState, |
| AccountType oldAccountType) { |
| mStatus = Status.EDITING; |
| |
| final RawContact rawContact = new RawContact(mContext); |
| if (newAccount != null) { |
| rawContact.setAccount(newAccount); |
| } else { |
| rawContact.setAccountToLocal(); |
| } |
| |
| RawContactDelta insert = new RawContactDelta(ValuesDelta.fromAfter(rawContact.getValues())); |
| if (oldState == null) { |
| // Parse any values from incoming intent |
| RawContactModifier.parseExtras(mContext, newAccountType, insert, mIntentExtras); |
| } else { |
| RawContactModifier.migrateStateForNewContact(mContext, oldState, insert, |
| oldAccountType, newAccountType); |
| } |
| |
| // 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(insert, newAccountType, Phone.CONTENT_ITEM_TYPE); |
| RawContactModifier.ensureKindExists(insert, newAccountType, Email.CONTENT_ITEM_TYPE); |
| RawContactModifier.ensureKindExists(insert, newAccountType, Organization.CONTENT_ITEM_TYPE); |
| RawContactModifier.ensureKindExists(insert, newAccountType, Event.CONTENT_ITEM_TYPE); |
| RawContactModifier.ensureKindExists(insert, newAccountType, |
| StructuredPostal.CONTENT_ITEM_TYPE); |
| |
| // Set the correct URI for saving the contact as a profile |
| if (mNewLocalProfile) { |
| insert.setProfileQueryUri(); |
| } |
| |
| if (mState == null) { |
| // Create state if none exists yet |
| mState = RawContactDeltaList.fromSingle(insert); |
| } else { |
| // Add contact onto end of existing state |
| mState.add(insert); |
| } |
| |
| mRequestFocus = true; |
| |
| bindEditors(); |
| } |
| |
| private void bindEditors() { |
| // bindEditors() can only bind views if there is data in mState, so immediately return |
| // if mState is null |
| if (mState == null) { |
| return; |
| } |
| |
| // Sort the editors |
| Collections.sort(mState, mComparator); |
| |
| // Remove any existing editors and rebuild any visible |
| mContent.removeAllViews(); |
| |
| final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( |
| Context.LAYOUT_INFLATER_SERVICE); |
| final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); |
| int numRawContacts = mState.size(); |
| for (int i = 0; i < numRawContacts; i++) { |
| // TODO ensure proper ordering of entities in the list |
| final RawContactDelta rawContactDelta = mState.get(i); |
| if (!rawContactDelta.isVisible()) continue; |
| |
| final AccountType type = rawContactDelta.getAccountType(accountTypes); |
| final long rawContactId = rawContactDelta.getRawContactId(); |
| |
| final BaseRawContactEditorView editor; |
| if (!type.areContactsWritable()) { |
| editor = (BaseRawContactEditorView) inflater.inflate( |
| R.layout.raw_contact_readonly_editor_view, mContent, false); |
| ((RawContactReadOnlyEditorView) editor).setListener(this); |
| } else { |
| editor = (RawContactEditorView) inflater.inflate(R.layout.raw_contact_editor_view, |
| mContent, false); |
| } |
| if (Intent.ACTION_INSERT.equals(mAction) && numRawContacts == 1) { |
| final List<AccountWithDataSet> accounts = |
| AccountTypeManager.getInstance(mContext).getAccounts(true); |
| if (accounts.size() > 1 && !mNewLocalProfile) { |
| addAccountSwitcher(mState.get(0), editor); |
| } else { |
| disableAccountSwitcher(editor); |
| } |
| } else { |
| disableAccountSwitcher(editor); |
| } |
| |
| editor.setEnabled(mEnabled); |
| |
| mContent.addView(editor); |
| |
| editor.setState(rawContactDelta, type, mViewIdGenerator, isEditingUserProfile()); |
| |
| // Set up the photo handler. |
| bindPhotoHandler(editor, type, mState); |
| |
| // If a new photo was chosen but not yet saved, we need to |
| // update the thumbnail to reflect this. |
| Bitmap bitmap = updatedBitmapForRawContact(rawContactId); |
| if (bitmap != null) editor.setPhotoBitmap(bitmap); |
| |
| if (editor instanceof RawContactEditorView) { |
| final Activity activity = getActivity(); |
| final RawContactEditorView rawContactEditor = (RawContactEditorView) editor; |
| EditorListener listener = new EditorListener() { |
| |
| @Override |
| public void onRequest(int request) { |
| if (activity.isFinishing()) { // Make sure activity is still running. |
| return; |
| } |
| if (request == EditorListener.FIELD_CHANGED && !isEditingUserProfile()) { |
| acquireAggregationSuggestions(activity, rawContactEditor); |
| } |
| } |
| |
| @Override |
| public void onDeleteRequested(Editor removedEditor) { |
| } |
| }; |
| |
| final TextFieldsEditorView nameEditor = rawContactEditor.getNameEditor(); |
| if (mRequestFocus) { |
| nameEditor.requestFocus(); |
| mRequestFocus = false; |
| } |
| nameEditor.setEditorListener(listener); |
| |
| final TextFieldsEditorView phoneticNameEditor = |
| rawContactEditor.getPhoneticNameEditor(); |
| phoneticNameEditor.setEditorListener(listener); |
| rawContactEditor.setAutoAddToDefaultGroup(mAutoAddToDefaultGroup); |
| |
| if (rawContactId == mAggregationSuggestionsRawContactId) { |
| acquireAggregationSuggestions(activity, rawContactEditor); |
| } |
| } |
| } |
| |
| mRequestFocus = false; |
| |
| bindGroupMetaData(); |
| |
| // Show editor now that we've loaded state |
| mContent.setVisibility(View.VISIBLE); |
| |
| // Refresh Action Bar as the visibility of the join command |
| // Activity can be null if we have been detached from the Activity |
| final Activity activity = getActivity(); |
| if (activity != null) activity.invalidateOptionsMenu(); |
| } |
| |
| /** |
| * If we've stashed a temporary file containing a contact's new photo, |
| * decode it and return the bitmap. |
| * @param rawContactId identifies the raw-contact whose Bitmap we'll try to return. |
| * @return Bitmap of photo for specified raw-contact, or null |
| */ |
| private Bitmap updatedBitmapForRawContact(long rawContactId) { |
| String path = mUpdatedPhotos.getString(String.valueOf(rawContactId)); |
| return BitmapFactory.decodeFile(path); |
| } |
| |
| private void bindPhotoHandler(BaseRawContactEditorView editor, AccountType type, |
| RawContactDeltaList state) { |
| final int mode; |
| if (type.areContactsWritable()) { |
| if (editor.hasSetPhoto()) { |
| if (hasMoreThanOnePhoto()) { |
| mode = PhotoActionPopup.Modes.PHOTO_ALLOW_PRIMARY; |
| } else { |
| mode = PhotoActionPopup.Modes.PHOTO_DISALLOW_PRIMARY; |
| } |
| } else { |
| mode = PhotoActionPopup.Modes.NO_PHOTO; |
| } |
| } else { |
| if (editor.hasSetPhoto() && hasMoreThanOnePhoto()) { |
| mode = PhotoActionPopup.Modes.READ_ONLY_ALLOW_PRIMARY; |
| } else { |
| // Read-only and either no photo or the only photo ==> no options |
| editor.getPhotoEditor().setEditorListener(null); |
| return; |
| } |
| } |
| final PhotoHandler photoHandler = new PhotoHandler(mContext, editor, mode, state); |
| editor.getPhotoEditor().setEditorListener( |
| (PhotoHandler.PhotoEditorListener) photoHandler.getListener()); |
| |
| // Note a newly created raw contact gets some random negative ID, so any value is valid |
| // here. (i.e. don't check against -1 or anything.) |
| if (mRawContactIdRequestingPhoto == editor.getRawContactId()) { |
| mCurrentPhotoHandler = photoHandler; |
| } |
| } |
| |
| private void bindGroupMetaData() { |
| if (mGroupMetaData == null) { |
| return; |
| } |
| |
| int editorCount = mContent.getChildCount(); |
| for (int i = 0; i < editorCount; i++) { |
| BaseRawContactEditorView editor = (BaseRawContactEditorView) mContent.getChildAt(i); |
| editor.setGroupMetaData(mGroupMetaData); |
| } |
| } |
| |
| private void saveDefaultAccountIfNecessary() { |
| // Verify that this is a newly created contact, that the contact is composed of only |
| // 1 raw contact, and that the contact is not a user profile. |
| if (!Intent.ACTION_INSERT.equals(mAction) && mState.size() == 1 && |
| !isEditingUserProfile()) { |
| return; |
| } |
| |
| // Find the associated account for this contact (retrieve it here because there are |
| // multiple paths to creating a contact and this ensures we always have the correct |
| // account). |
| final RawContactDelta rawContactDelta = mState.get(0); |
| String name = rawContactDelta.getAccountName(); |
| String type = rawContactDelta.getAccountType(); |
| String dataSet = rawContactDelta.getDataSet(); |
| |
| AccountWithDataSet account = (name == null || type == null) ? null : |
| new AccountWithDataSet(name, type, dataSet); |
| mEditorUtils.saveDefaultAndAllAccounts(account); |
| } |
| |
| private void addAccountSwitcher( |
| final RawContactDelta currentState, BaseRawContactEditorView editor) { |
| final AccountWithDataSet currentAccount = new AccountWithDataSet( |
| currentState.getAccountName(), |
| currentState.getAccountType(), |
| currentState.getDataSet()); |
| final View accountView = editor.findViewById(R.id.account); |
| final View anchorView = editor.findViewById(R.id.account_container); |
| accountView.setOnClickListener(new View.OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| final ListPopupWindow popup = new ListPopupWindow(mContext, null); |
| final AccountsListAdapter adapter = |
| new AccountsListAdapter(mContext, |
| AccountListFilter.ACCOUNTS_CONTACT_WRITABLE, currentAccount); |
| popup.setWidth(anchorView.getWidth()); |
| popup.setAnchorView(anchorView); |
| popup.setAdapter(adapter); |
| popup.setModal(true); |
| popup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); |
| popup.setOnItemClickListener(new AdapterView.OnItemClickListener() { |
| @Override |
| public void onItemClick(AdapterView<?> parent, View view, int position, |
| long id) { |
| popup.dismiss(); |
| AccountWithDataSet newAccount = adapter.getItem(position); |
| if (!newAccount.equals(currentAccount)) { |
| rebindEditorsForNewContact(currentState, currentAccount, newAccount); |
| } |
| } |
| }); |
| popup.show(); |
| } |
| }); |
| } |
| |
| private void disableAccountSwitcher(BaseRawContactEditorView editor) { |
| // Remove the pressed state from the account header because the user cannot switch accounts |
| // on an existing contact |
| final View accountView = editor.findViewById(R.id.account); |
| accountView.setBackground(null); |
| accountView.setEnabled(false); |
| } |
| |
| @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 doneMenu = menu.findItem(R.id.menu_done); |
| final MenuItem splitMenu = menu.findItem(R.id.menu_split); |
| final MenuItem joinMenu = menu.findItem(R.id.menu_join); |
| final MenuItem helpMenu = menu.findItem(R.id.menu_help); |
| |
| // Set visibility of menus |
| doneMenu.setVisible(false); |
| |
| // Split only if more than one raw profile and not a user profile |
| splitMenu.setVisible(mState != null && mState.size() > 1 && !isEditingUserProfile()); |
| |
| // Cannot join a user profile |
| joinMenu.setVisible(!isEditingUserProfile()); |
| |
| // help menu depending on whether this is inserting or editing |
| if (Intent.ACTION_INSERT.equals(mAction)) { |
| // inserting |
| HelpUtils.prepareHelpMenuItem(mContext, helpMenu, R.string.help_url_people_add); |
| } else if (Intent.ACTION_EDIT.equals(mAction)) { |
| // editing |
| HelpUtils.prepareHelpMenuItem(mContext, helpMenu, R.string.help_url_people_edit); |
| } else { |
| // something else, so don't show the help menu |
| helpMenu.setVisible(false); |
| } |
| |
| int size = menu.size(); |
| for (int i = 0; i < size; i++) { |
| menu.getItem(i).setEnabled(mEnabled); |
| } |
| } |
| |
| @Override |
| public boolean onOptionsItemSelected(MenuItem item) { |
| switch (item.getItemId()) { |
| case R.id.menu_done: |
| return save(SaveMode.CLOSE); |
| case R.id.menu_discard: |
| return revert(); |
| case R.id.menu_split: |
| return doSplitContactAction(); |
| case R.id.menu_join: |
| return doJoinContactAction(); |
| } |
| return false; |
| } |
| |
| private boolean doSplitContactAction() { |
| if (!hasValidState()) return false; |
| |
| final SplitContactConfirmationDialogFragment dialog = |
| new SplitContactConfirmationDialogFragment(); |
| dialog.setTargetFragment(this, 0); |
| dialog.show(getFragmentManager(), SplitContactConfirmationDialogFragment.TAG); |
| return true; |
| } |
| |
| private boolean doJoinContactAction() { |
| if (!hasValidState()) { |
| 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; |
| } |
| |
| return save(SaveMode.JOIN); |
| } |
| |
| /** |
| * Check if our internal {@link #mState} is valid, usually checked before |
| * performing user actions. |
| */ |
| private boolean hasValidState() { |
| return mState != null && mState.size() > 0; |
| } |
| |
| /** |
| * Return true if there are any edits to the current contact which need to |
| * be saved. |
| */ |
| private boolean hasPendingChanges() { |
| final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); |
| return RawContactModifier.hasChanges(mState, accountTypes); |
| } |
| |
| /** |
| * Saves or creates the contact based on the mode, and if successful |
| * finishes the activity. |
| */ |
| 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.SPLIT) { |
| getLoaderManager().destroyLoader(LOADER_DATA); |
| } |
| |
| 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(false, saveMode, mLookupUri != null, mLookupUri); |
| return true; |
| } |
| |
| setEnabled(false); |
| |
| // Store account as default account, only if this is a new contact |
| saveDefaultAccountIfNecessary(); |
| |
| // Save contact |
| Intent intent = ContactSaveService.createSaveContactIntent(mContext, mState, |
| SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(), |
| ((Activity)mContext).getClass(), ContactEditorActivity.ACTION_SAVE_COMPLETED, |
| mUpdatedPhotos); |
| mContext.startService(intent); |
| |
| // Don't try to save the same photos twice. |
| mUpdatedPhotos = new Bundle(); |
| |
| return true; |
| } |
| |
| public static class CancelEditDialogFragment extends DialogFragment { |
| |
| public static void show(ContactEditorFragment fragment) { |
| CancelEditDialogFragment dialog = new CancelEditDialogFragment(); |
| dialog.setTargetFragment(fragment, 0); |
| dialog.show(fragment.getFragmentManager(), "cancelEditor"); |
| } |
| |
| @Override |
| public Dialog onCreateDialog(Bundle savedInstanceState) { |
| AlertDialog dialog = new AlertDialog.Builder(getActivity()) |
| .setIconAttribute(android.R.attr.alertDialogIcon) |
| .setMessage(R.string.cancel_confirmation_dialog_message) |
| .setPositiveButton(android.R.string.ok, |
| new DialogInterface.OnClickListener() { |
| @Override |
| public void onClick(DialogInterface dialogInterface, int whichButton) { |
| ((ContactEditorFragment)getTargetFragment()).doRevertAction(); |
| } |
| } |
| ) |
| .setNegativeButton(android.R.string.cancel, null) |
| .create(); |
| return dialog; |
| } |
| } |
| |
| private boolean revert() { |
| if (mState == null || !hasPendingChanges()) { |
| doRevertAction(); |
| } else { |
| CancelEditDialogFragment.show(this); |
| } |
| return true; |
| } |
| |
| private void doRevertAction() { |
| // When this Fragment is closed we don't want it to auto-save |
| mStatus = Status.CLOSING; |
| if (mListener != null) mListener.onReverted(); |
| } |
| |
| public void doSaveAction() { |
| save(SaveMode.CLOSE); |
| } |
| |
| public void onJoinCompleted(Uri uri) { |
| onSaveCompleted(false, SaveMode.RELOAD, uri != null, uri); |
| } |
| |
| public void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded, |
| Uri contactLookupUri) { |
| if (hadChanges) { |
| if (saveSucceeded) { |
| if (saveMode != SaveMode.JOIN) { |
| Toast.makeText(mContext, R.string.contactSavedToast, Toast.LENGTH_SHORT).show(); |
| } |
| } else { |
| Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show(); |
| } |
| } |
| switch (saveMode) { |
| case SaveMode.CLOSE: |
| case SaveMode.HOME: |
| final Intent resultIntent; |
| if (saveSucceeded && contactLookupUri != null) { |
| final String requestAuthority = |
| mLookupUri == null ? null : mLookupUri.getAuthority(); |
| |
| final String legacyAuthority = "contacts"; |
| |
| resultIntent = new Intent(); |
| resultIntent.setAction(Intent.ACTION_VIEW); |
| if (legacyAuthority.equals(requestAuthority)) { |
| // Build legacy Uri when requested by caller |
| final long contactId = ContentUris.parseId(Contacts.lookupContact( |
| mContext.getContentResolver(), contactLookupUri)); |
| final Uri legacyContentUri = Uri.parse("content://contacts/people"); |
| final Uri legacyUri = ContentUris.withAppendedId( |
| legacyContentUri, contactId); |
| resultIntent.setData(legacyUri); |
| } else { |
| // Otherwise pass back a lookup-style Uri |
| resultIntent.setData(contactLookupUri); |
| } |
| |
| } else { |
| resultIntent = null; |
| } |
| // It is already saved, so prevent that it is saved again |
| mStatus = Status.CLOSING; |
| if (mListener != null) mListener.onSaveFinished(resultIntent); |
| break; |
| |
| case SaveMode.RELOAD: |
| case SaveMode.JOIN: |
| if (saveSucceeded && contactLookupUri != null) { |
| // If it was a JOIN, we are now ready to bring up the join activity. |
| if (saveMode == SaveMode.JOIN && hasValidState()) { |
| showJoinAggregateActivity(contactLookupUri); |
| } |
| |
| // 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 = null; |
| load(Intent.ACTION_EDIT, contactLookupUri, null); |
| mStatus = Status.LOADING; |
| getLoaderManager().restartLoader(LOADER_DATA, null, mDataLoaderListener); |
| } |
| break; |
| |
| case SaveMode.SPLIT: |
| mStatus = Status.CLOSING; |
| if (mListener != null) { |
| mListener.onContactSplit(contactLookupUri); |
| } else { |
| 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); |
| mContactWritableForJoin = isContactWritable(); |
| final Intent intent = new Intent(JoinContactActivity.JOIN_CONTACT); |
| intent.putExtra(JoinContactActivity.EXTRA_TARGET_CONTACT_ID, mContactIdForJoin); |
| startActivityForResult(intent, REQUEST_CODE_JOIN); |
| } |
| |
| /** |
| * Performs aggregation with the contact selected by the user from suggestions or A-Z list. |
| */ |
| private void joinAggregate(final long contactId) { |
| Intent intent = ContactSaveService.createJoinContactsIntent(mContext, mContactIdForJoin, |
| contactId, mContactWritableForJoin, |
| ContactEditorActivity.class, ContactEditorActivity.ACTION_JOIN_COMPLETED); |
| mContext.startService(intent); |
| } |
| |
| /** |
| * Returns true if there is at least one writable raw contact in the current contact. |
| */ |
| private boolean isContactWritable() { |
| final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); |
| int size = mState.size(); |
| for (int i = 0; i < size; i++) { |
| RawContactDelta entity = mState.get(i); |
| final AccountType type = entity.getAccountType(accountTypes); |
| if (type.areContactsWritable()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private boolean isEditingUserProfile() { |
| return mNewLocalProfile || mIsUserProfile; |
| } |
| |
| public static interface Listener { |
| /** |
| * Contact was not found, so somehow close this fragment. This is raised after a contact |
| * is removed via Menu/Delete (unless it was a new contact) |
| */ |
| 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 contact (a suggestion from the |
| * aggregation engine). |
| */ |
| void onEditOtherContactRequested( |
| Uri contactLookupUri, ArrayList<ContentValues> contentValues); |
| |
| /** |
| * Contact is being created for an external account that provides its own |
| * new contact activity. |
| */ |
| void onCustomCreateContactActivityRequested(AccountWithDataSet account, |
| Bundle intentExtras); |
| |
| /** |
| * The edited raw contact belongs to an external account that provides |
| * its own edit activity. |
| * |
| * @param redirect indicates that the current editor should be closed |
| * before the custom editor is shown. |
| */ |
| void onCustomEditContactActivityRequested(AccountWithDataSet account, Uri rawContactUri, |
| Bundle intentExtras, boolean redirect); |
| } |
| |
| private class EntityDeltaComparator implements Comparator<RawContactDelta> { |
| /** |
| * Compare EntityDeltas for sorting the stack of editors. |
| */ |
| @Override |
| public int compare(RawContactDelta one, RawContactDelta two) { |
| // Check direct equality |
| if (one.equals(two)) { |
| return 0; |
| } |
| |
| final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); |
| String accountType1 = one.getValues().getAsString(RawContacts.ACCOUNT_TYPE); |
| String dataSet1 = one.getValues().getAsString(RawContacts.DATA_SET); |
| final AccountType type1 = accountTypes.getAccountType(accountType1, dataSet1); |
| String accountType2 = two.getValues().getAsString(RawContacts.ACCOUNT_TYPE); |
| String dataSet2 = two.getValues().getAsString(RawContacts.DATA_SET); |
| final AccountType type2 = accountTypes.getAccountType(accountType2, dataSet2); |
| |
| // Check read-only |
| if (!type1.areContactsWritable() && type2.areContactsWritable()) { |
| return 1; |
| } else if (type1.areContactsWritable() && !type2.areContactsWritable()) { |
| return -1; |
| } |
| |
| // Check account type |
| boolean skipAccountTypeCheck = false; |
| boolean isGoogleAccount1 = type1 instanceof GoogleAccountType; |
| boolean isGoogleAccount2 = type2 instanceof GoogleAccountType; |
| if (isGoogleAccount1 && !isGoogleAccount2) { |
| return -1; |
| } else if (!isGoogleAccount1 && isGoogleAccount2) { |
| return 1; |
| } else if (isGoogleAccount1 && isGoogleAccount2){ |
| skipAccountTypeCheck = true; |
| } |
| |
| int value; |
| if (!skipAccountTypeCheck) { |
| if (type1.accountType == null) { |
| return 1; |
| } |
| value = type1.accountType.compareTo(type2.accountType); |
| if (value != 0) { |
| return value; |
| } else { |
| // Fall back to data set. |
| if (type1.dataSet != null) { |
| value = type1.dataSet.compareTo(type2.dataSet); |
| if (value != 0) { |
| return value; |
| } |
| } else if (type2.dataSet != null) { |
| return 1; |
| } |
| } |
| } |
| |
| // Check account name |
| String oneAccount = one.getAccountName(); |
| if (oneAccount == null) oneAccount = ""; |
| String twoAccount = two.getAccountName(); |
| if (twoAccount == null) twoAccount = ""; |
| value = oneAccount.compareTo(twoAccount); |
| if (value != 0) { |
| return value; |
| } |
| |
| // Both are in the same account, fall back to contact ID |
| Long oneId = one.getRawContactId(); |
| Long twoId = two.getRawContactId(); |
| if (oneId == null) { |
| return -1; |
| } else if (twoId == null) { |
| return 1; |
| } |
| |
| return (int)(oneId - twoId); |
| } |
| } |
| |
| /** |
| * Returns the contact ID for the currently edited contact or 0 if the contact is new. |
| */ |
| protected long getContactId() { |
| if (mState != null) { |
| for (RawContactDelta rawContact : mState) { |
| Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID); |
| if (contactId != null) { |
| return contactId; |
| } |
| } |
| } |
| return 0; |
| } |
| |
| /** |
| * Triggers an asynchronous search for aggregation suggestions. |
| */ |
| private void acquireAggregationSuggestions(Context context, |
| RawContactEditorView rawContactEditor) { |
| long rawContactId = rawContactEditor.getRawContactId(); |
| if (mAggregationSuggestionsRawContactId != rawContactId |
| && mAggregationSuggestionView != null) { |
| mAggregationSuggestionView.setVisibility(View.GONE); |
| mAggregationSuggestionView = null; |
| mAggregationSuggestionEngine.reset(); |
| } |
| |
| mAggregationSuggestionsRawContactId = rawContactId; |
| |
| if (mAggregationSuggestionEngine == null) { |
| mAggregationSuggestionEngine = new AggregationSuggestionEngine(context); |
| mAggregationSuggestionEngine.setListener(this); |
| mAggregationSuggestionEngine.start(); |
| } |
| |
| mAggregationSuggestionEngine.setContactId(getContactId()); |
| |
| LabeledEditorView nameEditor = rawContactEditor.getNameEditor(); |
| mAggregationSuggestionEngine.onNameChange(nameEditor.getValues()); |
| } |
| |
| @Override |
| public void onAggregationSuggestionChange() { |
| Activity activity = getActivity(); |
| if ((activity != null && activity.isFinishing()) |
| || !isVisible() || mState == null || mStatus != Status.EDITING) { |
| return; |
| } |
| |
| if (mAggregationSuggestionPopup != null && mAggregationSuggestionPopup.isShowing()) { |
| mAggregationSuggestionPopup.dismiss(); |
| } |
| |
| if (mAggregationSuggestionEngine.getSuggestedContactCount() == 0) { |
| return; |
| } |
| |
| final RawContactEditorView rawContactView = |
| (RawContactEditorView)getRawContactEditorView(mAggregationSuggestionsRawContactId); |
| if (rawContactView == null) { |
| return; // Raw contact deleted? |
| } |
| final View anchorView = rawContactView.findViewById(R.id.anchor_view); |
| mAggregationSuggestionPopup = new ListPopupWindow(mContext, null); |
| mAggregationSuggestionPopup.setAnchorView(anchorView); |
| mAggregationSuggestionPopup.setWidth(anchorView.getWidth()); |
| mAggregationSuggestionPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED); |
| mAggregationSuggestionPopup.setAdapter( |
| new AggregationSuggestionAdapter(getActivity(), |
| mState.size() == 1 && mState.get(0).isContactInsert(), |
| this, mAggregationSuggestionEngine.getSuggestions())); |
| mAggregationSuggestionPopup.setOnItemClickListener(mAggregationSuggestionItemClickListener); |
| mAggregationSuggestionPopup.show(); |
| } |
| |
| @Override |
| public void onJoinAction(long contactId, List<Long> rawContactIdList) { |
| long rawContactIds[] = new long[rawContactIdList.size()]; |
| for (int i = 0; i < rawContactIds.length; i++) { |
| rawContactIds[i] = rawContactIdList.get(i); |
| } |
| JoinSuggestedContactDialogFragment dialog = |
| new JoinSuggestedContactDialogFragment(); |
| Bundle args = new Bundle(); |
| args.putLongArray("rawContactIds", rawContactIds); |
| dialog.setArguments(args); |
| dialog.setTargetFragment(this, 0); |
| try { |
| dialog.show(getFragmentManager(), "join"); |
| } catch (Exception ex) { |
| // No problem - the activity is no longer available to display the dialog |
| } |
| } |
| |
| public static class JoinSuggestedContactDialogFragment extends DialogFragment { |
| |
| @Override |
| public Dialog onCreateDialog(Bundle savedInstanceState) { |
| return new AlertDialog.Builder(getActivity()) |
| .setIconAttribute(android.R.attr.alertDialogIcon) |
| .setMessage(R.string.aggregation_suggestion_join_dialog_message) |
| .setPositiveButton(android.R.string.yes, |
| new DialogInterface.OnClickListener() { |
| @Override |
| public void onClick(DialogInterface dialog, int whichButton) { |
| ContactEditorFragment targetFragment = |
| (ContactEditorFragment) getTargetFragment(); |
| long rawContactIds[] = |
| getArguments().getLongArray("rawContactIds"); |
| targetFragment.doJoinSuggestedContact(rawContactIds); |
| } |
| } |
| ) |
| .setNegativeButton(android.R.string.no, null) |
| .create(); |
| } |
| } |
| |
| /** |
| * Joins the suggested contact (specified by the id's of constituent raw |
| * contacts), save all changes, and stay in the editor. |
| */ |
| protected void doJoinSuggestedContact(long[] rawContactIds) { |
| if (!hasValidState() || mStatus != Status.EDITING) { |
| return; |
| } |
| |
| mState.setJoinWithRawContacts(rawContactIds); |
| save(SaveMode.RELOAD); |
| } |
| |
| @Override |
| public void onEditAction(Uri contactLookupUri) { |
| SuggestionEditConfirmationDialogFragment dialog = |
| new SuggestionEditConfirmationDialogFragment(); |
| Bundle args = new Bundle(); |
| args.putParcelable("contactUri", contactLookupUri); |
| dialog.setArguments(args); |
| dialog.setTargetFragment(this, 0); |
| dialog.show(getFragmentManager(), "edit"); |
| } |
| |
| public static class SuggestionEditConfirmationDialogFragment extends DialogFragment { |
| |
| @Override |
| public Dialog onCreateDialog(Bundle savedInstanceState) { |
| return new AlertDialog.Builder(getActivity()) |
| .setIconAttribute(android.R.attr.alertDialogIcon) |
| .setMessage(R.string.aggregation_suggestion_edit_dialog_message) |
| .setPositiveButton(android.R.string.yes, |
| new DialogInterface.OnClickListener() { |
| @Override |
| public void onClick(DialogInterface dialog, int whichButton) { |
| ContactEditorFragment targetFragment = |
| (ContactEditorFragment) getTargetFragment(); |
| Uri contactUri = |
| getArguments().getParcelable("contactUri"); |
| targetFragment.doEditSuggestedContact(contactUri); |
| } |
| } |
| ) |
| .setNegativeButton(android.R.string.no, null) |
| .create(); |
| } |
| } |
| |
| /** |
| * Abandons the currently edited contact and switches to editing the suggested |
| * one, transferring all the data there |
| */ |
| protected void doEditSuggestedContact(Uri contactUri) { |
| if (mListener != null) { |
| // make sure we don't save this contact when closing down |
| mStatus = Status.CLOSING; |
| mListener.onEditOtherContactRequested( |
| contactUri, mState.get(0).getContentValues()); |
| } |
| } |
| |
| public void setAggregationSuggestionViewEnabled(boolean enabled) { |
| if (mAggregationSuggestionView == null) { |
| return; |
| } |
| |
| LinearLayout itemList = (LinearLayout) mAggregationSuggestionView.findViewById( |
| R.id.aggregation_suggestions); |
| int count = itemList.getChildCount(); |
| for (int i = 0; i < count; i++) { |
| itemList.getChildAt(i).setEnabled(enabled); |
| } |
| } |
| |
| @Override |
| public void onSaveInstanceState(Bundle outState) { |
| outState.putParcelable(KEY_URI, mLookupUri); |
| outState.putString(KEY_ACTION, mAction); |
| |
| if (hasValidState()) { |
| // Store entities with modifications |
| outState.putParcelable(KEY_EDIT_STATE, mState); |
| } |
| outState.putLong(KEY_RAW_CONTACT_ID_REQUESTING_PHOTO, mRawContactIdRequestingPhoto); |
| outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator); |
| outState.putString(KEY_CURRENT_PHOTO_FILE, mCurrentPhotoFile); |
| outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin); |
| outState.putBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN, mContactWritableForJoin); |
| outState.putLong(KEY_SHOW_JOIN_SUGGESTIONS, mAggregationSuggestionsRawContactId); |
| outState.putBoolean(KEY_ENABLED, mEnabled); |
| outState.putBoolean(KEY_NEW_LOCAL_PROFILE, mNewLocalProfile); |
| outState.putBoolean(KEY_IS_USER_PROFILE, mIsUserProfile); |
| outState.putInt(KEY_STATUS, mStatus); |
| outState.putParcelable(KEY_UPDATED_PHOTOS, mUpdatedPhotos); |
| |
| super.onSaveInstanceState(outState); |
| } |
| |
| @Override |
| public void onActivityResult(int requestCode, int resultCode, Intent data) { |
| if (mStatus == Status.SUB_ACTIVITY) { |
| mStatus = Status.EDITING; |
| } |
| |
| // See if the photo selection handler handles this result. |
| if (mCurrentPhotoHandler != null && mCurrentPhotoHandler.handlePhotoActivityResult( |
| requestCode, resultCode, data)) { |
| return; |
| } |
| |
| 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()); |
| joinAggregate(contactId); |
| } |
| break; |
| } |
| case REQUEST_CODE_ACCOUNTS_CHANGED: { |
| // Bail if the account selector was not successful. |
| if (resultCode != Activity.RESULT_OK) { |
| mListener.onReverted(); |
| return; |
| } |
| // If there's an account specified, use it. |
| if (data != null) { |
| AccountWithDataSet account = data.getParcelableExtra(Intents.Insert.ACCOUNT); |
| if (account != null) { |
| createContact(account); |
| return; |
| } |
| } |
| // If there isn't an account specified, then this is likely a phone-local |
| // contact, so we should continue setting up the editor by automatically selecting |
| // the most appropriate account. |
| createContact(); |
| break; |
| } |
| } |
| } |
| |
| /** |
| * Sets the photo stored in mPhoto and writes it to the RawContact with the given id |
| */ |
| private void setPhoto(long rawContact, Bitmap photo, String photoFile) { |
| BaseRawContactEditorView requestingEditor = getRawContactEditorView(rawContact); |
| |
| if (photo == null || photo.getHeight() < 0 || photo.getWidth() < 0) { |
| // This is unexpected. |
| Log.w(TAG, "Invalid bitmap passed to setPhoto()"); |
| } |
| |
| if (requestingEditor != null) { |
| requestingEditor.setPhotoBitmap(photo); |
| } else { |
| Log.w(TAG, "The contact that requested the photo is no longer present."); |
| } |
| |
| final String croppedPhotoPath = |
| ContactPhotoUtils.pathForCroppedPhoto(mContext, mCurrentPhotoFile); |
| mUpdatedPhotos.putString(String.valueOf(rawContact), croppedPhotoPath); |
| } |
| |
| /** |
| * Finds raw contact editor view for the given rawContactId. |
| */ |
| public BaseRawContactEditorView getRawContactEditorView(long rawContactId) { |
| for (int i = 0; i < mContent.getChildCount(); i++) { |
| final View childView = mContent.getChildAt(i); |
| if (childView instanceof BaseRawContactEditorView) { |
| final BaseRawContactEditorView editor = (BaseRawContactEditorView) childView; |
| if (editor.getRawContactId() == rawContactId) { |
| return editor; |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns true if there is currently more than one photo on screen. |
| */ |
| private boolean hasMoreThanOnePhoto() { |
| int countWithPicture = 0; |
| final int numEntities = mState.size(); |
| for (int i = 0; i < numEntities; i++) { |
| final RawContactDelta entity = mState.get(i); |
| if (entity.isVisible()) { |
| final ValuesDelta primary = entity.getPrimaryEntry(Photo.CONTENT_ITEM_TYPE); |
| if (primary != null && primary.getPhoto() != null) { |
| countWithPicture++; |
| } else { |
| final long rawContactId = entity.getRawContactId(); |
| final String path = mUpdatedPhotos.getString(String.valueOf(rawContactId)); |
| if (path != null) { |
| final File file = new File(path); |
| if (file.exists()) { |
| countWithPicture++; |
| } |
| } |
| } |
| |
| if (countWithPicture > 1) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * The listener for the data loader |
| */ |
| private final LoaderManager.LoaderCallbacks<Contact> mDataLoaderListener = |
| new LoaderCallbacks<Contact>() { |
| @Override |
| public Loader<Contact> onCreateLoader(int id, Bundle args) { |
| mLoaderStartTime = SystemClock.elapsedRealtime(); |
| return new ContactLoader(mContext, mLookupUri, true); |
| } |
| |
| @Override |
| public void onLoadFinished(Loader<Contact> loader, Contact data) { |
| final long loaderCurrentTime = SystemClock.elapsedRealtime(); |
| Log.v(TAG, "Time needed for loading: " + (loaderCurrentTime-mLoaderStartTime)); |
| if (!data.isLoaded()) { |
| // Item has been deleted |
| Log.i(TAG, "No contact found. Closing activity"); |
| if (mListener != null) mListener.onContactNotFound(); |
| return; |
| } |
| |
| mStatus = Status.EDITING; |
| mLookupUri = data.getLookupUri(); |
| final long setDataStartTime = SystemClock.elapsedRealtime(); |
| setData(data); |
| final long setDataEndTime = SystemClock.elapsedRealtime(); |
| |
| Log.v(TAG, "Time needed for setting UI: " + (setDataEndTime-setDataStartTime)); |
| } |
| |
| @Override |
| public void onLoaderReset(Loader<Contact> loader) { |
| } |
| }; |
| |
| /** |
| * The listener for the group meta data loader for all groups. |
| */ |
| private final LoaderManager.LoaderCallbacks<Cursor> mGroupLoaderListener = |
| new LoaderCallbacks<Cursor>() { |
| |
| @Override |
| public CursorLoader onCreateLoader(int id, Bundle args) { |
| return new GroupMetaDataLoader(mContext, Groups.CONTENT_URI); |
| } |
| |
| @Override |
| public void onLoadFinished(Loader<Cursor> loader, Cursor data) { |
| mGroupMetaData = data; |
| bindGroupMetaData(); |
| } |
| |
| @Override |
| public void onLoaderReset(Loader<Cursor> loader) { |
| } |
| }; |
| |
| @Override |
| public void onSplitContactConfirmed() { |
| if (mState == null) { |
| // 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; |
| } |
| |
| mState.markRawContactsForSplitting(); |
| save(SaveMode.SPLIT); |
| } |
| |
| /** |
| * Custom photo handler for the editor. The inner listener that this creates also has a |
| * reference to the editor and acts as an {@link EditorListener}, and uses that editor to hold |
| * state information in several of the listener methods. |
| */ |
| private final class PhotoHandler extends PhotoSelectionHandler { |
| |
| final long mRawContactId; |
| private final BaseRawContactEditorView mEditor; |
| private final PhotoActionListener mPhotoEditorListener; |
| |
| public PhotoHandler(Context context, BaseRawContactEditorView editor, int photoMode, |
| RawContactDeltaList state) { |
| super(context, editor.getPhotoEditor(), photoMode, false, state); |
| mEditor = editor; |
| mRawContactId = editor.getRawContactId(); |
| mPhotoEditorListener = new PhotoEditorListener(); |
| } |
| |
| @Override |
| public PhotoActionListener getListener() { |
| return mPhotoEditorListener; |
| } |
| |
| @Override |
| public void startPhotoActivity(Intent intent, int requestCode, String photoFile) { |
| mRawContactIdRequestingPhoto = mEditor.getRawContactId(); |
| mCurrentPhotoHandler = this; |
| mStatus = Status.SUB_ACTIVITY; |
| mCurrentPhotoFile = photoFile; |
| ContactEditorFragment.this.startActivityForResult(intent, requestCode); |
| } |
| |
| private final class PhotoEditorListener extends PhotoSelectionHandler.PhotoActionListener |
| implements EditorListener { |
| |
| @Override |
| public void onRequest(int request) { |
| if (!hasValidState()) return; |
| |
| if (request == EditorListener.REQUEST_PICK_PHOTO) { |
| onClick(mEditor.getPhotoEditor()); |
| } |
| } |
| |
| @Override |
| public void onDeleteRequested(Editor removedEditor) { |
| // The picture cannot be deleted, it can only be removed, which is handled by |
| // onRemovePictureChosen() |
| } |
| |
| /** |
| * User has chosen to set the selected photo as the (super) primary photo |
| */ |
| @Override |
| public void onUseAsPrimaryChosen() { |
| // Set the IsSuperPrimary for each editor |
| int count = mContent.getChildCount(); |
| for (int i = 0; i < count; i++) { |
| final View childView = mContent.getChildAt(i); |
| if (childView instanceof BaseRawContactEditorView) { |
| final BaseRawContactEditorView editor = |
| (BaseRawContactEditorView) childView; |
| final PhotoEditorView photoEditor = editor.getPhotoEditor(); |
| photoEditor.setSuperPrimary(editor == mEditor); |
| } |
| } |
| bindEditors(); |
| } |
| |
| /** |
| * User has chosen to remove a picture |
| */ |
| @Override |
| public void onRemovePictureChosen() { |
| mEditor.setPhotoBitmap(null); |
| |
| // Prevent bitmap from being restored if rotate the device. |
| // (only if we first chose a new photo before removing it) |
| mUpdatedPhotos.remove(String.valueOf(mRawContactId)); |
| bindEditors(); |
| } |
| |
| @Override |
| public void onPhotoSelected(Bitmap bitmap) { |
| setPhoto(mRawContactId, bitmap, mCurrentPhotoFile); |
| mCurrentPhotoHandler = null; |
| bindEditors(); |
| } |
| |
| @Override |
| public String getCurrentPhotoFile() { |
| return mCurrentPhotoFile; |
| } |
| |
| @Override |
| public void onPhotoSelectionDismissed() { |
| // Nothing to do. |
| } |
| } |
| } |
| } |