Home | History | Annotate | Download | only in editor
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 
     17 package com.android.contacts.editor;
     18 
     19 import android.accounts.Account;
     20 import android.app.Activity;
     21 import android.app.AlertDialog;
     22 import android.app.Dialog;
     23 import android.app.DialogFragment;
     24 import android.app.Fragment;
     25 import android.app.LoaderManager;
     26 import android.app.LoaderManager.LoaderCallbacks;
     27 import android.content.ActivityNotFoundException;
     28 import android.content.ContentUris;
     29 import android.content.ContentValues;
     30 import android.content.Context;
     31 import android.content.CursorLoader;
     32 import android.content.DialogInterface;
     33 import android.content.Intent;
     34 import android.content.Loader;
     35 import android.database.Cursor;
     36 import android.graphics.Bitmap;
     37 import android.graphics.BitmapFactory;
     38 import android.graphics.Rect;
     39 import android.media.RingtoneManager;
     40 import android.net.Uri;
     41 import android.os.Bundle;
     42 import android.os.SystemClock;
     43 import android.provider.ContactsContract.CommonDataKinds.Email;
     44 import android.provider.ContactsContract.CommonDataKinds.Event;
     45 import android.provider.ContactsContract.CommonDataKinds.Organization;
     46 import android.provider.ContactsContract.CommonDataKinds.Phone;
     47 import android.provider.ContactsContract.CommonDataKinds.Photo;
     48 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
     49 import android.provider.ContactsContract.Contacts;
     50 import android.provider.ContactsContract.Groups;
     51 import android.provider.ContactsContract.Intents;
     52 import android.provider.ContactsContract.Intents.UI;
     53 import android.provider.ContactsContract.QuickContact;
     54 import android.provider.ContactsContract.RawContacts;
     55 import android.text.TextUtils;
     56 import android.util.Log;
     57 import android.view.LayoutInflater;
     58 import android.view.Menu;
     59 import android.view.MenuInflater;
     60 import android.view.MenuItem;
     61 import android.view.View;
     62 import android.view.ViewGroup;
     63 import android.widget.AdapterView;
     64 import android.widget.AdapterView.OnItemClickListener;
     65 import android.widget.BaseAdapter;
     66 import android.widget.LinearLayout;
     67 import android.widget.ListPopupWindow;
     68 import android.widget.Toast;
     69 
     70 import com.android.contacts.ContactSaveService;
     71 import com.android.contacts.GroupMetaDataLoader;
     72 import com.android.contacts.R;
     73 import com.android.contacts.activities.ContactEditorAccountsChangedActivity;
     74 import com.android.contacts.activities.ContactEditorActivity;
     75 import com.android.contacts.common.model.AccountTypeManager;
     76 import com.android.contacts.common.model.ValuesDelta;
     77 import com.android.contacts.common.model.account.AccountType;
     78 import com.android.contacts.common.model.account.AccountWithDataSet;
     79 import com.android.contacts.common.model.account.GoogleAccountType;
     80 import com.android.contacts.common.util.AccountsListAdapter;
     81 import com.android.contacts.common.util.AccountsListAdapter.AccountListFilter;
     82 import com.android.contacts.detail.PhotoSelectionHandler;
     83 import com.android.contacts.editor.AggregationSuggestionEngine.Suggestion;
     84 import com.android.contacts.editor.Editor.EditorListener;
     85 import com.android.contacts.common.model.Contact;
     86 import com.android.contacts.common.model.ContactLoader;
     87 import com.android.contacts.common.model.RawContact;
     88 import com.android.contacts.common.model.RawContactDelta;
     89 import com.android.contacts.common.model.RawContactDeltaList;
     90 import com.android.contacts.common.model.RawContactModifier;
     91 import com.android.contacts.quickcontact.QuickContactActivity;
     92 import com.android.contacts.util.ContactPhotoUtils;
     93 import com.android.contacts.util.HelpUtils;
     94 import com.android.contacts.util.PhoneCapabilityTester;
     95 import com.android.contacts.util.UiClosables;
     96 import com.google.common.collect.ImmutableList;
     97 import com.google.common.collect.Lists;
     98 
     99 import java.io.FileNotFoundException;
    100 import java.util.ArrayList;
    101 import java.util.Collections;
    102 import java.util.Comparator;
    103 import java.util.List;
    104 
    105 public class ContactEditorFragment extends Fragment implements
    106         SplitContactConfirmationDialogFragment.Listener,
    107         AggregationSuggestionEngine.Listener, AggregationSuggestionView.Listener,
    108         RawContactReadOnlyEditorView.Listener {
    109 
    110     private static final String TAG = ContactEditorFragment.class.getSimpleName();
    111 
    112     private static final int LOADER_DATA = 1;
    113     private static final int LOADER_GROUPS = 2;
    114 
    115     private static final String KEY_URI = "uri";
    116     private static final String KEY_ACTION = "action";
    117     private static final String KEY_EDIT_STATE = "state";
    118     private static final String KEY_RAW_CONTACT_ID_REQUESTING_PHOTO = "photorequester";
    119     private static final String KEY_VIEW_ID_GENERATOR = "viewidgenerator";
    120     private static final String KEY_CURRENT_PHOTO_URI = "currentphotouri";
    121     private static final String KEY_CONTACT_ID_FOR_JOIN = "contactidforjoin";
    122     private static final String KEY_CONTACT_WRITABLE_FOR_JOIN = "contactwritableforjoin";
    123     private static final String KEY_SHOW_JOIN_SUGGESTIONS = "showJoinSuggestions";
    124     private static final String KEY_ENABLED = "enabled";
    125     private static final String KEY_STATUS = "status";
    126     private static final String KEY_NEW_LOCAL_PROFILE = "newLocalProfile";
    127     private static final String KEY_IS_USER_PROFILE = "isUserProfile";
    128     private static final String KEY_DISABLE_DELETE_MENU_OPTION = "disableDeleteMenuOption";
    129     private static final String KEY_UPDATED_PHOTOS = "updatedPhotos";
    130     private static final String KEY_IS_EDIT = "isEdit";
    131     private static final String KEY_HAS_NEW_CONTACT = "hasNewContact";
    132     private static final String KEY_NEW_CONTACT_READY = "newContactDataReady";
    133     private static final String KEY_EXISTING_CONTACT_READY = "existingContactDataReady";
    134     private static final String KEY_RAW_CONTACTS = "rawContacts";
    135     private static final String KEY_SEND_TO_VOICE_MAIL_STATE = "sendToVoicemailState";
    136     private static final String KEY_CUSTOM_RINGTONE = "customRingtone";
    137     private static final String KEY_ARE_PHONE_OPTIONS_CHANGEABLE = "arePhoneOptionsChangable";
    138 
    139     public static final String SAVE_MODE_EXTRA_KEY = "saveMode";
    140 
    141 
    142     /**
    143      * An intent extra that forces the editor to add the edited contact
    144      * to the default group (e.g. "My Contacts").
    145      */
    146     public static final String INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY = "addToDefaultDirectory";
    147 
    148     public static final String INTENT_EXTRA_NEW_LOCAL_PROFILE = "newLocalProfile";
    149 
    150     public static final String INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION =
    151             "disableDeleteMenuOption";
    152 
    153     /**
    154      * Modes that specify what the AsyncTask has to perform after saving
    155      */
    156     public interface SaveMode {
    157         /**
    158          * Close the editor after saving
    159          */
    160         public static final int CLOSE = 0;
    161 
    162         /**
    163          * Reload the data so that the user can continue editing
    164          */
    165         public static final int RELOAD = 1;
    166 
    167         /**
    168          * Split the contact after saving
    169          */
    170         public static final int SPLIT = 2;
    171 
    172         /**
    173          * Join another contact after saving
    174          */
    175         public static final int JOIN = 3;
    176 
    177         /**
    178          * Navigate to Contacts Home activity after saving.
    179          */
    180         public static final int HOME = 4;
    181     }
    182 
    183     private interface Status {
    184         /**
    185          * The loader is fetching data
    186          */
    187         public static final int LOADING = 0;
    188 
    189         /**
    190          * Not currently busy. We are waiting for the user to enter data
    191          */
    192         public static final int EDITING = 1;
    193 
    194         /**
    195          * The data is currently being saved. This is used to prevent more
    196          * auto-saves (they shouldn't overlap)
    197          */
    198         public static final int SAVING = 2;
    199 
    200         /**
    201          * Prevents any more saves. This is used if in the following cases:
    202          * - After Save/Close
    203          * - After Revert
    204          * - After the user has accepted an edit suggestion
    205          */
    206         public static final int CLOSING = 3;
    207 
    208         /**
    209          * Prevents saving while running a child activity.
    210          */
    211         public static final int SUB_ACTIVITY = 4;
    212     }
    213 
    214     private static final int REQUEST_CODE_JOIN = 0;
    215     private static final int REQUEST_CODE_ACCOUNTS_CHANGED = 1;
    216     private static final int REQUEST_CODE_PICK_RINGTONE = 2;
    217 
    218     /**
    219      * The raw contact for which we started "take photo" or "choose photo from gallery" most
    220      * recently.  Used to restore {@link #mCurrentPhotoHandler} after orientation change.
    221      */
    222     private long mRawContactIdRequestingPhoto;
    223     /**
    224      * The {@link PhotoHandler} for the photo editor for the {@link #mRawContactIdRequestingPhoto}
    225      * raw contact.
    226      *
    227      * A {@link PhotoHandler} is created for each photo editor in {@link #bindPhotoHandler}, but
    228      * the only "active" one should get the activity result.  This member represents the active
    229      * one.
    230      */
    231     private PhotoHandler mCurrentPhotoHandler;
    232 
    233     private final EntityDeltaComparator mComparator = new EntityDeltaComparator();
    234 
    235     private Cursor mGroupMetaData;
    236 
    237     private Uri mCurrentPhotoUri;
    238     private Bundle mUpdatedPhotos = new Bundle();
    239 
    240     private Context mContext;
    241     private String mAction;
    242     private Uri mLookupUri;
    243     private Bundle mIntentExtras;
    244     private Listener mListener;
    245 
    246     private long mContactIdForJoin;
    247     private boolean mContactWritableForJoin;
    248 
    249     private ContactEditorUtils mEditorUtils;
    250 
    251     private LinearLayout mContent;
    252     private RawContactDeltaList mState;
    253 
    254     private ViewIdGenerator mViewIdGenerator;
    255 
    256     private long mLoaderStartTime;
    257 
    258     private int mStatus;
    259 
    260     // Whether to show the new contact blank form and if it's corresponding delta is ready.
    261     private boolean mHasNewContact = false;
    262     private boolean mNewContactDataReady = false;
    263 
    264     // Whether it's an edit of existing contact and if it's corresponding delta is ready.
    265     private boolean mIsEdit = false;
    266     private boolean mExistingContactDataReady = false;
    267 
    268     // Variables related to phone specific option menus
    269     private boolean mSendToVoicemailState;
    270     private boolean mArePhoneOptionsChangable;
    271     private String mCustomRingtone;
    272 
    273     // This is used to pre-populate the editor with a display name when a user edits a read-only
    274     // contact.
    275     private String mDefaultDisplayName;
    276 
    277     // Used to temporarily store existing contact data during a rebind call (i.e. account switch)
    278     private ImmutableList<RawContact> mRawContacts;
    279 
    280     private AggregationSuggestionEngine mAggregationSuggestionEngine;
    281     private long mAggregationSuggestionsRawContactId;
    282     private View mAggregationSuggestionView;
    283 
    284     private ListPopupWindow mAggregationSuggestionPopup;
    285 
    286     private static final class AggregationSuggestionAdapter extends BaseAdapter {
    287         private final Activity mActivity;
    288         private final boolean mSetNewContact;
    289         private final AggregationSuggestionView.Listener mListener;
    290         private final List<Suggestion> mSuggestions;
    291 
    292         public AggregationSuggestionAdapter(Activity activity, boolean setNewContact,
    293                 AggregationSuggestionView.Listener listener, List<Suggestion> suggestions) {
    294             mActivity = activity;
    295             mSetNewContact = setNewContact;
    296             mListener = listener;
    297             mSuggestions = suggestions;
    298         }
    299 
    300         @Override
    301         public View getView(int position, View convertView, ViewGroup parent) {
    302             Suggestion suggestion = (Suggestion) getItem(position);
    303             LayoutInflater inflater = mActivity.getLayoutInflater();
    304             AggregationSuggestionView suggestionView =
    305                     (AggregationSuggestionView) inflater.inflate(
    306                             R.layout.aggregation_suggestions_item, null);
    307             suggestionView.setNewContact(mSetNewContact);
    308             suggestionView.setListener(mListener);
    309             suggestionView.bindSuggestion(suggestion);
    310             return suggestionView;
    311         }
    312 
    313         @Override
    314         public long getItemId(int position) {
    315             return position;
    316         }
    317 
    318         @Override
    319         public Object getItem(int position) {
    320             return mSuggestions.get(position);
    321         }
    322 
    323         @Override
    324         public int getCount() {
    325             return mSuggestions.size();
    326         }
    327     }
    328 
    329     private OnItemClickListener mAggregationSuggestionItemClickListener =
    330             new OnItemClickListener() {
    331         @Override
    332         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    333             final AggregationSuggestionView suggestionView = (AggregationSuggestionView) view;
    334             suggestionView.handleItemClickEvent();
    335             UiClosables.closeQuietly(mAggregationSuggestionPopup);
    336             mAggregationSuggestionPopup = null;
    337         }
    338     };
    339 
    340     private boolean mAutoAddToDefaultGroup;
    341 
    342     private boolean mEnabled = true;
    343     private boolean mRequestFocus;
    344     private boolean mNewLocalProfile = false;
    345     private boolean mIsUserProfile = false;
    346     private boolean mDisableDeleteMenuOption = false;
    347 
    348     public ContactEditorFragment() {
    349     }
    350 
    351     public void setEnabled(boolean enabled) {
    352         if (mEnabled != enabled) {
    353             mEnabled = enabled;
    354             if (mContent != null) {
    355                 int count = mContent.getChildCount();
    356                 for (int i = 0; i < count; i++) {
    357                     mContent.getChildAt(i).setEnabled(enabled);
    358                 }
    359             }
    360             setAggregationSuggestionViewEnabled(enabled);
    361             final Activity activity = getActivity();
    362             if (activity != null) activity.invalidateOptionsMenu();
    363         }
    364     }
    365 
    366     @Override
    367     public void onAttach(Activity activity) {
    368         super.onAttach(activity);
    369         mContext = activity;
    370         mEditorUtils = ContactEditorUtils.getInstance(mContext);
    371     }
    372 
    373     @Override
    374     public void onStop() {
    375         super.onStop();
    376 
    377         UiClosables.closeQuietly(mAggregationSuggestionPopup);
    378 
    379         // If anything was left unsaved, save it now but keep the editor open.
    380         if (!getActivity().isChangingConfigurations() && mStatus == Status.EDITING) {
    381             save(SaveMode.RELOAD);
    382         }
    383     }
    384 
    385     @Override
    386     public void onDestroy() {
    387         super.onDestroy();
    388         if (mAggregationSuggestionEngine != null) {
    389             mAggregationSuggestionEngine.quit();
    390         }
    391     }
    392 
    393     @Override
    394     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
    395         final View view = inflater.inflate(R.layout.contact_editor_fragment, container, false);
    396 
    397         mContent = (LinearLayout) view.findViewById(R.id.editors);
    398 
    399         setHasOptionsMenu(true);
    400 
    401         return view;
    402     }
    403 
    404     @Override
    405     public void onActivityCreated(Bundle savedInstanceState) {
    406         super.onActivityCreated(savedInstanceState);
    407 
    408         validateAction(mAction);
    409 
    410         if (mState.isEmpty()) {
    411             // The delta list may not have finished loading before orientation change happens.
    412             // In this case, there will be a saved state but deltas will be missing.  Reload from
    413             // database.
    414             if (Intent.ACTION_EDIT.equals(mAction)) {
    415                 // Either...
    416                 // 1) orientation change but load never finished.
    417                 // or
    418                 // 2) not an orientation change.  data needs to be loaded for first time.
    419                 getLoaderManager().initLoader(LOADER_DATA, null, mDataLoaderListener);
    420             }
    421         } else {
    422             // Orientation change, we already have mState, it was loaded by onCreate
    423             bindEditors();
    424         }
    425 
    426         // Handle initial actions only when existing state missing
    427         if (savedInstanceState == null) {
    428             if (Intent.ACTION_EDIT.equals(mAction)) {
    429                 mIsEdit = true;
    430             } else if (Intent.ACTION_INSERT.equals(mAction)) {
    431                 mHasNewContact = true;
    432                 final Account account = mIntentExtras == null ? null :
    433                         (Account) mIntentExtras.getParcelable(Intents.Insert.ACCOUNT);
    434                 final String dataSet = mIntentExtras == null ? null :
    435                         mIntentExtras.getString(Intents.Insert.DATA_SET);
    436 
    437                 if (account != null) {
    438                     // Account specified in Intent
    439                     createContact(new AccountWithDataSet(account.name, account.type, dataSet));
    440                 } else {
    441                     // No Account specified. Let the user choose
    442                     // Load Accounts async so that we can present them
    443                     selectAccountAndCreateContact();
    444                 }
    445             }
    446         }
    447     }
    448 
    449     /**
    450      * Checks if the requested action is valid.
    451      *
    452      * @param action The action to test.
    453      * @throws IllegalArgumentException when the action is invalid.
    454      */
    455     private void validateAction(String action) {
    456         if (Intent.ACTION_EDIT.equals(action) || Intent.ACTION_INSERT.equals(action) ||
    457                 ContactEditorActivity.ACTION_SAVE_COMPLETED.equals(action)) {
    458             return;
    459         }
    460         throw new IllegalArgumentException("Unknown Action String " + mAction +
    461                 ". Only support " + Intent.ACTION_EDIT + " or " + Intent.ACTION_INSERT + " or " +
    462                 ContactEditorActivity.ACTION_SAVE_COMPLETED);
    463     }
    464 
    465     @Override
    466     public void onStart() {
    467         getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupLoaderListener);
    468         super.onStart();
    469     }
    470 
    471     public void load(String action, Uri lookupUri, Bundle intentExtras) {
    472         mAction = action;
    473         mLookupUri = lookupUri;
    474         mIntentExtras = intentExtras;
    475         mAutoAddToDefaultGroup = mIntentExtras != null
    476                 && mIntentExtras.containsKey(INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY);
    477         mNewLocalProfile = mIntentExtras != null
    478                 && mIntentExtras.getBoolean(INTENT_EXTRA_NEW_LOCAL_PROFILE);
    479         mDisableDeleteMenuOption = mIntentExtras != null
    480                 && mIntentExtras.getBoolean(INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION);
    481     }
    482 
    483     public void setListener(Listener value) {
    484         mListener = value;
    485     }
    486 
    487     @Override
    488     public void onCreate(Bundle savedState) {
    489         if (savedState != null) {
    490             // Restore mUri before calling super.onCreate so that onInitializeLoaders
    491             // would already have a uri and an action to work with
    492             mLookupUri = savedState.getParcelable(KEY_URI);
    493             mAction = savedState.getString(KEY_ACTION);
    494         }
    495 
    496         super.onCreate(savedState);
    497 
    498         if (savedState == null) {
    499             // If savedState is non-null, onRestoreInstanceState() will restore the generator.
    500             mViewIdGenerator = new ViewIdGenerator();
    501         } else {
    502             // Read state from savedState. No loading involved here
    503             mState = savedState.<RawContactDeltaList> getParcelable(KEY_EDIT_STATE);
    504             mRawContactIdRequestingPhoto = savedState.getLong(
    505                     KEY_RAW_CONTACT_ID_REQUESTING_PHOTO);
    506             mViewIdGenerator = savedState.getParcelable(KEY_VIEW_ID_GENERATOR);
    507             mCurrentPhotoUri = savedState.getParcelable(KEY_CURRENT_PHOTO_URI);
    508             mContactIdForJoin = savedState.getLong(KEY_CONTACT_ID_FOR_JOIN);
    509             mContactWritableForJoin = savedState.getBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN);
    510             mAggregationSuggestionsRawContactId = savedState.getLong(KEY_SHOW_JOIN_SUGGESTIONS);
    511             mEnabled = savedState.getBoolean(KEY_ENABLED);
    512             mStatus = savedState.getInt(KEY_STATUS);
    513             mNewLocalProfile = savedState.getBoolean(KEY_NEW_LOCAL_PROFILE);
    514             mDisableDeleteMenuOption = savedState.getBoolean(KEY_DISABLE_DELETE_MENU_OPTION);
    515             mIsUserProfile = savedState.getBoolean(KEY_IS_USER_PROFILE);
    516             mUpdatedPhotos = savedState.getParcelable(KEY_UPDATED_PHOTOS);
    517             mIsEdit = savedState.getBoolean(KEY_IS_EDIT);
    518             mHasNewContact = savedState.getBoolean(KEY_HAS_NEW_CONTACT);
    519             mNewContactDataReady = savedState.getBoolean(KEY_NEW_CONTACT_READY);
    520             mExistingContactDataReady = savedState.getBoolean(KEY_EXISTING_CONTACT_READY);
    521             mRawContacts = ImmutableList.copyOf(savedState.<RawContact>getParcelableArrayList(
    522                     KEY_RAW_CONTACTS));
    523             mSendToVoicemailState = savedState.getBoolean(KEY_SEND_TO_VOICE_MAIL_STATE);
    524             mCustomRingtone =  savedState.getString(KEY_CUSTOM_RINGTONE);
    525             mArePhoneOptionsChangable =  savedState.getBoolean(KEY_ARE_PHONE_OPTIONS_CHANGEABLE);
    526         }
    527 
    528         // mState can still be null because it may not have have finished loading before
    529         // onSaveInstanceState was called.
    530         if (mState == null) {
    531             mState = new RawContactDeltaList();
    532         }
    533     }
    534 
    535     public void setData(Contact contact) {
    536 
    537         // If we have already loaded data, we do not want to change it here to not confuse the user
    538         if (!mState.isEmpty()) {
    539             Log.v(TAG, "Ignoring background change. This will have to be rebased later");
    540             return;
    541         }
    542 
    543         // See if this edit operation needs to be redirected to a custom editor
    544         mRawContacts = contact.getRawContacts();
    545         if (mRawContacts.size() == 1) {
    546             RawContact rawContact = mRawContacts.get(0);
    547             String type = rawContact.getAccountTypeString();
    548             String dataSet = rawContact.getDataSet();
    549             AccountType accountType = rawContact.getAccountType(mContext);
    550             if (accountType.getEditContactActivityClassName() != null &&
    551                     !accountType.areContactsWritable()) {
    552                 if (mListener != null) {
    553                     String name = rawContact.getAccountName();
    554                     long rawContactId = rawContact.getId();
    555                     mListener.onCustomEditContactActivityRequested(
    556                             new AccountWithDataSet(name, type, dataSet),
    557                             ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
    558                             mIntentExtras, true);
    559                 }
    560                 return;
    561             }
    562         }
    563 
    564         String displayName = null;
    565         // Check for writable raw contacts.  If there are none, then we need to create one so user
    566         // can edit.  For the user profile case, there is already an editable contact.
    567         if (!contact.isUserProfile() && !contact.isWritableContact(mContext)) {
    568             mHasNewContact = true;
    569 
    570             // This is potentially an asynchronous call and will add deltas to list.
    571             selectAccountAndCreateContact();
    572             displayName = contact.getDisplayName();
    573         }
    574 
    575         // This also adds deltas to list
    576         // If displayName is null at this point it is simply ignored later on by the editor.
    577         bindEditorsForExistingContact(displayName, contact.isUserProfile(),
    578                 mRawContacts);
    579 
    580         bindMenuItemsForPhone(contact);
    581     }
    582 
    583     @Override
    584     public void onExternalEditorRequest(AccountWithDataSet account, Uri uri) {
    585         mListener.onCustomEditContactActivityRequested(account, uri, null, false);
    586     }
    587 
    588     private void bindEditorsForExistingContact(String displayName, boolean isUserProfile,
    589             ImmutableList<RawContact> rawContacts) {
    590         setEnabled(true);
    591         mDefaultDisplayName = displayName;
    592 
    593         mState.addAll(rawContacts.iterator());
    594         setIntentExtras(mIntentExtras);
    595         mIntentExtras = null;
    596 
    597         // For user profile, change the contacts query URI
    598         mIsUserProfile = isUserProfile;
    599         boolean localProfileExists = false;
    600 
    601         if (mIsUserProfile) {
    602             for (RawContactDelta state : mState) {
    603                 // For profile contacts, we need a different query URI
    604                 state.setProfileQueryUri();
    605                 // Try to find a local profile contact
    606                 if (state.getValues().getAsString(RawContacts.ACCOUNT_TYPE) == null) {
    607                     localProfileExists = true;
    608                 }
    609             }
    610             // Editor should always present a local profile for editing
    611             if (!localProfileExists) {
    612                 final RawContact rawContact = new RawContact();
    613                 rawContact.setAccountToLocal();
    614 
    615                 RawContactDelta insert = new RawContactDelta(ValuesDelta.fromAfter(
    616                         rawContact.getValues()));
    617                 insert.setProfileQueryUri();
    618                 mState.add(insert);
    619             }
    620         }
    621         mRequestFocus = true;
    622         mExistingContactDataReady = true;
    623         bindEditors();
    624     }
    625 
    626     private void bindMenuItemsForPhone(Contact contact) {
    627         mSendToVoicemailState = contact.isSendToVoicemail();
    628         mCustomRingtone = contact.getCustomRingtone();
    629         mArePhoneOptionsChangable = arePhoneOptionsChangable(contact);
    630     }
    631 
    632     private boolean arePhoneOptionsChangable(Contact contact) {
    633         return contact != null && !contact.isDirectoryEntry()
    634                 && PhoneCapabilityTester.isPhone(mContext);
    635     }
    636 
    637     /**
    638      * Merges extras from the intent.
    639      */
    640     public void setIntentExtras(Bundle extras) {
    641         if (extras == null || extras.size() == 0) {
    642             return;
    643         }
    644 
    645         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
    646         for (RawContactDelta state : mState) {
    647             final AccountType type = state.getAccountType(accountTypes);
    648             if (type.areContactsWritable()) {
    649                 // Apply extras to the first writable raw contact only
    650                 RawContactModifier.parseExtras(mContext, type, state, extras);
    651                 break;
    652             }
    653         }
    654     }
    655 
    656     private void selectAccountAndCreateContact() {
    657         // If this is a local profile, then skip the logic about showing the accounts changed
    658         // activity and create a phone-local contact.
    659         if (mNewLocalProfile) {
    660             createContact(null);
    661             return;
    662         }
    663 
    664         // If there is no default account or the accounts have changed such that we need to
    665         // prompt the user again, then launch the account prompt.
    666         if (mEditorUtils.shouldShowAccountChangedNotification()) {
    667             Intent intent = new Intent(mContext, ContactEditorAccountsChangedActivity.class);
    668             mStatus = Status.SUB_ACTIVITY;
    669             startActivityForResult(intent, REQUEST_CODE_ACCOUNTS_CHANGED);
    670         } else {
    671             // Otherwise, there should be a default account. Then either create a local contact
    672             // (if default account is null) or create a contact with the specified account.
    673             AccountWithDataSet defaultAccount = mEditorUtils.getDefaultAccount();
    674             if (defaultAccount == null) {
    675                 createContact(null);
    676             } else {
    677                 createContact(defaultAccount);
    678             }
    679         }
    680     }
    681 
    682     /**
    683      * Create a contact by automatically selecting the first account. If there's no available
    684      * account, a device-local contact should be created.
    685      */
    686     private void createContact() {
    687         final List<AccountWithDataSet> accounts =
    688                 AccountTypeManager.getInstance(mContext).getAccounts(true);
    689         // No Accounts available. Create a phone-local contact.
    690         if (accounts.isEmpty()) {
    691             createContact(null);
    692             return;
    693         }
    694 
    695         // We have an account switcher in "create-account" screen, so don't need to ask a user to
    696         // select an account here.
    697         createContact(accounts.get(0));
    698     }
    699 
    700     /**
    701      * Shows account creation screen associated with a given account.
    702      *
    703      * @param account may be null to signal a device-local contact should be created.
    704      */
    705     private void createContact(AccountWithDataSet account) {
    706         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
    707         final AccountType accountType =
    708                 accountTypes.getAccountType(account != null ? account.type : null,
    709                         account != null ? account.dataSet : null);
    710 
    711         if (accountType.getCreateContactActivityClassName() != null) {
    712             if (mListener != null) {
    713                 mListener.onCustomCreateContactActivityRequested(account, mIntentExtras);
    714             }
    715         } else {
    716             bindEditorsForNewContact(account, accountType);
    717         }
    718     }
    719 
    720     /**
    721      * Removes a current editor ({@link #mState}) and rebinds new editor for a new account.
    722      * Some of old data are reused with new restriction enforced by the new account.
    723      *
    724      * @param oldState Old data being edited.
    725      * @param oldAccount Old account associated with oldState.
    726      * @param newAccount New account to be used.
    727      */
    728     private void rebindEditorsForNewContact(
    729             RawContactDelta oldState, AccountWithDataSet oldAccount,
    730             AccountWithDataSet newAccount) {
    731         AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
    732         AccountType oldAccountType = accountTypes.getAccountType(
    733                 oldAccount.type, oldAccount.dataSet);
    734         AccountType newAccountType = accountTypes.getAccountType(
    735                 newAccount.type, newAccount.dataSet);
    736 
    737         if (newAccountType.getCreateContactActivityClassName() != null) {
    738             Log.w(TAG, "external activity called in rebind situation");
    739             if (mListener != null) {
    740                 mListener.onCustomCreateContactActivityRequested(newAccount, mIntentExtras);
    741             }
    742         } else {
    743             mExistingContactDataReady = false;
    744             mNewContactDataReady = false;
    745             mState = new RawContactDeltaList();
    746             bindEditorsForNewContact(newAccount, newAccountType, oldState, oldAccountType);
    747             if (mIsEdit) {
    748                 bindEditorsForExistingContact(mDefaultDisplayName, mIsUserProfile, mRawContacts);
    749             }
    750         }
    751     }
    752 
    753     private void bindEditorsForNewContact(AccountWithDataSet account,
    754             final AccountType accountType) {
    755         bindEditorsForNewContact(account, accountType, null, null);
    756     }
    757 
    758     private void bindEditorsForNewContact(AccountWithDataSet newAccount,
    759             final AccountType newAccountType, RawContactDelta oldState,
    760             AccountType oldAccountType) {
    761         mStatus = Status.EDITING;
    762 
    763         final RawContact rawContact = new RawContact();
    764         if (newAccount != null) {
    765             rawContact.setAccount(newAccount);
    766         } else {
    767             rawContact.setAccountToLocal();
    768         }
    769 
    770         final ValuesDelta valuesDelta = ValuesDelta.fromAfter(rawContact.getValues());
    771         final RawContactDelta insert = new RawContactDelta(valuesDelta);
    772         if (oldState == null) {
    773             // Parse any values from incoming intent
    774             RawContactModifier.parseExtras(mContext, newAccountType, insert, mIntentExtras);
    775         } else {
    776             RawContactModifier.migrateStateForNewContact(mContext, oldState, insert,
    777                     oldAccountType, newAccountType);
    778         }
    779 
    780         // Ensure we have some default fields (if the account type does not support a field,
    781         // ensureKind will not add it, so it is safe to add e.g. Event)
    782         RawContactModifier.ensureKindExists(insert, newAccountType, Phone.CONTENT_ITEM_TYPE);
    783         RawContactModifier.ensureKindExists(insert, newAccountType, Email.CONTENT_ITEM_TYPE);
    784         RawContactModifier.ensureKindExists(insert, newAccountType, Organization.CONTENT_ITEM_TYPE);
    785         RawContactModifier.ensureKindExists(insert, newAccountType, Event.CONTENT_ITEM_TYPE);
    786         RawContactModifier.ensureKindExists(insert, newAccountType,
    787                 StructuredPostal.CONTENT_ITEM_TYPE);
    788 
    789         // Set the correct URI for saving the contact as a profile
    790         if (mNewLocalProfile) {
    791             insert.setProfileQueryUri();
    792         }
    793 
    794         mState.add(insert);
    795 
    796         mRequestFocus = true;
    797 
    798         mNewContactDataReady = true;
    799         bindEditors();
    800     }
    801 
    802     private void bindEditors() {
    803         // bindEditors() can only bind views if there is data in mState, so immediately return
    804         // if mState is null
    805         if (mState.isEmpty()) {
    806             return;
    807         }
    808 
    809         // Check if delta list is ready.  Delta list is populated from existing data and when
    810         // editing an read-only contact, it's also populated with newly created data for the
    811         // blank form.  When the data is not ready, skip. This method will be called multiple times.
    812         if ((mIsEdit && !mExistingContactDataReady) || (mHasNewContact && !mNewContactDataReady)) {
    813             return;
    814         }
    815 
    816         // Sort the editors
    817         Collections.sort(mState, mComparator);
    818 
    819         // Remove any existing editors and rebuild any visible
    820         mContent.removeAllViews();
    821 
    822         final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
    823                 Context.LAYOUT_INFLATER_SERVICE);
    824         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
    825         int numRawContacts = mState.size();
    826 
    827         for (int i = 0; i < numRawContacts; i++) {
    828             // TODO ensure proper ordering of entities in the list
    829             final RawContactDelta rawContactDelta = mState.get(i);
    830             if (!rawContactDelta.isVisible()) continue;
    831 
    832             final AccountType type = rawContactDelta.getAccountType(accountTypes);
    833             final long rawContactId = rawContactDelta.getRawContactId();
    834 
    835             final BaseRawContactEditorView editor;
    836             if (!type.areContactsWritable()) {
    837                 editor = (BaseRawContactEditorView) inflater.inflate(
    838                         R.layout.raw_contact_readonly_editor_view, mContent, false);
    839                 ((RawContactReadOnlyEditorView) editor).setListener(this);
    840             } else {
    841                 editor = (RawContactEditorView) inflater.inflate(R.layout.raw_contact_editor_view,
    842                         mContent, false);
    843             }
    844             if (mHasNewContact && !mNewLocalProfile) {
    845                 final List<AccountWithDataSet> accounts =
    846                         AccountTypeManager.getInstance(mContext).getAccounts(true);
    847                 if (accounts.size() > 1) {
    848                     addAccountSwitcher(mState.get(0), editor);
    849                 } else {
    850                     disableAccountSwitcher(editor);
    851                 }
    852             } else {
    853                 disableAccountSwitcher(editor);
    854             }
    855 
    856             editor.setEnabled(mEnabled);
    857 
    858             mContent.addView(editor);
    859 
    860             editor.setState(rawContactDelta, type, mViewIdGenerator, isEditingUserProfile());
    861 
    862             // Set up the photo handler.
    863             bindPhotoHandler(editor, type, mState);
    864 
    865             // If a new photo was chosen but not yet saved, we need to
    866             // update the thumbnail to reflect this.
    867             Bitmap bitmap = updatedBitmapForRawContact(rawContactId);
    868             if (bitmap != null) editor.setPhotoBitmap(bitmap);
    869 
    870             if (editor instanceof RawContactEditorView) {
    871                 final Activity activity = getActivity();
    872                 final RawContactEditorView rawContactEditor = (RawContactEditorView) editor;
    873                 EditorListener listener = new EditorListener() {
    874 
    875                     @Override
    876                     public void onRequest(int request) {
    877                         if (activity.isFinishing()) { // Make sure activity is still running.
    878                             return;
    879                         }
    880                         if (request == EditorListener.FIELD_CHANGED && !isEditingUserProfile()) {
    881                             acquireAggregationSuggestions(activity, rawContactEditor);
    882                         }
    883                     }
    884 
    885                     @Override
    886                     public void onDeleteRequested(Editor removedEditor) {
    887                     }
    888                 };
    889 
    890                 final StructuredNameEditorView nameEditor = rawContactEditor.getNameEditor();
    891                 if (mRequestFocus) {
    892                     nameEditor.requestFocus();
    893                     mRequestFocus = false;
    894                 }
    895                 nameEditor.setEditorListener(listener);
    896                 if (!TextUtils.isEmpty(mDefaultDisplayName)) {
    897                     nameEditor.setDisplayName(mDefaultDisplayName);
    898                 }
    899 
    900                 final TextFieldsEditorView phoneticNameEditor =
    901                         rawContactEditor.getPhoneticNameEditor();
    902                 phoneticNameEditor.setEditorListener(listener);
    903                 rawContactEditor.setAutoAddToDefaultGroup(mAutoAddToDefaultGroup);
    904 
    905                 if (rawContactId == mAggregationSuggestionsRawContactId) {
    906                     acquireAggregationSuggestions(activity, rawContactEditor);
    907                 }
    908             }
    909         }
    910 
    911         mRequestFocus = false;
    912 
    913         bindGroupMetaData();
    914 
    915         // Show editor now that we've loaded state
    916         mContent.setVisibility(View.VISIBLE);
    917 
    918         // Refresh Action Bar as the visibility of the join command
    919         // Activity can be null if we have been detached from the Activity
    920         final Activity activity = getActivity();
    921         if (activity != null) activity.invalidateOptionsMenu();
    922     }
    923 
    924     /**
    925      * If we've stashed a temporary file containing a contact's new photo,
    926      * decode it and return the bitmap.
    927      * @param rawContactId identifies the raw-contact whose Bitmap we'll try to return.
    928      * @return Bitmap of photo for specified raw-contact, or null
    929     */
    930     private Bitmap updatedBitmapForRawContact(long rawContactId) {
    931         String path = mUpdatedPhotos.getString(String.valueOf(rawContactId));
    932         return path == null ? null : BitmapFactory.decodeFile(path);
    933     }
    934 
    935     private void bindPhotoHandler(BaseRawContactEditorView editor, AccountType type,
    936             RawContactDeltaList state) {
    937         final int mode;
    938         if (type.areContactsWritable()) {
    939             if (editor.hasSetPhoto()) {
    940                 if (hasMoreThanOnePhoto()) {
    941                     mode = PhotoActionPopup.Modes.PHOTO_ALLOW_PRIMARY;
    942                 } else {
    943                     mode = PhotoActionPopup.Modes.PHOTO_DISALLOW_PRIMARY;
    944                 }
    945             } else {
    946                 mode = PhotoActionPopup.Modes.NO_PHOTO;
    947             }
    948         } else {
    949             if (editor.hasSetPhoto() && hasMoreThanOnePhoto()) {
    950                 mode = PhotoActionPopup.Modes.READ_ONLY_ALLOW_PRIMARY;
    951             } else {
    952                 // Read-only and either no photo or the only photo ==> no options
    953                 editor.getPhotoEditor().setEditorListener(null);
    954                 return;
    955             }
    956         }
    957         final PhotoHandler photoHandler = new PhotoHandler(mContext, editor, mode, state);
    958         editor.getPhotoEditor().setEditorListener(
    959                 (PhotoHandler.PhotoEditorListener) photoHandler.getListener());
    960 
    961         // Note a newly created raw contact gets some random negative ID, so any value is valid
    962         // here. (i.e. don't check against -1 or anything.)
    963         if (mRawContactIdRequestingPhoto == editor.getRawContactId()) {
    964             mCurrentPhotoHandler = photoHandler;
    965         }
    966     }
    967 
    968     private void bindGroupMetaData() {
    969         if (mGroupMetaData == null) {
    970             return;
    971         }
    972 
    973         int editorCount = mContent.getChildCount();
    974         for (int i = 0; i < editorCount; i++) {
    975             BaseRawContactEditorView editor = (BaseRawContactEditorView) mContent.getChildAt(i);
    976             editor.setGroupMetaData(mGroupMetaData);
    977         }
    978     }
    979 
    980     private void saveDefaultAccountIfNecessary() {
    981         // Verify that this is a newly created contact, that the contact is composed of only
    982         // 1 raw contact, and that the contact is not a user profile.
    983         if (!Intent.ACTION_INSERT.equals(mAction) && mState.size() == 1 &&
    984                 !isEditingUserProfile()) {
    985             return;
    986         }
    987 
    988         // Find the associated account for this contact (retrieve it here because there are
    989         // multiple paths to creating a contact and this ensures we always have the correct
    990         // account).
    991         final RawContactDelta rawContactDelta = mState.get(0);
    992         String name = rawContactDelta.getAccountName();
    993         String type = rawContactDelta.getAccountType();
    994         String dataSet = rawContactDelta.getDataSet();
    995 
    996         AccountWithDataSet account = (name == null || type == null) ? null :
    997                 new AccountWithDataSet(name, type, dataSet);
    998         mEditorUtils.saveDefaultAndAllAccounts(account);
    999     }
   1000 
   1001     private void addAccountSwitcher(
   1002             final RawContactDelta currentState, BaseRawContactEditorView editor) {
   1003         final AccountWithDataSet currentAccount = new AccountWithDataSet(
   1004                 currentState.getAccountName(),
   1005                 currentState.getAccountType(),
   1006                 currentState.getDataSet());
   1007         final View accountView = editor.findViewById(R.id.account);
   1008         final View anchorView = editor.findViewById(R.id.account_container);
   1009         accountView.setOnClickListener(new View.OnClickListener() {
   1010             @Override
   1011             public void onClick(View v) {
   1012                 final ListPopupWindow popup = new ListPopupWindow(mContext, null);
   1013                 final AccountsListAdapter adapter =
   1014                         new AccountsListAdapter(mContext,
   1015                         AccountListFilter.ACCOUNTS_CONTACT_WRITABLE, currentAccount);
   1016                 popup.setWidth(anchorView.getWidth());
   1017                 popup.setAnchorView(anchorView);
   1018                 popup.setAdapter(adapter);
   1019                 popup.setModal(true);
   1020                 popup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
   1021                 popup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
   1022                     @Override
   1023                     public void onItemClick(AdapterView<?> parent, View view, int position,
   1024                             long id) {
   1025                         UiClosables.closeQuietly(popup);
   1026                         AccountWithDataSet newAccount = adapter.getItem(position);
   1027                         if (!newAccount.equals(currentAccount)) {
   1028                             rebindEditorsForNewContact(currentState, currentAccount, newAccount);
   1029                         }
   1030                     }
   1031                 });
   1032                 popup.show();
   1033             }
   1034         });
   1035     }
   1036 
   1037     private void disableAccountSwitcher(BaseRawContactEditorView editor) {
   1038         // Remove the pressed state from the account header because the user cannot switch accounts
   1039         // on an existing contact
   1040         final View accountView = editor.findViewById(R.id.account);
   1041         accountView.setBackground(null);
   1042         accountView.setEnabled(false);
   1043     }
   1044 
   1045     @Override
   1046     public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
   1047         inflater.inflate(R.menu.edit_contact, menu);
   1048     }
   1049 
   1050     @Override
   1051     public void onPrepareOptionsMenu(Menu menu) {
   1052         // This supports the keyboard shortcut to save changes to a contact but shouldn't be visible
   1053         // because the custom action bar contains the "save" button now (not the overflow menu).
   1054         // TODO: Find a better way to handle shortcuts, i.e. onKeyDown()?
   1055         final MenuItem doneMenu = menu.findItem(R.id.menu_done);
   1056         final MenuItem splitMenu = menu.findItem(R.id.menu_split);
   1057         final MenuItem joinMenu = menu.findItem(R.id.menu_join);
   1058         final MenuItem helpMenu = menu.findItem(R.id.menu_help);
   1059         final MenuItem discardMenu = menu.findItem(R.id.menu_discard);
   1060         final MenuItem sendToVoiceMailMenu = menu.findItem(R.id.menu_send_to_voicemail);
   1061         final MenuItem ringToneMenu = menu.findItem(R.id.menu_set_ringtone);
   1062         final MenuItem deleteMenu = menu.findItem(R.id.menu_delete);
   1063 
   1064         // Set visibility of menus
   1065         doneMenu.setVisible(false);
   1066 
   1067         // Discard menu is only available if at least one raw contact is editable
   1068         discardMenu.setVisible(mState != null &&
   1069                 mState.getFirstWritableRawContact(mContext) != null);
   1070 
   1071         // help menu depending on whether this is inserting or editing
   1072         if (Intent.ACTION_INSERT.equals(mAction)) {
   1073             HelpUtils.prepareHelpMenuItem(mContext, helpMenu, R.string.help_url_people_add);
   1074             splitMenu.setVisible(false);
   1075             joinMenu.setVisible(false);
   1076             deleteMenu.setVisible(false);
   1077         } else if (Intent.ACTION_EDIT.equals(mAction)) {
   1078             HelpUtils.prepareHelpMenuItem(mContext, helpMenu, R.string.help_url_people_edit);
   1079             // Split only if more than one raw profile and not a user profile
   1080             splitMenu.setVisible(mState.size() > 1 && !isEditingUserProfile());
   1081             // Cannot join a user profile
   1082             joinMenu.setVisible(!isEditingUserProfile());
   1083             deleteMenu.setVisible(!mDisableDeleteMenuOption);
   1084         } else {
   1085             // something else, so don't show the help menu
   1086             helpMenu.setVisible(false);
   1087         }
   1088 
   1089         // Hide telephony-related settings (ringtone, send to voicemail)
   1090         // if we don't have a telephone or are editing a new contact.
   1091         sendToVoiceMailMenu.setChecked(mSendToVoicemailState);
   1092         sendToVoiceMailMenu.setVisible(mArePhoneOptionsChangable);
   1093         ringToneMenu.setVisible(mArePhoneOptionsChangable);
   1094 
   1095         int size = menu.size();
   1096         for (int i = 0; i < size; i++) {
   1097             menu.getItem(i).setEnabled(mEnabled);
   1098         }
   1099     }
   1100 
   1101     @Override
   1102     public boolean onOptionsItemSelected(MenuItem item) {
   1103         switch (item.getItemId()) {
   1104             case R.id.menu_done:
   1105                 return save(SaveMode.CLOSE);
   1106             case R.id.menu_discard:
   1107                 return revert();
   1108             case R.id.menu_delete:
   1109                 if (mListener != null) mListener.onDeleteRequested(mLookupUri);
   1110                 return true;
   1111             case R.id.menu_split:
   1112                 return doSplitContactAction();
   1113             case R.id.menu_join:
   1114                 return doJoinContactAction();
   1115             case R.id.menu_set_ringtone:
   1116                 doPickRingtone();
   1117                 return true;
   1118             case R.id.menu_send_to_voicemail:
   1119                 // Update state and save
   1120                 mSendToVoicemailState = !mSendToVoicemailState;
   1121                 item.setChecked(mSendToVoicemailState);
   1122                 final Intent intent = ContactSaveService.createSetSendToVoicemail(
   1123                         mContext, mLookupUri, mSendToVoicemailState);
   1124                 mContext.startService(intent);
   1125                 return true;
   1126         }
   1127 
   1128         return false;
   1129     }
   1130 
   1131     private boolean doSplitContactAction() {
   1132         if (!hasValidState()) return false;
   1133 
   1134         final SplitContactConfirmationDialogFragment dialog =
   1135                 new SplitContactConfirmationDialogFragment();
   1136         dialog.setTargetFragment(this, 0);
   1137         dialog.show(getFragmentManager(), SplitContactConfirmationDialogFragment.TAG);
   1138         return true;
   1139     }
   1140 
   1141     private boolean doJoinContactAction() {
   1142         if (!hasValidState()) {
   1143             return false;
   1144         }
   1145 
   1146         // If we just started creating a new contact and haven't added any data, it's too
   1147         // early to do a join
   1148         if (mState.size() == 1 && mState.get(0).isContactInsert() && !hasPendingChanges()) {
   1149             Toast.makeText(mContext, R.string.toast_join_with_empty_contact,
   1150                             Toast.LENGTH_LONG).show();
   1151             return true;
   1152         }
   1153 
   1154         return save(SaveMode.JOIN);
   1155     }
   1156 
   1157     /**
   1158      * Check if our internal {@link #mState} is valid, usually checked before
   1159      * performing user actions.
   1160      */
   1161     private boolean hasValidState() {
   1162         return mState.size() > 0;
   1163     }
   1164 
   1165     /**
   1166      * Return true if there are any edits to the current contact which need to
   1167      * be saved.
   1168      */
   1169     private boolean hasPendingChanges() {
   1170         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
   1171         return RawContactModifier.hasChanges(mState, accountTypes);
   1172     }
   1173 
   1174     /**
   1175      * Saves or creates the contact based on the mode, and if successful
   1176      * finishes the activity.
   1177      */
   1178     public boolean save(int saveMode) {
   1179         if (!hasValidState() || mStatus != Status.EDITING) {
   1180             return false;
   1181         }
   1182 
   1183         // If we are about to close the editor - there is no need to refresh the data
   1184         if (saveMode == SaveMode.CLOSE || saveMode == SaveMode.SPLIT) {
   1185             getLoaderManager().destroyLoader(LOADER_DATA);
   1186         }
   1187 
   1188         mStatus = Status.SAVING;
   1189 
   1190         if (!hasPendingChanges()) {
   1191             if (mLookupUri == null && saveMode == SaveMode.RELOAD) {
   1192                 // We don't have anything to save and there isn't even an existing contact yet.
   1193                 // Nothing to do, simply go back to editing mode
   1194                 mStatus = Status.EDITING;
   1195                 return true;
   1196             }
   1197             onSaveCompleted(false, saveMode, mLookupUri != null, mLookupUri);
   1198             return true;
   1199         }
   1200 
   1201         setEnabled(false);
   1202 
   1203         // Store account as default account, only if this is a new contact
   1204         saveDefaultAccountIfNecessary();
   1205 
   1206         // Save contact
   1207         Intent intent = ContactSaveService.createSaveContactIntent(mContext, mState,
   1208                 SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(),
   1209                 ((Activity)mContext).getClass(), ContactEditorActivity.ACTION_SAVE_COMPLETED,
   1210                 mUpdatedPhotos);
   1211         mContext.startService(intent);
   1212 
   1213         // Don't try to save the same photos twice.
   1214         mUpdatedPhotos = new Bundle();
   1215 
   1216         return true;
   1217     }
   1218 
   1219     private void doPickRingtone() {
   1220 
   1221         final Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
   1222         // Allow user to pick 'Default'
   1223         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
   1224         // Show only ringtones
   1225         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_RINGTONE);
   1226         // Allow the user to pick a silent ringtone
   1227         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
   1228 
   1229         final Uri ringtoneUri;
   1230         if (mCustomRingtone != null) {
   1231             ringtoneUri = Uri.parse(mCustomRingtone);
   1232         } else {
   1233             // Otherwise pick default ringtone Uri so that something is selected.
   1234             ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
   1235         }
   1236 
   1237         // Put checkmark next to the current ringtone for this contact
   1238         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, ringtoneUri);
   1239 
   1240         // Launch!
   1241         try {
   1242             startActivityForResult(intent, REQUEST_CODE_PICK_RINGTONE);
   1243         } catch (ActivityNotFoundException ex) {
   1244             Toast.makeText(mContext, R.string.missing_app, Toast.LENGTH_SHORT).show();
   1245         }
   1246     }
   1247 
   1248     private void handleRingtonePicked(Uri pickedUri) {
   1249         if (pickedUri == null || RingtoneManager.isDefault(pickedUri)) {
   1250             mCustomRingtone = null;
   1251         } else {
   1252             mCustomRingtone = pickedUri.toString();
   1253         }
   1254         Intent intent = ContactSaveService.createSetRingtone(
   1255                 mContext, mLookupUri, mCustomRingtone);
   1256         mContext.startService(intent);
   1257     }
   1258 
   1259     public static class CancelEditDialogFragment extends DialogFragment {
   1260 
   1261         public static void show(ContactEditorFragment fragment) {
   1262             CancelEditDialogFragment dialog = new CancelEditDialogFragment();
   1263             dialog.setTargetFragment(fragment, 0);
   1264             dialog.show(fragment.getFragmentManager(), "cancelEditor");
   1265         }
   1266 
   1267         @Override
   1268         public Dialog onCreateDialog(Bundle savedInstanceState) {
   1269             AlertDialog dialog = new AlertDialog.Builder(getActivity())
   1270                     .setIconAttribute(android.R.attr.alertDialogIcon)
   1271                     .setMessage(R.string.cancel_confirmation_dialog_message)
   1272                     .setPositiveButton(android.R.string.ok,
   1273                         new DialogInterface.OnClickListener() {
   1274                             @Override
   1275                             public void onClick(DialogInterface dialogInterface, int whichButton) {
   1276                                 ((ContactEditorFragment)getTargetFragment()).doRevertAction();
   1277                             }
   1278                         }
   1279                     )
   1280                     .setNegativeButton(android.R.string.cancel, null)
   1281                     .create();
   1282             return dialog;
   1283         }
   1284     }
   1285 
   1286     private boolean revert() {
   1287         if (mState.isEmpty() || !hasPendingChanges()) {
   1288             doRevertAction();
   1289         } else {
   1290             CancelEditDialogFragment.show(this);
   1291         }
   1292         return true;
   1293     }
   1294 
   1295     private void doRevertAction() {
   1296         // When this Fragment is closed we don't want it to auto-save
   1297         mStatus = Status.CLOSING;
   1298         if (mListener != null) mListener.onReverted();
   1299     }
   1300 
   1301     public void doSaveAction() {
   1302         save(SaveMode.CLOSE);
   1303     }
   1304 
   1305     public void onJoinCompleted(Uri uri) {
   1306         onSaveCompleted(false, SaveMode.RELOAD, uri != null, uri);
   1307     }
   1308 
   1309     public void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded,
   1310             Uri contactLookupUri) {
   1311         if (hadChanges) {
   1312             if (saveSucceeded) {
   1313                 if (saveMode != SaveMode.JOIN) {
   1314                     Toast.makeText(mContext, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
   1315                 }
   1316             } else {
   1317                 Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
   1318             }
   1319         }
   1320         switch (saveMode) {
   1321             case SaveMode.CLOSE:
   1322             case SaveMode.HOME:
   1323                 final Intent resultIntent;
   1324                 if (saveSucceeded && contactLookupUri != null) {
   1325                     final String requestAuthority =
   1326                             mLookupUri == null ? null : mLookupUri.getAuthority();
   1327 
   1328                     final String legacyAuthority = "contacts";
   1329                     final Uri lookupUri;
   1330                     if (legacyAuthority.equals(requestAuthority)) {
   1331                         // Build legacy Uri when requested by caller
   1332                         final long contactId = ContentUris.parseId(Contacts.lookupContact(
   1333                                 mContext.getContentResolver(), contactLookupUri));
   1334                         final Uri legacyContentUri = Uri.parse("content://contacts/people");
   1335                         final Uri legacyUri = ContentUris.withAppendedId(
   1336                                 legacyContentUri, contactId);
   1337                         lookupUri = legacyUri;
   1338                     } else {
   1339                         // Otherwise pass back a lookup-style Uri
   1340                         lookupUri = contactLookupUri;
   1341                     }
   1342                     resultIntent = QuickContact.composeQuickContactsIntent(getActivity(),
   1343                             (Rect) null, lookupUri, QuickContactActivity.MODE_FULLY_EXPANDED, null);
   1344                     // Make sure not to show QuickContacts on top of another QuickContacts.
   1345                     resultIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
   1346                 } else {
   1347                     resultIntent = null;
   1348                 }
   1349                 // It is already saved, so prevent that it is saved again
   1350                 mStatus = Status.CLOSING;
   1351                 if (mListener != null) mListener.onSaveFinished(resultIntent);
   1352                 break;
   1353 
   1354             case SaveMode.RELOAD:
   1355             case SaveMode.JOIN:
   1356                 if (saveSucceeded && contactLookupUri != null) {
   1357                     // If it was a JOIN, we are now ready to bring up the join activity.
   1358                     if (saveMode == SaveMode.JOIN && hasValidState()) {
   1359                         showJoinAggregateActivity(contactLookupUri);
   1360                     }
   1361 
   1362                     // If this was in INSERT, we are changing into an EDIT now.
   1363                     // If it already was an EDIT, we are changing to the new Uri now
   1364                     mState = new RawContactDeltaList();
   1365                     load(Intent.ACTION_EDIT, contactLookupUri, null);
   1366                     mStatus = Status.LOADING;
   1367                     getLoaderManager().restartLoader(LOADER_DATA, null, mDataLoaderListener);
   1368                 }
   1369                 break;
   1370 
   1371             case SaveMode.SPLIT:
   1372                 mStatus = Status.CLOSING;
   1373                 if (mListener != null) {
   1374                     mListener.onContactSplit(contactLookupUri);
   1375                 } else {
   1376                     Log.d(TAG, "No listener registered, can not call onSplitFinished");
   1377                 }
   1378                 break;
   1379         }
   1380     }
   1381 
   1382     /**
   1383      * Shows a list of aggregates that can be joined into the currently viewed aggregate.
   1384      *
   1385      * @param contactLookupUri the fresh URI for the currently edited contact (after saving it)
   1386      */
   1387     private void showJoinAggregateActivity(Uri contactLookupUri) {
   1388         if (contactLookupUri == null || !isAdded()) {
   1389             return;
   1390         }
   1391 
   1392         mContactIdForJoin = ContentUris.parseId(contactLookupUri);
   1393         mContactWritableForJoin = isContactWritable();
   1394         final Intent intent = new Intent(UI.PICK_JOIN_CONTACT_ACTION);
   1395         intent.putExtra(UI.TARGET_CONTACT_ID_EXTRA_KEY, mContactIdForJoin);
   1396         startActivityForResult(intent, REQUEST_CODE_JOIN);
   1397     }
   1398 
   1399     /**
   1400      * Performs aggregation with the contact selected by the user from suggestions or A-Z list.
   1401      */
   1402     private void joinAggregate(final long contactId) {
   1403         Intent intent = ContactSaveService.createJoinContactsIntent(mContext, mContactIdForJoin,
   1404                 contactId, mContactWritableForJoin,
   1405                 ContactEditorActivity.class, ContactEditorActivity.ACTION_JOIN_COMPLETED);
   1406         mContext.startService(intent);
   1407     }
   1408 
   1409     /**
   1410      * Returns true if there is at least one writable raw contact in the current contact.
   1411      */
   1412     private boolean isContactWritable() {
   1413         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
   1414         int size = mState.size();
   1415         for (int i = 0; i < size; i++) {
   1416             RawContactDelta entity = mState.get(i);
   1417             final AccountType type = entity.getAccountType(accountTypes);
   1418             if (type.areContactsWritable()) {
   1419                 return true;
   1420             }
   1421         }
   1422         return false;
   1423     }
   1424 
   1425     private boolean isEditingUserProfile() {
   1426         return mNewLocalProfile || mIsUserProfile;
   1427     }
   1428 
   1429     public static interface Listener {
   1430         /**
   1431          * Contact was not found, so somehow close this fragment. This is raised after a contact
   1432          * is removed via Menu/Delete (unless it was a new contact)
   1433          */
   1434         void onContactNotFound();
   1435 
   1436         /**
   1437          * Contact was split, so we can close now.
   1438          * @param newLookupUri The lookup uri of the new contact that should be shown to the user.
   1439          * The editor tries best to chose the most natural contact here.
   1440          */
   1441         void onContactSplit(Uri newLookupUri);
   1442 
   1443         /**
   1444          * User has tapped Revert, close the fragment now.
   1445          */
   1446         void onReverted();
   1447 
   1448         /**
   1449          * Contact was saved and the Fragment can now be closed safely.
   1450          */
   1451         void onSaveFinished(Intent resultIntent);
   1452 
   1453         /**
   1454          * User switched to editing a different contact (a suggestion from the
   1455          * aggregation engine).
   1456          */
   1457         void onEditOtherContactRequested(
   1458                 Uri contactLookupUri, ArrayList<ContentValues> contentValues);
   1459 
   1460         /**
   1461          * Contact is being created for an external account that provides its own
   1462          * new contact activity.
   1463          */
   1464         void onCustomCreateContactActivityRequested(AccountWithDataSet account,
   1465                 Bundle intentExtras);
   1466 
   1467         /**
   1468          * The edited raw contact belongs to an external account that provides
   1469          * its own edit activity.
   1470          *
   1471          * @param redirect indicates that the current editor should be closed
   1472          *            before the custom editor is shown.
   1473          */
   1474         void onCustomEditContactActivityRequested(AccountWithDataSet account, Uri rawContactUri,
   1475                 Bundle intentExtras, boolean redirect);
   1476 
   1477         void onDeleteRequested(Uri contactUri);
   1478     }
   1479 
   1480     private class EntityDeltaComparator implements Comparator<RawContactDelta> {
   1481         /**
   1482          * Compare EntityDeltas for sorting the stack of editors.
   1483          */
   1484         @Override
   1485         public int compare(RawContactDelta one, RawContactDelta two) {
   1486             // Check direct equality
   1487             if (one.equals(two)) {
   1488                 return 0;
   1489             }
   1490 
   1491             final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
   1492             String accountType1 = one.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
   1493             String dataSet1 = one.getValues().getAsString(RawContacts.DATA_SET);
   1494             final AccountType type1 = accountTypes.getAccountType(accountType1, dataSet1);
   1495             String accountType2 = two.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
   1496             String dataSet2 = two.getValues().getAsString(RawContacts.DATA_SET);
   1497             final AccountType type2 = accountTypes.getAccountType(accountType2, dataSet2);
   1498 
   1499             // Check read-only. Sort read/write before read-only.
   1500             if (!type1.areContactsWritable() && type2.areContactsWritable()) {
   1501                 return 1;
   1502             } else if (type1.areContactsWritable() && !type2.areContactsWritable()) {
   1503                 return -1;
   1504             }
   1505 
   1506             // Check account type. Sort Google before non-Google.
   1507             boolean skipAccountTypeCheck = false;
   1508             boolean isGoogleAccount1 = type1 instanceof GoogleAccountType;
   1509             boolean isGoogleAccount2 = type2 instanceof GoogleAccountType;
   1510             if (isGoogleAccount1 && !isGoogleAccount2) {
   1511                 return -1;
   1512             } else if (!isGoogleAccount1 && isGoogleAccount2) {
   1513                 return 1;
   1514             } else if (isGoogleAccount1 && isGoogleAccount2){
   1515                 skipAccountTypeCheck = true;
   1516             }
   1517 
   1518             int value;
   1519             if (!skipAccountTypeCheck) {
   1520                 // Sort accounts with type before accounts without types.
   1521                 if (type1.accountType != null && type2.accountType == null) {
   1522                     return -1;
   1523                 } else if (type1.accountType == null && type2.accountType != null) {
   1524                     return 1;
   1525                 }
   1526 
   1527                 if (type1.accountType != null && type2.accountType != null) {
   1528                     value = type1.accountType.compareTo(type2.accountType);
   1529                     if (value != 0) {
   1530                         return value;
   1531                     }
   1532                 }
   1533 
   1534                 // Fall back to data set. Sort accounts with data sets before
   1535                 // those without.
   1536                 if (type1.dataSet != null && type2.dataSet == null) {
   1537                     return -1;
   1538                 } else if (type1.dataSet == null && type2.dataSet != null) {
   1539                     return 1;
   1540                 }
   1541 
   1542                 if (type1.dataSet != null && type2.dataSet != null) {
   1543                     value = type1.dataSet.compareTo(type2.dataSet);
   1544                     if (value != 0) {
   1545                         return value;
   1546                     }
   1547                 }
   1548             }
   1549 
   1550             // Check account name
   1551             String oneAccount = one.getAccountName();
   1552             if (oneAccount == null) oneAccount = "";
   1553             String twoAccount = two.getAccountName();
   1554             if (twoAccount == null) twoAccount = "";
   1555             value = oneAccount.compareTo(twoAccount);
   1556             if (value != 0) {
   1557                 return value;
   1558             }
   1559 
   1560             // Both are in the same account, fall back to contact ID
   1561             Long oneId = one.getRawContactId();
   1562             Long twoId = two.getRawContactId();
   1563             if (oneId == null) {
   1564                 return -1;
   1565             } else if (twoId == null) {
   1566                 return 1;
   1567             }
   1568 
   1569             return (int)(oneId - twoId);
   1570         }
   1571     }
   1572 
   1573     /**
   1574      * Returns the contact ID for the currently edited contact or 0 if the contact is new.
   1575      */
   1576     protected long getContactId() {
   1577         for (RawContactDelta rawContact : mState) {
   1578             Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID);
   1579             if (contactId != null) {
   1580                 return contactId;
   1581             }
   1582         }
   1583         return 0;
   1584     }
   1585 
   1586     /**
   1587      * Triggers an asynchronous search for aggregation suggestions.
   1588      */
   1589     private void acquireAggregationSuggestions(Context context,
   1590             RawContactEditorView rawContactEditor) {
   1591         long rawContactId = rawContactEditor.getRawContactId();
   1592         if (mAggregationSuggestionsRawContactId != rawContactId
   1593                 && mAggregationSuggestionView != null) {
   1594             mAggregationSuggestionView.setVisibility(View.GONE);
   1595             mAggregationSuggestionView = null;
   1596             mAggregationSuggestionEngine.reset();
   1597         }
   1598 
   1599         mAggregationSuggestionsRawContactId = rawContactId;
   1600 
   1601         if (mAggregationSuggestionEngine == null) {
   1602             mAggregationSuggestionEngine = new AggregationSuggestionEngine(context);
   1603             mAggregationSuggestionEngine.setListener(this);
   1604             mAggregationSuggestionEngine.start();
   1605         }
   1606 
   1607         mAggregationSuggestionEngine.setContactId(getContactId());
   1608 
   1609         LabeledEditorView nameEditor = rawContactEditor.getNameEditor();
   1610         mAggregationSuggestionEngine.onNameChange(nameEditor.getValues());
   1611     }
   1612 
   1613     @Override
   1614     public void onAggregationSuggestionChange() {
   1615         Activity activity = getActivity();
   1616         if ((activity != null && activity.isFinishing())
   1617                 || !isVisible() ||  mState.isEmpty() || mStatus != Status.EDITING) {
   1618             return;
   1619         }
   1620 
   1621         UiClosables.closeQuietly(mAggregationSuggestionPopup);
   1622 
   1623         if (mAggregationSuggestionEngine.getSuggestedContactCount() == 0) {
   1624             return;
   1625         }
   1626 
   1627         final RawContactEditorView rawContactView =
   1628                 (RawContactEditorView)getRawContactEditorView(mAggregationSuggestionsRawContactId);
   1629         if (rawContactView == null) {
   1630             return; // Raw contact deleted?
   1631         }
   1632         final View anchorView = rawContactView.findViewById(R.id.anchor_view);
   1633         mAggregationSuggestionPopup = new ListPopupWindow(mContext, null);
   1634         mAggregationSuggestionPopup.setAnchorView(anchorView);
   1635         mAggregationSuggestionPopup.setWidth(anchorView.getWidth());
   1636         mAggregationSuggestionPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
   1637         mAggregationSuggestionPopup.setAdapter(
   1638                 new AggregationSuggestionAdapter(getActivity(),
   1639                         mState.size() == 1 && mState.get(0).isContactInsert(),
   1640                         this, mAggregationSuggestionEngine.getSuggestions()));
   1641         mAggregationSuggestionPopup.setOnItemClickListener(mAggregationSuggestionItemClickListener);
   1642         mAggregationSuggestionPopup.show();
   1643     }
   1644 
   1645     @Override
   1646     public void onJoinAction(long contactId, List<Long> rawContactIdList) {
   1647         long rawContactIds[] = new long[rawContactIdList.size()];
   1648         for (int i = 0; i < rawContactIds.length; i++) {
   1649             rawContactIds[i] = rawContactIdList.get(i);
   1650         }
   1651         JoinSuggestedContactDialogFragment dialog =
   1652                 new JoinSuggestedContactDialogFragment();
   1653         Bundle args = new Bundle();
   1654         args.putLongArray("rawContactIds", rawContactIds);
   1655         dialog.setArguments(args);
   1656         dialog.setTargetFragment(this, 0);
   1657         try {
   1658             dialog.show(getFragmentManager(), "join");
   1659         } catch (Exception ex) {
   1660             // No problem - the activity is no longer available to display the dialog
   1661         }
   1662     }
   1663 
   1664     public static class JoinSuggestedContactDialogFragment extends DialogFragment {
   1665 
   1666         @Override
   1667         public Dialog onCreateDialog(Bundle savedInstanceState) {
   1668             return new AlertDialog.Builder(getActivity())
   1669                     .setIconAttribute(android.R.attr.alertDialogIcon)
   1670                     .setMessage(R.string.aggregation_suggestion_join_dialog_message)
   1671                     .setPositiveButton(android.R.string.yes,
   1672                         new DialogInterface.OnClickListener() {
   1673                             @Override
   1674                             public void onClick(DialogInterface dialog, int whichButton) {
   1675                                 ContactEditorFragment targetFragment =
   1676                                         (ContactEditorFragment) getTargetFragment();
   1677                                 long rawContactIds[] =
   1678                                         getArguments().getLongArray("rawContactIds");
   1679                                 targetFragment.doJoinSuggestedContact(rawContactIds);
   1680                             }
   1681                         }
   1682                     )
   1683                     .setNegativeButton(android.R.string.no, null)
   1684                     .create();
   1685         }
   1686     }
   1687 
   1688     /**
   1689      * Joins the suggested contact (specified by the id's of constituent raw
   1690      * contacts), save all changes, and stay in the editor.
   1691      */
   1692     protected void doJoinSuggestedContact(long[] rawContactIds) {
   1693         if (!hasValidState() || mStatus != Status.EDITING) {
   1694             return;
   1695         }
   1696 
   1697         mState.setJoinWithRawContacts(rawContactIds);
   1698         save(SaveMode.RELOAD);
   1699     }
   1700 
   1701     @Override
   1702     public void onEditAction(Uri contactLookupUri) {
   1703         SuggestionEditConfirmationDialogFragment dialog =
   1704                 new SuggestionEditConfirmationDialogFragment();
   1705         Bundle args = new Bundle();
   1706         args.putParcelable("contactUri", contactLookupUri);
   1707         dialog.setArguments(args);
   1708         dialog.setTargetFragment(this, 0);
   1709         dialog.show(getFragmentManager(), "edit");
   1710     }
   1711 
   1712     public static class SuggestionEditConfirmationDialogFragment extends DialogFragment {
   1713 
   1714         @Override
   1715         public Dialog onCreateDialog(Bundle savedInstanceState) {
   1716             return new AlertDialog.Builder(getActivity())
   1717                     .setIconAttribute(android.R.attr.alertDialogIcon)
   1718                     .setMessage(R.string.aggregation_suggestion_edit_dialog_message)
   1719                     .setPositiveButton(android.R.string.yes,
   1720                         new DialogInterface.OnClickListener() {
   1721                             @Override
   1722                             public void onClick(DialogInterface dialog, int whichButton) {
   1723                                 ContactEditorFragment targetFragment =
   1724                                         (ContactEditorFragment) getTargetFragment();
   1725                                 Uri contactUri =
   1726                                         getArguments().getParcelable("contactUri");
   1727                                 targetFragment.doEditSuggestedContact(contactUri);
   1728                             }
   1729                         }
   1730                     )
   1731                     .setNegativeButton(android.R.string.no, null)
   1732                     .create();
   1733         }
   1734     }
   1735 
   1736     /**
   1737      * Abandons the currently edited contact and switches to editing the suggested
   1738      * one, transferring all the data there
   1739      */
   1740     protected void doEditSuggestedContact(Uri contactUri) {
   1741         if (mListener != null) {
   1742             // make sure we don't save this contact when closing down
   1743             mStatus = Status.CLOSING;
   1744             mListener.onEditOtherContactRequested(
   1745                     contactUri, mState.get(0).getContentValues());
   1746         }
   1747     }
   1748 
   1749     public void setAggregationSuggestionViewEnabled(boolean enabled) {
   1750         if (mAggregationSuggestionView == null) {
   1751             return;
   1752         }
   1753 
   1754         LinearLayout itemList = (LinearLayout) mAggregationSuggestionView.findViewById(
   1755                 R.id.aggregation_suggestions);
   1756         int count = itemList.getChildCount();
   1757         for (int i = 0; i < count; i++) {
   1758             itemList.getChildAt(i).setEnabled(enabled);
   1759         }
   1760     }
   1761 
   1762     @Override
   1763     public void onSaveInstanceState(Bundle outState) {
   1764         outState.putParcelable(KEY_URI, mLookupUri);
   1765         outState.putString(KEY_ACTION, mAction);
   1766 
   1767         if (hasValidState()) {
   1768             // Store entities with modifications
   1769             outState.putParcelable(KEY_EDIT_STATE, mState);
   1770         }
   1771         outState.putLong(KEY_RAW_CONTACT_ID_REQUESTING_PHOTO, mRawContactIdRequestingPhoto);
   1772         outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator);
   1773         outState.putParcelable(KEY_CURRENT_PHOTO_URI, mCurrentPhotoUri);
   1774         outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin);
   1775         outState.putBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN, mContactWritableForJoin);
   1776         outState.putLong(KEY_SHOW_JOIN_SUGGESTIONS, mAggregationSuggestionsRawContactId);
   1777         outState.putBoolean(KEY_ENABLED, mEnabled);
   1778         outState.putBoolean(KEY_NEW_LOCAL_PROFILE, mNewLocalProfile);
   1779         outState.putBoolean(KEY_DISABLE_DELETE_MENU_OPTION, mDisableDeleteMenuOption);
   1780         outState.putBoolean(KEY_IS_USER_PROFILE, mIsUserProfile);
   1781         outState.putInt(KEY_STATUS, mStatus);
   1782         outState.putParcelable(KEY_UPDATED_PHOTOS, mUpdatedPhotos);
   1783         outState.putBoolean(KEY_HAS_NEW_CONTACT, mHasNewContact);
   1784         outState.putBoolean(KEY_IS_EDIT, mIsEdit);
   1785         outState.putBoolean(KEY_NEW_CONTACT_READY, mNewContactDataReady);
   1786         outState.putBoolean(KEY_EXISTING_CONTACT_READY, mExistingContactDataReady);
   1787         outState.putParcelableArrayList(KEY_RAW_CONTACTS,
   1788                 mRawContacts == null ?
   1789                 Lists.<RawContact> newArrayList() :  Lists.newArrayList(mRawContacts));
   1790         outState.putBoolean(KEY_SEND_TO_VOICE_MAIL_STATE, mSendToVoicemailState);
   1791         outState.putString(KEY_CUSTOM_RINGTONE, mCustomRingtone);
   1792         outState.putBoolean(KEY_ARE_PHONE_OPTIONS_CHANGEABLE, mArePhoneOptionsChangable);
   1793 
   1794         super.onSaveInstanceState(outState);
   1795     }
   1796 
   1797     @Override
   1798     public void onActivityResult(int requestCode, int resultCode, Intent data) {
   1799         if (mStatus == Status.SUB_ACTIVITY) {
   1800             mStatus = Status.EDITING;
   1801         }
   1802 
   1803         // See if the photo selection handler handles this result.
   1804         if (mCurrentPhotoHandler != null && mCurrentPhotoHandler.handlePhotoActivityResult(
   1805                 requestCode, resultCode, data)) {
   1806             return;
   1807         }
   1808 
   1809         switch (requestCode) {
   1810             case REQUEST_CODE_JOIN: {
   1811                 // Ignore failed requests
   1812                 if (resultCode != Activity.RESULT_OK) return;
   1813                 if (data != null) {
   1814                     final long contactId = ContentUris.parseId(data.getData());
   1815                     joinAggregate(contactId);
   1816                 }
   1817                 break;
   1818             }
   1819             case REQUEST_CODE_ACCOUNTS_CHANGED: {
   1820                 // Bail if the account selector was not successful.
   1821                 if (resultCode != Activity.RESULT_OK) {
   1822                     mListener.onReverted();
   1823                     return;
   1824                 }
   1825                 // If there's an account specified, use it.
   1826                 if (data != null) {
   1827                     AccountWithDataSet account = data.getParcelableExtra(Intents.Insert.ACCOUNT);
   1828                     if (account != null) {
   1829                         createContact(account);
   1830                         return;
   1831                     }
   1832                 }
   1833                 // If there isn't an account specified, then this is likely a phone-local
   1834                 // contact, so we should continue setting up the editor by automatically selecting
   1835                 // the most appropriate account.
   1836                 createContact();
   1837                 break;
   1838             }
   1839             case REQUEST_CODE_PICK_RINGTONE: {
   1840                 if (data != null) {
   1841                     final Uri pickedUri = data.getParcelableExtra(
   1842                             RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
   1843                     handleRingtonePicked(pickedUri);
   1844                 }
   1845                 break;
   1846             }
   1847         }
   1848     }
   1849 
   1850     /**
   1851      * Sets the photo stored in mPhoto and writes it to the RawContact with the given id
   1852      */
   1853     private void setPhoto(long rawContact, Bitmap photo, Uri photoUri) {
   1854         BaseRawContactEditorView requestingEditor = getRawContactEditorView(rawContact);
   1855 
   1856         if (photo == null || photo.getHeight() < 0 || photo.getWidth() < 0) {
   1857             // This is unexpected.
   1858             Log.w(TAG, "Invalid bitmap passed to setPhoto()");
   1859         }
   1860 
   1861         if (requestingEditor != null) {
   1862             requestingEditor.setPhotoBitmap(photo);
   1863         } else {
   1864             Log.w(TAG, "The contact that requested the photo is no longer present.");
   1865         }
   1866 
   1867         mUpdatedPhotos.putParcelable(String.valueOf(rawContact), photoUri);
   1868     }
   1869 
   1870     /**
   1871      * Finds raw contact editor view for the given rawContactId.
   1872      */
   1873     public BaseRawContactEditorView getRawContactEditorView(long rawContactId) {
   1874         for (int i = 0; i < mContent.getChildCount(); i++) {
   1875             final View childView = mContent.getChildAt(i);
   1876             if (childView instanceof BaseRawContactEditorView) {
   1877                 final BaseRawContactEditorView editor = (BaseRawContactEditorView) childView;
   1878                 if (editor.getRawContactId() == rawContactId) {
   1879                     return editor;
   1880                 }
   1881             }
   1882         }
   1883         return null;
   1884     }
   1885 
   1886     /**
   1887      * Returns true if there is currently more than one photo on screen.
   1888      */
   1889     private boolean hasMoreThanOnePhoto() {
   1890         int countWithPicture = 0;
   1891         final int numEntities = mState.size();
   1892         for (int i = 0; i < numEntities; i++) {
   1893             final RawContactDelta entity = mState.get(i);
   1894             if (entity.isVisible()) {
   1895                 final ValuesDelta primary = entity.getPrimaryEntry(Photo.CONTENT_ITEM_TYPE);
   1896                 if (primary != null && primary.getPhoto() != null) {
   1897                     countWithPicture++;
   1898                 } else {
   1899                     final long rawContactId = entity.getRawContactId();
   1900                     final Uri uri = mUpdatedPhotos.getParcelable(String.valueOf(rawContactId));
   1901                     if (uri != null) {
   1902                         try {
   1903                             mContext.getContentResolver().openInputStream(uri);
   1904                             countWithPicture++;
   1905                         } catch (FileNotFoundException e) {
   1906                         }
   1907                     }
   1908                 }
   1909 
   1910                 if (countWithPicture > 1) {
   1911                     return true;
   1912                 }
   1913             }
   1914         }
   1915         return false;
   1916     }
   1917 
   1918     /**
   1919      * The listener for the data loader
   1920      */
   1921     private final LoaderManager.LoaderCallbacks<Contact> mDataLoaderListener =
   1922             new LoaderCallbacks<Contact>() {
   1923         @Override
   1924         public Loader<Contact> onCreateLoader(int id, Bundle args) {
   1925             mLoaderStartTime = SystemClock.elapsedRealtime();
   1926             return new ContactLoader(mContext, mLookupUri, true);
   1927         }
   1928 
   1929         @Override
   1930         public void onLoadFinished(Loader<Contact> loader, Contact data) {
   1931             final long loaderCurrentTime = SystemClock.elapsedRealtime();
   1932             Log.v(TAG, "Time needed for loading: " + (loaderCurrentTime-mLoaderStartTime));
   1933             if (!data.isLoaded()) {
   1934                 // Item has been deleted
   1935                 Log.i(TAG, "No contact found. Closing activity");
   1936                 if (mListener != null) mListener.onContactNotFound();
   1937                 return;
   1938             }
   1939 
   1940             mStatus = Status.EDITING;
   1941             mLookupUri = data.getLookupUri();
   1942             final long setDataStartTime = SystemClock.elapsedRealtime();
   1943             setData(data);
   1944             final long setDataEndTime = SystemClock.elapsedRealtime();
   1945 
   1946             Log.v(TAG, "Time needed for setting UI: " + (setDataEndTime-setDataStartTime));
   1947         }
   1948 
   1949         @Override
   1950         public void onLoaderReset(Loader<Contact> loader) {
   1951         }
   1952     };
   1953 
   1954     /**
   1955      * The listener for the group meta data loader for all groups.
   1956      */
   1957     private final LoaderManager.LoaderCallbacks<Cursor> mGroupLoaderListener =
   1958             new LoaderCallbacks<Cursor>() {
   1959 
   1960         @Override
   1961         public CursorLoader onCreateLoader(int id, Bundle args) {
   1962             return new GroupMetaDataLoader(mContext, Groups.CONTENT_URI);
   1963         }
   1964 
   1965         @Override
   1966         public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
   1967             mGroupMetaData = data;
   1968             bindGroupMetaData();
   1969         }
   1970 
   1971         @Override
   1972         public void onLoaderReset(Loader<Cursor> loader) {
   1973         }
   1974     };
   1975 
   1976     @Override
   1977     public void onSplitContactConfirmed() {
   1978         if (mState.isEmpty()) {
   1979             // This may happen when this Fragment is recreated by the system during users
   1980             // confirming the split action (and thus this method is called just before onCreate()),
   1981             // for example.
   1982             Log.e(TAG, "mState became null during the user's confirming split action. " +
   1983                     "Cannot perform the save action.");
   1984             return;
   1985         }
   1986 
   1987         mState.markRawContactsForSplitting();
   1988         save(SaveMode.SPLIT);
   1989     }
   1990 
   1991     /**
   1992      * Custom photo handler for the editor.  The inner listener that this creates also has a
   1993      * reference to the editor and acts as an {@link EditorListener}, and uses that editor to hold
   1994      * state information in several of the listener methods.
   1995      */
   1996     private final class PhotoHandler extends PhotoSelectionHandler {
   1997 
   1998         final long mRawContactId;
   1999         private final BaseRawContactEditorView mEditor;
   2000         private final PhotoActionListener mPhotoEditorListener;
   2001 
   2002         public PhotoHandler(Context context, BaseRawContactEditorView editor, int photoMode,
   2003                 RawContactDeltaList state) {
   2004             super(context, editor.getPhotoEditor(), photoMode, false, state);
   2005             mEditor = editor;
   2006             mRawContactId = editor.getRawContactId();
   2007             mPhotoEditorListener = new PhotoEditorListener();
   2008         }
   2009 
   2010         @Override
   2011         public PhotoActionListener getListener() {
   2012             return mPhotoEditorListener;
   2013         }
   2014 
   2015         @Override
   2016         public void startPhotoActivity(Intent intent, int requestCode, Uri photoUri) {
   2017             mRawContactIdRequestingPhoto = mEditor.getRawContactId();
   2018             mCurrentPhotoHandler = this;
   2019             mStatus = Status.SUB_ACTIVITY;
   2020             mCurrentPhotoUri = photoUri;
   2021             ContactEditorFragment.this.startActivityForResult(intent, requestCode);
   2022         }
   2023 
   2024         private final class PhotoEditorListener extends PhotoSelectionHandler.PhotoActionListener
   2025                 implements EditorListener {
   2026 
   2027             @Override
   2028             public void onRequest(int request) {
   2029                 if (!hasValidState()) return;
   2030 
   2031                 if (request == EditorListener.REQUEST_PICK_PHOTO) {
   2032                     onClick(mEditor.getPhotoEditor());
   2033                 }
   2034             }
   2035 
   2036             @Override
   2037             public void onDeleteRequested(Editor removedEditor) {
   2038                 // The picture cannot be deleted, it can only be removed, which is handled by
   2039                 // onRemovePictureChosen()
   2040             }
   2041 
   2042             /**
   2043              * User has chosen to set the selected photo as the (super) primary photo
   2044              */
   2045             @Override
   2046             public void onUseAsPrimaryChosen() {
   2047                 // Set the IsSuperPrimary for each editor
   2048                 int count = mContent.getChildCount();
   2049                 for (int i = 0; i < count; i++) {
   2050                     final View childView = mContent.getChildAt(i);
   2051                     if (childView instanceof BaseRawContactEditorView) {
   2052                         final BaseRawContactEditorView editor =
   2053                                 (BaseRawContactEditorView) childView;
   2054                         final PhotoEditorView photoEditor = editor.getPhotoEditor();
   2055                         photoEditor.setSuperPrimary(editor == mEditor);
   2056                     }
   2057                 }
   2058                 bindEditors();
   2059             }
   2060 
   2061             /**
   2062              * User has chosen to remove a picture
   2063              */
   2064             @Override
   2065             public void onRemovePictureChosen() {
   2066                 mEditor.setPhotoBitmap(null);
   2067 
   2068                 // Prevent bitmap from being restored if rotate the device.
   2069                 // (only if we first chose a new photo before removing it)
   2070                 mUpdatedPhotos.remove(String.valueOf(mRawContactId));
   2071                 bindEditors();
   2072             }
   2073 
   2074             @Override
   2075             public void onPhotoSelected(Uri uri) throws FileNotFoundException {
   2076                 final Bitmap bitmap = ContactPhotoUtils.getBitmapFromUri(mContext, uri);
   2077                 setPhoto(mRawContactId, bitmap, uri);
   2078                 mCurrentPhotoHandler = null;
   2079                 bindEditors();
   2080             }
   2081 
   2082             @Override
   2083             public Uri getCurrentPhotoUri() {
   2084                 return mCurrentPhotoUri;
   2085             }
   2086 
   2087             @Override
   2088             public void onPhotoSelectionDismissed() {
   2089                 // Nothing to do.
   2090             }
   2091         }
   2092     }
   2093 }
   2094