Home | History | Annotate | Download | only in editor
      1 /*
      2  * Copyright (C) 2015 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.Fragment;
     22 import android.app.LoaderManager;
     23 import android.content.ContentResolver;
     24 import android.content.ContentUris;
     25 import android.content.ContentValues;
     26 import android.content.Context;
     27 import android.content.CursorLoader;
     28 import android.content.Intent;
     29 import android.content.Loader;
     30 import android.database.Cursor;
     31 import android.graphics.Bitmap;
     32 import android.net.Uri;
     33 import android.os.Bundle;
     34 import android.os.SystemClock;
     35 import android.provider.ContactsContract;
     36 import android.provider.ContactsContract.CommonDataKinds.Email;
     37 import android.provider.ContactsContract.CommonDataKinds.Event;
     38 import android.provider.ContactsContract.CommonDataKinds.Organization;
     39 import android.provider.ContactsContract.CommonDataKinds.Phone;
     40 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
     41 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
     42 import android.provider.ContactsContract.Intents;
     43 import android.provider.ContactsContract.RawContacts;
     44 import android.support.v7.widget.Toolbar;
     45 import android.text.TextUtils;
     46 import android.util.Log;
     47 import android.view.LayoutInflater;
     48 import android.view.Menu;
     49 import android.view.MenuInflater;
     50 import android.view.MenuItem;
     51 import android.view.View;
     52 import android.view.ViewGroup;
     53 import android.widget.AdapterView;
     54 import android.widget.BaseAdapter;
     55 import android.widget.EditText;
     56 import android.widget.LinearLayout;
     57 import android.widget.ListPopupWindow;
     58 import android.widget.Toast;
     59 
     60 import com.android.contacts.ContactSaveService;
     61 import com.android.contacts.GroupMetaDataLoader;
     62 import com.android.contacts.R;
     63 import com.android.contacts.activities.ContactEditorAccountsChangedActivity;
     64 import com.android.contacts.activities.ContactEditorActivity;
     65 import com.android.contacts.activities.ContactEditorActivity.ContactEditor;
     66 import com.android.contacts.activities.ContactSelectionActivity;
     67 import com.android.contacts.activities.RequestPermissionsActivity;
     68 import com.android.contacts.editor.AggregationSuggestionEngine.Suggestion;
     69 import com.android.contacts.group.GroupUtil;
     70 import com.android.contacts.list.UiIntentActions;
     71 import com.android.contacts.logging.ScreenEvent.ScreenType;
     72 import com.android.contacts.model.AccountTypeManager;
     73 import com.android.contacts.model.Contact;
     74 import com.android.contacts.model.ContactLoader;
     75 import com.android.contacts.model.RawContact;
     76 import com.android.contacts.model.RawContactDelta;
     77 import com.android.contacts.model.RawContactDeltaList;
     78 import com.android.contacts.model.RawContactModifier;
     79 import com.android.contacts.model.ValuesDelta;
     80 import com.android.contacts.model.account.AccountInfo;
     81 import com.android.contacts.model.account.AccountType;
     82 import com.android.contacts.model.account.AccountWithDataSet;
     83 import com.android.contacts.model.account.AccountsLoader;
     84 import com.android.contacts.preference.ContactsPreferences;
     85 import com.android.contacts.quickcontact.InvisibleContactUtil;
     86 import com.android.contacts.quickcontact.QuickContactActivity;
     87 import com.android.contacts.util.ContactDisplayUtils;
     88 import com.android.contacts.util.ContactPhotoUtils;
     89 import com.android.contacts.util.ImplicitIntentsUtil;
     90 import com.android.contacts.util.MaterialColorMapUtils;
     91 import com.android.contacts.util.UiClosables;
     92 import com.android.contactsbind.HelpUtils;
     93 
     94 import com.google.common.base.Preconditions;
     95 import com.google.common.collect.ImmutableList;
     96 import com.google.common.collect.Lists;
     97 
     98 import java.io.FileNotFoundException;
     99 import java.util.ArrayList;
    100 import java.util.Collections;
    101 import java.util.HashSet;
    102 import java.util.Iterator;
    103 import java.util.List;
    104 import java.util.Locale;
    105 import java.util.Set;
    106 
    107 /**
    108  * Contact editor with only the most important fields displayed initially.
    109  */
    110 public class ContactEditorFragment extends Fragment implements
    111         ContactEditor, SplitContactConfirmationDialogFragment.Listener,
    112         JoinContactConfirmationDialogFragment.Listener,
    113         AggregationSuggestionEngine.Listener, AggregationSuggestionView.Listener,
    114         CancelEditDialogFragment.Listener,
    115         RawContactEditorView.Listener, PhotoEditorView.Listener,
    116         AccountsLoader.AccountsListener {
    117 
    118     static final String TAG = "ContactEditor";
    119 
    120     private static final int LOADER_CONTACT = 1;
    121     private static final int LOADER_GROUPS = 2;
    122     private static final int LOADER_ACCOUNTS = 3;
    123 
    124     private static final String KEY_PHOTO_RAW_CONTACT_ID = "photo_raw_contact_id";
    125     private static final String KEY_UPDATED_PHOTOS = "updated_photos";
    126 
    127     private static final List<String> VALID_INTENT_ACTIONS = new ArrayList<String>() {{
    128         add(Intent.ACTION_EDIT);
    129         add(Intent.ACTION_INSERT);
    130         add(ContactEditorActivity.ACTION_SAVE_COMPLETED);
    131     }};
    132 
    133     private static final String KEY_ACTION = "action";
    134     private static final String KEY_URI = "uri";
    135     private static final String KEY_AUTO_ADD_TO_DEFAULT_GROUP = "autoAddToDefaultGroup";
    136     private static final String KEY_DISABLE_DELETE_MENU_OPTION = "disableDeleteMenuOption";
    137     private static final String KEY_NEW_LOCAL_PROFILE = "newLocalProfile";
    138     private static final String KEY_MATERIAL_PALETTE = "materialPalette";
    139     private static final String KEY_ACCOUNT = "saveToAccount";
    140     private static final String KEY_VIEW_ID_GENERATOR = "viewidgenerator";
    141 
    142     private static final String KEY_RAW_CONTACTS = "rawContacts";
    143 
    144     private static final String KEY_EDIT_STATE = "state";
    145     private static final String KEY_STATUS = "status";
    146 
    147     private static final String KEY_HAS_NEW_CONTACT = "hasNewContact";
    148     private static final String KEY_NEW_CONTACT_READY = "newContactDataReady";
    149 
    150     private static final String KEY_IS_EDIT = "isEdit";
    151     private static final String KEY_EXISTING_CONTACT_READY = "existingContactDataReady";
    152 
    153     private static final String KEY_IS_USER_PROFILE = "isUserProfile";
    154 
    155     private static final String KEY_ENABLED = "enabled";
    156 
    157     // Aggregation PopupWindow
    158     private static final String KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID =
    159             "aggregationSuggestionsRawContactId";
    160 
    161     // Join Activity
    162     private static final String KEY_CONTACT_ID_FOR_JOIN = "contactidforjoin";
    163 
    164     private static final String KEY_READ_ONLY_DISPLAY_NAME_ID = "readOnlyDisplayNameId";
    165     private static final String KEY_COPY_READ_ONLY_DISPLAY_NAME = "copyReadOnlyDisplayName";
    166 
    167     protected static final int REQUEST_CODE_JOIN = 0;
    168     protected static final int REQUEST_CODE_ACCOUNTS_CHANGED = 1;
    169 
    170     /**
    171      * An intent extra that forces the editor to add the edited contact
    172      * to the default group (e.g. "My Contacts").
    173      */
    174     public static final String INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY = "addToDefaultDirectory";
    175 
    176     public static final String INTENT_EXTRA_NEW_LOCAL_PROFILE = "newLocalProfile";
    177 
    178     public static final String INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION =
    179             "disableDeleteMenuOption";
    180 
    181     /**
    182      * Intent key to pass the photo palette primary color calculated by
    183      * {@link com.android.contacts.quickcontact.QuickContactActivity} to the editor.
    184      */
    185     public static final String INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR =
    186             "material_palette_primary_color";
    187 
    188     /**
    189      * Intent key to pass the photo palette secondary color calculated by
    190      * {@link com.android.contacts.quickcontact.QuickContactActivity} to the editor.
    191      */
    192     public static final String INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR =
    193             "material_palette_secondary_color";
    194 
    195     /**
    196      * Intent key to pass the ID of the photo to display on the editor.
    197      */
    198     // TODO: This can be cleaned up if we decide to not pass the photo id through
    199     // QuickContactActivity.
    200     public static final String INTENT_EXTRA_PHOTO_ID = "photo_id";
    201 
    202     /**
    203      * Intent key to pass the ID of the raw contact id that should be displayed in the full editor
    204      * by itself.
    205      */
    206     public static final String INTENT_EXTRA_RAW_CONTACT_ID_TO_DISPLAY_ALONE =
    207             "raw_contact_id_to_display_alone";
    208 
    209     /**
    210      * Intent extra to specify a {@link ContactEditor.SaveMode}.
    211      */
    212     public static final String SAVE_MODE_EXTRA_KEY = "saveMode";
    213 
    214     /**
    215      * Intent extra key for the contact ID to join the current contact to after saving.
    216      */
    217     public static final String JOIN_CONTACT_ID_EXTRA_KEY = "joinContactId";
    218 
    219     /**
    220      * Callbacks for Activities that host contact editors Fragments.
    221      */
    222     public interface Listener {
    223 
    224         /**
    225          * Contact was not found, so somehow close this fragment. This is raised after a contact
    226          * is removed via Menu/Delete
    227          */
    228         void onContactNotFound();
    229 
    230         /**
    231          * Contact was split, so we can close now.
    232          *
    233          * @param newLookupUri The lookup uri of the new contact that should be shown to the user.
    234          *                     The editor tries best to chose the most natural contact here.
    235          */
    236         void onContactSplit(Uri newLookupUri);
    237 
    238         /**
    239          * User has tapped Revert, close the fragment now.
    240          */
    241         void onReverted();
    242 
    243         /**
    244          * Contact was saved and the Fragment can now be closed safely.
    245          */
    246         void onSaveFinished(Intent resultIntent);
    247 
    248         /**
    249          * User switched to editing a different raw contact (a suggestion from the
    250          * aggregation engine).
    251          */
    252         void onEditOtherRawContactRequested(Uri contactLookupUri, long rawContactId,
    253                 ArrayList<ContentValues> contentValues);
    254 
    255         /**
    256          * User has requested that contact be deleted.
    257          */
    258         void onDeleteRequested(Uri contactUri);
    259     }
    260 
    261     /**
    262      * Adapter for aggregation suggestions displayed in a PopupWindow when
    263      * editor fields change.
    264      */
    265     private static final class AggregationSuggestionAdapter extends BaseAdapter {
    266         private final LayoutInflater mLayoutInflater;
    267         private final AggregationSuggestionView.Listener mListener;
    268         private final List<AggregationSuggestionEngine.Suggestion> mSuggestions;
    269 
    270         public AggregationSuggestionAdapter(Activity activity,
    271                 AggregationSuggestionView.Listener listener, List<Suggestion> suggestions) {
    272             mLayoutInflater = activity.getLayoutInflater();
    273             mListener = listener;
    274             mSuggestions = suggestions;
    275         }
    276 
    277         @Override
    278         public View getView(int position, View convertView, ViewGroup parent) {
    279             final Suggestion suggestion = (Suggestion) getItem(position);
    280             final AggregationSuggestionView suggestionView =
    281                     (AggregationSuggestionView) mLayoutInflater.inflate(
    282                             R.layout.aggregation_suggestions_item, null);
    283             suggestionView.setListener(mListener);
    284             suggestionView.bindSuggestion(suggestion);
    285             return suggestionView;
    286         }
    287 
    288         @Override
    289         public long getItemId(int position) {
    290             return position;
    291         }
    292 
    293         @Override
    294         public Object getItem(int position) {
    295             return mSuggestions.get(position);
    296         }
    297 
    298         @Override
    299         public int getCount() {
    300             return mSuggestions.size();
    301         }
    302     }
    303 
    304     protected Context mContext;
    305     protected Listener mListener;
    306 
    307     //
    308     // Views
    309     //
    310     protected LinearLayout mContent;
    311     protected ListPopupWindow mAggregationSuggestionPopup;
    312 
    313     //
    314     // Parameters passed in on {@link #load}
    315     //
    316     protected String mAction;
    317     protected Uri mLookupUri;
    318     protected Bundle mIntentExtras;
    319     protected boolean mAutoAddToDefaultGroup;
    320     protected boolean mDisableDeleteMenuOption;
    321     protected boolean mNewLocalProfile;
    322     protected MaterialColorMapUtils.MaterialPalette mMaterialPalette;
    323 
    324     //
    325     // Helpers
    326     //
    327     protected ContactEditorUtils mEditorUtils;
    328     protected RawContactDeltaComparator mComparator;
    329     protected ViewIdGenerator mViewIdGenerator;
    330     private AggregationSuggestionEngine mAggregationSuggestionEngine;
    331 
    332     //
    333     // Loaded data
    334     //
    335     // Used to store existing contact data so it can be re-applied during a rebind call,
    336     // i.e. account switch.
    337     protected Contact mContact;
    338     protected ImmutableList<RawContact> mRawContacts;
    339     protected Cursor mGroupMetaData;
    340 
    341     //
    342     // Editor state
    343     //
    344     protected RawContactDeltaList mState;
    345     protected int mStatus;
    346     protected long mRawContactIdToDisplayAlone = -1;
    347 
    348     // Whether to show the new contact blank form and if it's corresponding delta is ready.
    349     protected boolean mHasNewContact;
    350     protected AccountWithDataSet mAccountWithDataSet;
    351     protected List<AccountInfo> mWritableAccounts = Collections.emptyList();
    352     protected boolean mNewContactDataReady;
    353     protected boolean mNewContactAccountChanged;
    354 
    355     // Whether it's an edit of existing contact and if it's corresponding delta is ready.
    356     protected boolean mIsEdit;
    357     protected boolean mExistingContactDataReady;
    358 
    359     // Whether we are editing the "me" profile
    360     protected boolean mIsUserProfile;
    361 
    362     // Whether editor views and options menu items should be enabled
    363     private boolean mEnabled = true;
    364 
    365     // Aggregation PopupWindow
    366     private long mAggregationSuggestionsRawContactId;
    367 
    368     // Join Activity
    369     protected long mContactIdForJoin;
    370 
    371     // Used to pre-populate the editor with a display name when a user edits a read-only contact.
    372     protected long mReadOnlyDisplayNameId;
    373     protected boolean mCopyReadOnlyName;
    374 
    375     /**
    376      * The contact data loader listener.
    377      */
    378     protected final LoaderManager.LoaderCallbacks<Contact> mContactLoaderListener =
    379             new LoaderManager.LoaderCallbacks<Contact>() {
    380 
    381                 protected long mLoaderStartTime;
    382 
    383                 @Override
    384                 public Loader<Contact> onCreateLoader(int id, Bundle args) {
    385                     mLoaderStartTime = SystemClock.elapsedRealtime();
    386                     return new ContactLoader(mContext, mLookupUri,
    387                             /* postViewNotification */ true,
    388                             /* loadGroupMetaData */ true);
    389                 }
    390 
    391                 @Override
    392                 public void onLoadFinished(Loader<Contact> loader, Contact contact) {
    393                     final long loaderCurrentTime = SystemClock.elapsedRealtime();
    394                     if (Log.isLoggable(TAG, Log.VERBOSE)) {
    395                         Log.v(TAG,
    396                                 "Time needed for loading: " + (loaderCurrentTime-mLoaderStartTime));
    397                     }
    398                     if (!contact.isLoaded()) {
    399                         // Item has been deleted. Close activity without saving again.
    400                         Log.i(TAG, "No contact found. Closing activity");
    401                         mStatus = Status.CLOSING;
    402                         if (mListener != null) mListener.onContactNotFound();
    403                         return;
    404                     }
    405 
    406                     mStatus = Status.EDITING;
    407                     mLookupUri = contact.getLookupUri();
    408                     final long setDataStartTime = SystemClock.elapsedRealtime();
    409                     setState(contact);
    410                     final long setDataEndTime = SystemClock.elapsedRealtime();
    411                     if (Log.isLoggable(TAG, Log.VERBOSE)) {
    412                         Log.v(TAG, "Time needed for setting UI: "
    413                                 + (setDataEndTime - setDataStartTime));
    414                     }
    415                 }
    416 
    417                 @Override
    418                 public void onLoaderReset(Loader<Contact> loader) {
    419                 }
    420             };
    421 
    422     /**
    423      * The groups meta data loader listener.
    424      */
    425     protected final LoaderManager.LoaderCallbacks<Cursor> mGroupsLoaderListener =
    426             new LoaderManager.LoaderCallbacks<Cursor>() {
    427 
    428                 @Override
    429                 public CursorLoader onCreateLoader(int id, Bundle args) {
    430                     return new GroupMetaDataLoader(mContext, ContactsContract.Groups.CONTENT_URI,
    431                             GroupUtil.ALL_GROUPS_SELECTION);
    432                 }
    433 
    434                 @Override
    435                 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    436                     mGroupMetaData = data;
    437                     setGroupMetaData();
    438                 }
    439 
    440                 @Override
    441                 public void onLoaderReset(Loader<Cursor> loader) {
    442                 }
    443             };
    444 
    445     private long mPhotoRawContactId;
    446     private Bundle mUpdatedPhotos = new Bundle();
    447 
    448     @Override
    449     public Context getContext() {
    450         return getActivity();
    451     }
    452 
    453     @Override
    454     public void onAttach(Activity activity) {
    455         super.onAttach(activity);
    456         mContext = activity;
    457         mEditorUtils = ContactEditorUtils.create(mContext);
    458         mComparator = new RawContactDeltaComparator(mContext);
    459     }
    460 
    461     @Override
    462     public void onCreate(Bundle savedState) {
    463         if (savedState != null) {
    464             // Restore mUri before calling super.onCreate so that onInitializeLoaders
    465             // would already have a uri and an action to work with
    466             mAction = savedState.getString(KEY_ACTION);
    467             mLookupUri = savedState.getParcelable(KEY_URI);
    468         }
    469 
    470         super.onCreate(savedState);
    471 
    472         if (savedState == null) {
    473             mViewIdGenerator = new ViewIdGenerator();
    474 
    475             // mState can still be null because it may not have have finished loading before
    476             // onSaveInstanceState was called.
    477             mState = new RawContactDeltaList();
    478         } else {
    479             mViewIdGenerator = savedState.getParcelable(KEY_VIEW_ID_GENERATOR);
    480 
    481             mAutoAddToDefaultGroup = savedState.getBoolean(KEY_AUTO_ADD_TO_DEFAULT_GROUP);
    482             mDisableDeleteMenuOption = savedState.getBoolean(KEY_DISABLE_DELETE_MENU_OPTION);
    483             mNewLocalProfile = savedState.getBoolean(KEY_NEW_LOCAL_PROFILE);
    484             mMaterialPalette = savedState.getParcelable(KEY_MATERIAL_PALETTE);
    485             mAccountWithDataSet = savedState.getParcelable(KEY_ACCOUNT);
    486             mRawContacts = ImmutableList.copyOf(savedState.<RawContact>getParcelableArrayList(
    487                     KEY_RAW_CONTACTS));
    488             // NOTE: mGroupMetaData is not saved/restored
    489 
    490             // Read state from savedState. No loading involved here
    491             mState = savedState.<RawContactDeltaList> getParcelable(KEY_EDIT_STATE);
    492             mStatus = savedState.getInt(KEY_STATUS);
    493 
    494             mHasNewContact = savedState.getBoolean(KEY_HAS_NEW_CONTACT);
    495             mNewContactDataReady = savedState.getBoolean(KEY_NEW_CONTACT_READY);
    496 
    497             mIsEdit = savedState.getBoolean(KEY_IS_EDIT);
    498             mExistingContactDataReady = savedState.getBoolean(KEY_EXISTING_CONTACT_READY);
    499 
    500             mIsUserProfile = savedState.getBoolean(KEY_IS_USER_PROFILE);
    501 
    502             mEnabled = savedState.getBoolean(KEY_ENABLED);
    503 
    504             // Aggregation PopupWindow
    505             mAggregationSuggestionsRawContactId = savedState.getLong(
    506                     KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID);
    507 
    508             // Join Activity
    509             mContactIdForJoin = savedState.getLong(KEY_CONTACT_ID_FOR_JOIN);
    510 
    511             mReadOnlyDisplayNameId = savedState.getLong(KEY_READ_ONLY_DISPLAY_NAME_ID);
    512             mCopyReadOnlyName = savedState.getBoolean(KEY_COPY_READ_ONLY_DISPLAY_NAME, false);
    513 
    514             mPhotoRawContactId = savedState.getLong(KEY_PHOTO_RAW_CONTACT_ID);
    515             mUpdatedPhotos = savedState.getParcelable(KEY_UPDATED_PHOTOS);
    516         }
    517     }
    518 
    519     @Override
    520     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
    521         setHasOptionsMenu(true);
    522 
    523         final View view = inflater.inflate(
    524                 R.layout.contact_editor_fragment, container, false);
    525         mContent = (LinearLayout) view.findViewById(R.id.raw_contacts_editor_view);
    526         return view;
    527     }
    528 
    529     @Override
    530     public void onActivityCreated(Bundle savedInstanceState) {
    531         super.onActivityCreated(savedInstanceState);
    532 
    533         validateAction(mAction);
    534 
    535         if (mState.isEmpty()) {
    536             // The delta list may not have finished loading before orientation change happens.
    537             // In this case, there will be a saved state but deltas will be missing.  Reload from
    538             // database.
    539             if (Intent.ACTION_EDIT.equals(mAction)) {
    540                 // Either
    541                 // 1) orientation change but load never finished.
    542                 // 2) not an orientation change so data needs to be loaded for first time.
    543                 getLoaderManager().initLoader(LOADER_CONTACT, null, mContactLoaderListener);
    544                 getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupsLoaderListener);
    545             }
    546         } else {
    547             // Orientation change, we already have mState, it was loaded by onCreate
    548             bindEditors();
    549         }
    550 
    551         // Handle initial actions only when existing state missing
    552         if (savedInstanceState == null) {
    553             if (mIntentExtras != null) {
    554                 final Account account = mIntentExtras == null ? null :
    555                         (Account) mIntentExtras.getParcelable(Intents.Insert.EXTRA_ACCOUNT);
    556                 final String dataSet = mIntentExtras == null ? null :
    557                         mIntentExtras.getString(Intents.Insert.EXTRA_DATA_SET);
    558                 mAccountWithDataSet = account != null
    559                         ? new AccountWithDataSet(account.name, account.type, dataSet)
    560                         : mIntentExtras.<AccountWithDataSet>getParcelable(
    561                                 ContactEditorActivity.EXTRA_ACCOUNT_WITH_DATA_SET);
    562             }
    563 
    564             if (Intent.ACTION_EDIT.equals(mAction)) {
    565                 mIsEdit = true;
    566             } else if (Intent.ACTION_INSERT.equals(mAction)) {
    567                 mHasNewContact = true;
    568                 if (mAccountWithDataSet != null) {
    569                     createContact(mAccountWithDataSet);
    570                 } // else wait for accounts to be loaded
    571             }
    572         }
    573 
    574         if (mHasNewContact) {
    575             AccountsLoader.loadAccounts(this, LOADER_ACCOUNTS, AccountTypeManager.writableFilter());
    576         }
    577     }
    578 
    579     /**
    580      * Checks if the requested action is valid.
    581      *
    582      * @param action The action to test.
    583      * @throws IllegalArgumentException when the action is invalid.
    584      */
    585     private static void validateAction(String action) {
    586         if (VALID_INTENT_ACTIONS.contains(action)) {
    587             return;
    588         }
    589         throw new IllegalArgumentException(
    590                 "Unknown action " + action + "; Supported actions: " + VALID_INTENT_ACTIONS);
    591     }
    592 
    593     @Override
    594     public void onSaveInstanceState(Bundle outState) {
    595         outState.putString(KEY_ACTION, mAction);
    596         outState.putParcelable(KEY_URI, mLookupUri);
    597         outState.putBoolean(KEY_AUTO_ADD_TO_DEFAULT_GROUP, mAutoAddToDefaultGroup);
    598         outState.putBoolean(KEY_DISABLE_DELETE_MENU_OPTION, mDisableDeleteMenuOption);
    599         outState.putBoolean(KEY_NEW_LOCAL_PROFILE, mNewLocalProfile);
    600         if (mMaterialPalette != null) {
    601             outState.putParcelable(KEY_MATERIAL_PALETTE, mMaterialPalette);
    602         }
    603         outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator);
    604 
    605         outState.putParcelableArrayList(KEY_RAW_CONTACTS, mRawContacts == null ?
    606                 Lists.<RawContact>newArrayList() : Lists.newArrayList(mRawContacts));
    607         // NOTE: mGroupMetaData is not saved
    608 
    609         outState.putParcelable(KEY_EDIT_STATE, mState);
    610         outState.putInt(KEY_STATUS, mStatus);
    611         outState.putBoolean(KEY_HAS_NEW_CONTACT, mHasNewContact);
    612         outState.putBoolean(KEY_NEW_CONTACT_READY, mNewContactDataReady);
    613         outState.putBoolean(KEY_IS_EDIT, mIsEdit);
    614         outState.putBoolean(KEY_EXISTING_CONTACT_READY, mExistingContactDataReady);
    615         outState.putParcelable(KEY_ACCOUNT, mAccountWithDataSet);
    616         outState.putBoolean(KEY_IS_USER_PROFILE, mIsUserProfile);
    617 
    618         outState.putBoolean(KEY_ENABLED, mEnabled);
    619 
    620         // Aggregation PopupWindow
    621         outState.putLong(KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID,
    622                 mAggregationSuggestionsRawContactId);
    623 
    624         // Join Activity
    625         outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin);
    626 
    627         outState.putLong(KEY_READ_ONLY_DISPLAY_NAME_ID, mReadOnlyDisplayNameId);
    628         outState.putBoolean(KEY_COPY_READ_ONLY_DISPLAY_NAME, mCopyReadOnlyName);
    629 
    630         outState.putLong(KEY_PHOTO_RAW_CONTACT_ID, mPhotoRawContactId);
    631         outState.putParcelable(KEY_UPDATED_PHOTOS, mUpdatedPhotos);
    632         super.onSaveInstanceState(outState);
    633     }
    634 
    635     @Override
    636     public void onStop() {
    637         super.onStop();
    638         UiClosables.closeQuietly(mAggregationSuggestionPopup);
    639     }
    640 
    641     @Override
    642     public void onDestroy() {
    643         super.onDestroy();
    644         if (mAggregationSuggestionEngine != null) {
    645             mAggregationSuggestionEngine.quit();
    646         }
    647     }
    648 
    649     @Override
    650     public void onActivityResult(int requestCode, int resultCode, Intent data) {
    651         switch (requestCode) {
    652             case REQUEST_CODE_JOIN: {
    653                 // Ignore failed requests
    654                 if (resultCode != Activity.RESULT_OK) return;
    655                 if (data != null) {
    656                     final long contactId = ContentUris.parseId(data.getData());
    657                     if (hasPendingChanges()) {
    658                         // Ask the user if they want to save changes before doing the join
    659                         JoinContactConfirmationDialogFragment.show(this, contactId);
    660                     } else {
    661                         // Do the join immediately
    662                         joinAggregate(contactId);
    663                     }
    664                 }
    665                 break;
    666             }
    667             case REQUEST_CODE_ACCOUNTS_CHANGED: {
    668                 // Bail if the account selector was not successful.
    669                 if (resultCode != Activity.RESULT_OK || data == null ||
    670                         !data.hasExtra(Intents.Insert.EXTRA_ACCOUNT)) {
    671                     if (mListener != null) {
    672                         mListener.onReverted();
    673                     }
    674                     return;
    675                 }
    676                 AccountWithDataSet account = data.getParcelableExtra(
    677                         Intents.Insert.EXTRA_ACCOUNT);
    678                 createContact(account);
    679                 break;
    680             }
    681         }
    682     }
    683 
    684     @Override
    685     public void onAccountsLoaded(List<AccountInfo> data) {
    686         mWritableAccounts = data;
    687         // The user may need to select a new account to save to
    688         if (mAccountWithDataSet == null && mHasNewContact) {
    689             selectAccountAndCreateContact();
    690         }
    691 
    692         final RawContactEditorView view = getContent();
    693         if (view == null) {
    694             return;
    695         }
    696         view.setAccounts(data);
    697         if (mAccountWithDataSet == null && view.getCurrentRawContactDelta() == null) {
    698             return;
    699         }
    700 
    701         final AccountWithDataSet account = mAccountWithDataSet != null
    702                 ? mAccountWithDataSet
    703                 : view.getCurrentRawContactDelta().getAccountWithDataSet();
    704 
    705         // The current account was removed
    706         if (!AccountInfo.contains(data, account) && !data.isEmpty()) {
    707             if (isReadyToBindEditors()) {
    708                 onRebindEditorsForNewContact(getContent().getCurrentRawContactDelta(),
    709                         account, data.get(0).getAccount());
    710             } else {
    711                 mAccountWithDataSet = data.get(0).getAccount();
    712             }
    713         }
    714     }
    715 
    716     //
    717     // Options menu
    718     //
    719 
    720     @Override
    721     public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
    722         inflater.inflate(R.menu.edit_contact, menu);
    723     }
    724 
    725     @Override
    726     public void onPrepareOptionsMenu(Menu menu) {
    727         // This supports the keyboard shortcut to save changes to a contact but shouldn't be visible
    728         // because the custom action bar contains the "save" button now (not the overflow menu).
    729         // TODO: Find a better way to handle shortcuts, i.e. onKeyDown()?
    730         final MenuItem saveMenu = menu.findItem(R.id.menu_save);
    731         final MenuItem splitMenu = menu.findItem(R.id.menu_split);
    732         final MenuItem joinMenu = menu.findItem(R.id.menu_join);
    733         final MenuItem deleteMenu = menu.findItem(R.id.menu_delete);
    734 
    735         // TODO: b/30771904, b/31827701, temporarily disable these items until we get them to work
    736         // on a raw contact level.
    737         joinMenu.setVisible(false);
    738         splitMenu.setVisible(false);
    739         deleteMenu.setVisible(false);
    740         // Save menu is invisible when there's only one read only contact in the editor.
    741         saveMenu.setVisible(!isEditingReadOnlyRawContact());
    742         if (saveMenu.isVisible()) {
    743             // Since we're using a custom action layout we have to manually hook up the handler.
    744             saveMenu.getActionView().setOnClickListener(new View.OnClickListener() {
    745                 @Override
    746                 public void onClick(View v) {
    747                     onOptionsItemSelected(saveMenu);
    748                 }
    749             });
    750         }
    751 
    752         final MenuItem helpMenu = menu.findItem(R.id.menu_help);
    753         helpMenu.setVisible(HelpUtils.isHelpAndFeedbackAvailable());
    754 
    755         int size = menu.size();
    756         for (int i = 0; i < size; i++) {
    757             menu.getItem(i).setEnabled(mEnabled);
    758         }
    759     }
    760 
    761     @Override
    762     public boolean onOptionsItemSelected(MenuItem item) {
    763         if (item.getItemId() == android.R.id.home) {
    764             return revert();
    765         }
    766 
    767         final Activity activity = getActivity();
    768         if (activity == null || activity.isFinishing() || activity.isDestroyed()) {
    769             // If we no longer are attached to a running activity want to
    770             // drain this event.
    771             return true;
    772         }
    773 
    774         final int id = item.getItemId();
    775         if (id == R.id.menu_save) {
    776             return save(SaveMode.CLOSE);
    777         } else if (id == R.id.menu_delete) {
    778             if (mListener != null) mListener.onDeleteRequested(mLookupUri);
    779             return true;
    780         } else if (id == R.id.menu_split) {
    781             return doSplitContactAction();
    782         } else if (id == R.id.menu_join) {
    783             return doJoinContactAction();
    784         } else if (id == R.id.menu_help) {
    785             HelpUtils.launchHelpAndFeedbackForContactScreen(getActivity());
    786             return true;
    787         }
    788 
    789         return false;
    790     }
    791 
    792     @Override
    793     public boolean revert() {
    794         if (mState.isEmpty() || !hasPendingChanges()) {
    795             onCancelEditConfirmed();
    796         } else {
    797             CancelEditDialogFragment.show(this);
    798         }
    799         return true;
    800     }
    801 
    802     @Override
    803     public void onCancelEditConfirmed() {
    804         // When this Fragment is closed we don't want it to auto-save
    805         mStatus = Status.CLOSING;
    806         if (mListener != null) {
    807             mListener.onReverted();
    808         }
    809     }
    810 
    811     @Override
    812     public void onSplitContactConfirmed(boolean hasPendingChanges) {
    813         if (mState.isEmpty()) {
    814             // This may happen when this Fragment is recreated by the system during users
    815             // confirming the split action (and thus this method is called just before onCreate()),
    816             // for example.
    817             Log.e(TAG, "mState became null during the user's confirming split action. " +
    818                     "Cannot perform the save action.");
    819             return;
    820         }
    821 
    822         if (!hasPendingChanges && mHasNewContact) {
    823             // If the user didn't add anything new, we don't want to split out the newly created
    824             // raw contact into a name-only contact so remove them.
    825             final Iterator<RawContactDelta> iterator = mState.iterator();
    826             while (iterator.hasNext()) {
    827                 final RawContactDelta rawContactDelta = iterator.next();
    828                 if (rawContactDelta.getRawContactId() < 0) {
    829                     iterator.remove();
    830                 }
    831             }
    832         }
    833         mState.markRawContactsForSplitting();
    834         save(SaveMode.SPLIT);
    835     }
    836 
    837     @Override
    838     public void onSplitContactCanceled() {}
    839 
    840     private boolean doSplitContactAction() {
    841         if (!hasValidState()) return false;
    842 
    843         SplitContactConfirmationDialogFragment.show(this, hasPendingChanges());
    844         return true;
    845     }
    846 
    847     private boolean doJoinContactAction() {
    848         if (!hasValidState() || mLookupUri == null) {
    849             return false;
    850         }
    851 
    852         // If we just started creating a new contact and haven't added any data, it's too
    853         // early to do a join
    854         if (mState.size() == 1 && mState.get(0).isContactInsert()
    855                 && !hasPendingChanges()) {
    856             Toast.makeText(mContext, R.string.toast_join_with_empty_contact,
    857                     Toast.LENGTH_LONG).show();
    858             return true;
    859         }
    860 
    861         showJoinAggregateActivity(mLookupUri);
    862         return true;
    863     }
    864 
    865     @Override
    866     public void onJoinContactConfirmed(long joinContactId) {
    867         doSaveAction(SaveMode.JOIN, joinContactId);
    868     }
    869 
    870     @Override
    871     public boolean save(int saveMode) {
    872         if (!hasValidState() || mStatus != Status.EDITING) {
    873             return false;
    874         }
    875 
    876         // If we are about to close the editor - there is no need to refresh the data
    877         if (saveMode == SaveMode.CLOSE || saveMode == SaveMode.EDITOR
    878                 || saveMode == SaveMode.SPLIT) {
    879             getLoaderManager().destroyLoader(LOADER_CONTACT);
    880         }
    881 
    882         mStatus = Status.SAVING;
    883 
    884         if (!hasPendingChanges()) {
    885             if (mLookupUri == null && saveMode == SaveMode.RELOAD) {
    886                 // We don't have anything to save and there isn't even an existing contact yet.
    887                 // Nothing to do, simply go back to editing mode
    888                 mStatus = Status.EDITING;
    889                 return true;
    890             }
    891             onSaveCompleted(/* hadChanges =*/ false, saveMode,
    892                     /* saveSucceeded =*/ mLookupUri != null, mLookupUri, /* joinContactId =*/ null);
    893             return true;
    894         }
    895 
    896         setEnabled(false);
    897 
    898         return doSaveAction(saveMode, /* joinContactId */ null);
    899     }
    900 
    901     //
    902     // State accessor methods
    903     //
    904 
    905     /**
    906      * Check if our internal {@link #mState} is valid, usually checked before
    907      * performing user actions.
    908      */
    909     private boolean hasValidState() {
    910         return mState.size() > 0;
    911     }
    912 
    913     private boolean isEditingUserProfile() {
    914         return mNewLocalProfile || mIsUserProfile;
    915     }
    916 
    917     /**
    918      * Whether the contact being edited is composed of read-only raw contacts
    919      * aggregated with a newly created writable raw contact.
    920      */
    921     private boolean isEditingReadOnlyRawContactWithNewContact() {
    922         return mHasNewContact && mState.size() > 1;
    923     }
    924 
    925     /**
    926      * @return true if the single raw contact we're looking at is read-only.
    927      */
    928     private boolean isEditingReadOnlyRawContact() {
    929         return hasValidState() && mRawContactIdToDisplayAlone > 0
    930                 && !mState.getByRawContactId(mRawContactIdToDisplayAlone)
    931                         .getAccountType(AccountTypeManager.getInstance(mContext))
    932                                 .areContactsWritable();
    933     }
    934 
    935     /**
    936      * Return true if there are any edits to the current contact which need to
    937      * be saved.
    938      */
    939     private boolean hasPendingRawContactChanges(Set<String> excludedMimeTypes) {
    940         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
    941         return RawContactModifier.hasChanges(mState, accountTypes, excludedMimeTypes);
    942     }
    943 
    944     /**
    945      * Determines if changes were made in the editor that need to be saved, while taking into
    946      * account that name changes are not real for read-only contacts.
    947      * See go/editing-read-only-contacts
    948      */
    949     private boolean hasPendingChanges() {
    950         if (isEditingReadOnlyRawContactWithNewContact()) {
    951             // We created a new raw contact delta with a default display name.
    952             // We must test for pending changes while ignoring the default display name.
    953             final RawContactDelta beforeRawContactDelta = mState
    954                     .getByRawContactId(mReadOnlyDisplayNameId);
    955             final ValuesDelta beforeDelta = beforeRawContactDelta == null ? null :
    956                   beforeRawContactDelta.getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
    957             final ValuesDelta pendingDelta = mState
    958                     .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
    959             if (structuredNamesAreEqual(beforeDelta, pendingDelta)) {
    960                 final Set<String> excludedMimeTypes = new HashSet<>();
    961                 excludedMimeTypes.add(StructuredName.CONTENT_ITEM_TYPE);
    962                 return hasPendingRawContactChanges(excludedMimeTypes);
    963             }
    964             return true;
    965         }
    966         return hasPendingRawContactChanges(/* excludedMimeTypes =*/ null);
    967     }
    968 
    969     /**
    970      * Compares the two {@link ValuesDelta} to see if the structured name is changed. We made a copy
    971      * of a read only delta and now we want to check if the copied delta has changes.
    972      *
    973      * @param before original {@link ValuesDelta}
    974      * @param after copied {@link ValuesDelta}
    975      * @return true if the copied {@link ValuesDelta} has all the same values in the structured
    976      * name fields as the original.
    977      */
    978     private boolean structuredNamesAreEqual(ValuesDelta before, ValuesDelta after) {
    979         if (before == after) return true;
    980         if (before == null || after == null) return false;
    981         final ContentValues original = before.getBefore();
    982         final ContentValues pending = after.getAfter();
    983         if (original != null && pending != null) {
    984             final String beforeDisplayName = original.getAsString(StructuredName.DISPLAY_NAME);
    985             final String afterDisplayName = pending.getAsString(StructuredName.DISPLAY_NAME);
    986             if (!TextUtils.equals(beforeDisplayName, afterDisplayName)) return false;
    987 
    988             final String beforePrefix = original.getAsString(StructuredName.PREFIX);
    989             final String afterPrefix = pending.getAsString(StructuredName.PREFIX);
    990             if (!TextUtils.equals(beforePrefix, afterPrefix)) return false;
    991 
    992             final String beforeFirstName = original.getAsString(StructuredName.GIVEN_NAME);
    993             final String afterFirstName = pending.getAsString(StructuredName.GIVEN_NAME);
    994             if (!TextUtils.equals(beforeFirstName, afterFirstName)) return false;
    995 
    996             final String beforeMiddleName = original.getAsString(StructuredName.MIDDLE_NAME);
    997             final String afterMiddleName = pending.getAsString(StructuredName.MIDDLE_NAME);
    998             if (!TextUtils.equals(beforeMiddleName, afterMiddleName)) return false;
    999 
   1000             final String beforeLastName = original.getAsString(StructuredName.FAMILY_NAME);
   1001             final String afterLastName = pending.getAsString(StructuredName.FAMILY_NAME);
   1002             if (!TextUtils.equals(beforeLastName, afterLastName)) return false;
   1003 
   1004             final String beforeSuffix = original.getAsString(StructuredName.SUFFIX);
   1005             final String afterSuffix = pending.getAsString(StructuredName.SUFFIX);
   1006             return TextUtils.equals(beforeSuffix, afterSuffix);
   1007         }
   1008         return false;
   1009     }
   1010 
   1011     //
   1012     // Account creation
   1013     //
   1014 
   1015     private void selectAccountAndCreateContact() {
   1016         Preconditions.checkNotNull(mWritableAccounts, "Accounts must be loaded first");
   1017         // If this is a local profile, then skip the logic about showing the accounts changed
   1018         // activity and create a phone-local contact.
   1019         if (mNewLocalProfile) {
   1020             createContact(null);
   1021             return;
   1022         }
   1023 
   1024         final List<AccountWithDataSet> accounts = AccountInfo.extractAccounts(mWritableAccounts);
   1025         // If there is no default account or the accounts have changed such that we need to
   1026         // prompt the user again, then launch the account prompt.
   1027         if (mEditorUtils.shouldShowAccountChangedNotification(accounts)) {
   1028             Intent intent = new Intent(mContext, ContactEditorAccountsChangedActivity.class);
   1029             // Prevent a second instance from being started on rotates
   1030             intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
   1031             mStatus = Status.SUB_ACTIVITY;
   1032             startActivityForResult(intent, REQUEST_CODE_ACCOUNTS_CHANGED);
   1033         } else {
   1034             // Make sure the default account is automatically set if there is only one non-device
   1035             // account.
   1036             mEditorUtils.maybeUpdateDefaultAccount(accounts);
   1037             // Otherwise, there should be a default account. Then either create a local contact
   1038             // (if default account is null) or create a contact with the specified account.
   1039             AccountWithDataSet defaultAccount = mEditorUtils.getOnlyOrDefaultAccount(accounts);
   1040             createContact(defaultAccount);
   1041         }
   1042     }
   1043 
   1044     /**
   1045      * Shows account creation screen associated with a given account.
   1046      *
   1047      * @param account may be null to signal a device-local contact should be created.
   1048      */
   1049     private void createContact(AccountWithDataSet account) {
   1050         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
   1051         final AccountType accountType = accountTypes.getAccountTypeForAccount(account);
   1052 
   1053         setStateForNewContact(account, accountType, isEditingUserProfile());
   1054     }
   1055 
   1056     //
   1057     // Data binding
   1058     //
   1059 
   1060     private void setState(Contact contact) {
   1061         // If we have already loaded data, we do not want to change it here to not confuse the user
   1062         if (!mState.isEmpty()) {
   1063             if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1064                 Log.v(TAG, "Ignoring background change. This will have to be rebased later");
   1065             }
   1066             return;
   1067         }
   1068         mContact = contact;
   1069         mRawContacts = contact.getRawContacts();
   1070 
   1071         // Check for writable raw contacts.  If there are none, then we need to create one so user
   1072         // can edit.  For the user profile case, there is already an editable contact.
   1073         if (!contact.isUserProfile() && !contact.isWritableContact(mContext)) {
   1074             mHasNewContact = true;
   1075             mReadOnlyDisplayNameId = contact.getNameRawContactId();
   1076             mCopyReadOnlyName = true;
   1077             // This is potentially an asynchronous call and will add deltas to list.
   1078             selectAccountAndCreateContact();
   1079         } else {
   1080             mHasNewContact = false;
   1081         }
   1082 
   1083         setStateForExistingContact(contact.isUserProfile(), mRawContacts);
   1084         if (mAutoAddToDefaultGroup
   1085                 && InvisibleContactUtil.isInvisibleAndAddable(contact, getContext())) {
   1086             InvisibleContactUtil.markAddToDefaultGroup(contact, mState, getContext());
   1087         }
   1088     }
   1089 
   1090     /**
   1091      * Prepare {@link #mState} for a newly created phone-local contact.
   1092      */
   1093     private void setStateForNewContact(AccountWithDataSet account, AccountType accountType,
   1094             boolean isUserProfile) {
   1095         setStateForNewContact(account, accountType, /* oldState =*/ null,
   1096                 /* oldAccountType =*/ null, isUserProfile);
   1097     }
   1098 
   1099     /**
   1100      * Prepare {@link #mState} for a newly created phone-local contact, migrating the state
   1101      * specified by oldState and oldAccountType.
   1102      */
   1103     private void setStateForNewContact(AccountWithDataSet account, AccountType accountType,
   1104             RawContactDelta oldState, AccountType oldAccountType, boolean isUserProfile) {
   1105         mStatus = Status.EDITING;
   1106         mAccountWithDataSet = account;
   1107         mState.add(createNewRawContactDelta(account, accountType, oldState, oldAccountType));
   1108         mIsUserProfile = isUserProfile;
   1109         mNewContactDataReady = true;
   1110         bindEditors();
   1111     }
   1112 
   1113     /**
   1114      * Returns a {@link RawContactDelta} for a new contact suitable for addition into
   1115      * {@link #mState}.
   1116      *
   1117      * If oldState and oldAccountType are specified, the state specified by those parameters
   1118      * is migrated to the result {@link RawContactDelta}.
   1119      */
   1120     private RawContactDelta createNewRawContactDelta(AccountWithDataSet account,
   1121             AccountType accountType, RawContactDelta oldState, AccountType oldAccountType) {
   1122         final RawContact rawContact = new RawContact();
   1123         if (account != null) {
   1124             rawContact.setAccount(account);
   1125         } else {
   1126             rawContact.setAccountToLocal();
   1127         }
   1128 
   1129         final RawContactDelta result = new RawContactDelta(
   1130                 ValuesDelta.fromAfter(rawContact.getValues()));
   1131         if (oldState == null) {
   1132             // Parse any values from incoming intent
   1133             RawContactModifier.parseExtras(mContext, accountType, result, mIntentExtras);
   1134         } else {
   1135             RawContactModifier.migrateStateForNewContact(
   1136                     mContext, oldState, result, oldAccountType, accountType);
   1137         }
   1138 
   1139         // Ensure we have some default fields (if the account type does not support a field,
   1140         // ensureKind will not add it, so it is safe to add e.g. Event)
   1141         RawContactModifier.ensureKindExists(result, accountType, StructuredName.CONTENT_ITEM_TYPE);
   1142         RawContactModifier.ensureKindExists(result, accountType, Phone.CONTENT_ITEM_TYPE);
   1143         RawContactModifier.ensureKindExists(result, accountType, Email.CONTENT_ITEM_TYPE);
   1144         RawContactModifier.ensureKindExists(result, accountType, Organization.CONTENT_ITEM_TYPE);
   1145         RawContactModifier.ensureKindExists(result, accountType, Event.CONTENT_ITEM_TYPE);
   1146         RawContactModifier.ensureKindExists(result, accountType,
   1147                 StructuredPostal.CONTENT_ITEM_TYPE);
   1148 
   1149         // Set the correct URI for saving the contact as a profile
   1150         if (mNewLocalProfile) {
   1151             result.setProfileQueryUri();
   1152         }
   1153 
   1154         return result;
   1155     }
   1156 
   1157     /**
   1158      * Prepare {@link #mState} for an existing contact.
   1159      */
   1160     private void setStateForExistingContact(boolean isUserProfile,
   1161             ImmutableList<RawContact> rawContacts) {
   1162         setEnabled(true);
   1163 
   1164         mState.addAll(rawContacts.iterator());
   1165         setIntentExtras(mIntentExtras);
   1166         mIntentExtras = null;
   1167 
   1168         // For user profile, change the contacts query URI
   1169         mIsUserProfile = isUserProfile;
   1170         boolean localProfileExists = false;
   1171 
   1172         if (mIsUserProfile) {
   1173             for (RawContactDelta rawContactDelta : mState) {
   1174                 // For profile contacts, we need a different query URI
   1175                 rawContactDelta.setProfileQueryUri();
   1176                 // Try to find a local profile contact
   1177                 if (rawContactDelta.getValues().getAsString(RawContacts.ACCOUNT_TYPE) == null) {
   1178                     localProfileExists = true;
   1179                 }
   1180             }
   1181             // Editor should always present a local profile for editing
   1182             // TODO(wjang): Need to figure out when this case comes up.  We can't do this if we're
   1183             // going to prune all but the one raw contact that we're trying to display by itself.
   1184             if (!localProfileExists && mRawContactIdToDisplayAlone <= 0) {
   1185                 mState.add(createLocalRawContactDelta());
   1186             }
   1187         }
   1188         mExistingContactDataReady = true;
   1189         bindEditors();
   1190     }
   1191 
   1192     /**
   1193      * Set the enabled state of editors.
   1194      */
   1195     private void setEnabled(boolean enabled) {
   1196         if (mEnabled != enabled) {
   1197             mEnabled = enabled;
   1198 
   1199             // Enable/disable editors
   1200             if (mContent != null) {
   1201                 int count = mContent.getChildCount();
   1202                 for (int i = 0; i < count; i++) {
   1203                     mContent.getChildAt(i).setEnabled(enabled);
   1204                 }
   1205             }
   1206 
   1207             // Maybe invalidate the options menu
   1208             final Activity activity = getActivity();
   1209             if (activity != null) activity.invalidateOptionsMenu();
   1210         }
   1211     }
   1212 
   1213     /**
   1214      * Returns a {@link RawContactDelta} for a local contact suitable for addition into
   1215      * {@link #mState}.
   1216      */
   1217     private static RawContactDelta createLocalRawContactDelta() {
   1218         final RawContact rawContact = new RawContact();
   1219         rawContact.setAccountToLocal();
   1220 
   1221         final RawContactDelta result = new RawContactDelta(
   1222                 ValuesDelta.fromAfter(rawContact.getValues()));
   1223         result.setProfileQueryUri();
   1224 
   1225         return result;
   1226     }
   1227 
   1228     private void copyReadOnlyName() {
   1229         // We should only ever be doing this if we're creating a new writable contact to attach to
   1230         // a read only contact.
   1231         if (!isEditingReadOnlyRawContactWithNewContact()) {
   1232             return;
   1233         }
   1234         final int writableIndex = mState.indexOfFirstWritableRawContact(getContext());
   1235         final RawContactDelta writable = mState.get(writableIndex);
   1236         final RawContactDelta readOnly = mState.getByRawContactId(mContact.getNameRawContactId());
   1237         final ValuesDelta writeNameDelta = writable
   1238                 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
   1239         final ValuesDelta readNameDelta = readOnly
   1240                 .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
   1241         mCopyReadOnlyName = false;
   1242         if (writeNameDelta == null || readNameDelta == null) {
   1243             return;
   1244         }
   1245         writeNameDelta.copyStructuredNameFieldsFrom(readNameDelta);
   1246     }
   1247 
   1248     /**
   1249      * Bind editors using {@link #mState} and other members initialized from the loaded (or new)
   1250      * Contact.
   1251      */
   1252     protected void bindEditors() {
   1253         if (!isReadyToBindEditors()) {
   1254             return;
   1255         }
   1256 
   1257         // Add input fields for the loaded Contact
   1258         final RawContactEditorView editorView = getContent();
   1259         editorView.setListener(this);
   1260         if (mCopyReadOnlyName) {
   1261             copyReadOnlyName();
   1262         }
   1263         editorView.setState(mState, mMaterialPalette, mViewIdGenerator,
   1264                 mHasNewContact, mIsUserProfile, mAccountWithDataSet,
   1265                 mRawContactIdToDisplayAlone);
   1266         if (isEditingReadOnlyRawContact()) {
   1267             final Toolbar toolbar = getEditorActivity().getToolbar();
   1268             if (toolbar != null) {
   1269                 toolbar.setTitle(R.string.contact_editor_title_read_only_contact);
   1270                 // Set activity title for Talkback
   1271                 getEditorActivity().setTitle(R.string.contact_editor_title_read_only_contact);
   1272                 toolbar.setNavigationIcon(R.drawable.quantum_ic_arrow_back_vd_theme_24);
   1273                 toolbar.setNavigationContentDescription(R.string.back_arrow_content_description);
   1274                 toolbar.getNavigationIcon().setAutoMirrored(true);
   1275             }
   1276         }
   1277 
   1278         // Set up the photo widget
   1279         editorView.setPhotoListener(this);
   1280         mPhotoRawContactId = editorView.getPhotoRawContactId();
   1281         // If there is an updated full resolution photo apply it now, this will be the case if
   1282         // the user selects or takes a new photo, then rotates the device.
   1283         final Uri uri = (Uri) mUpdatedPhotos.get(String.valueOf(mPhotoRawContactId));
   1284         if (uri != null) {
   1285             editorView.setFullSizePhoto(uri);
   1286         }
   1287         final StructuredNameEditorView nameEditor = editorView.getNameEditorView();
   1288         final TextFieldsEditorView phoneticNameEditor = editorView.getPhoneticEditorView();
   1289         final boolean useJapaneseOrder =
   1290                        Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage());
   1291         if (useJapaneseOrder && nameEditor != null && phoneticNameEditor != null) {
   1292             nameEditor.setPhoneticView(phoneticNameEditor);
   1293         }
   1294 
   1295         // The editor is ready now so make it visible
   1296         editorView.setEnabled(mEnabled);
   1297         editorView.setVisibility(View.VISIBLE);
   1298 
   1299         // Refresh the ActionBar as the visibility of the join command
   1300         // Activity can be null if we have been detached from the Activity.
   1301         invalidateOptionsMenu();
   1302     }
   1303 
   1304     /**
   1305      * Invalidates the options menu if we are still associated with an Activity.
   1306      */
   1307     private void invalidateOptionsMenu() {
   1308         final Activity activity = getActivity();
   1309         if (activity != null) {
   1310             activity.invalidateOptionsMenu();
   1311         }
   1312     }
   1313 
   1314     private boolean isReadyToBindEditors() {
   1315         if (mState.isEmpty()) {
   1316             if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1317                 Log.v(TAG, "No data to bind editors");
   1318             }
   1319             return false;
   1320         }
   1321         if (mIsEdit && !mExistingContactDataReady) {
   1322             if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1323                 Log.v(TAG, "Existing contact data is not ready to bind editors.");
   1324             }
   1325             return false;
   1326         }
   1327         if (mHasNewContact && !mNewContactDataReady) {
   1328             if (Log.isLoggable(TAG, Log.VERBOSE)) {
   1329                 Log.v(TAG, "New contact data is not ready to bind editors.");
   1330             }
   1331             return false;
   1332         }
   1333         // Don't attempt to bind anything if we have no permissions.
   1334         return RequestPermissionsActivity.hasRequiredPermissions(mContext);
   1335     }
   1336 
   1337     /**
   1338      * Removes a current editor ({@link #mState}) and rebinds new editor for a new account.
   1339      * Some of old data are reused with new restriction enforced by the new account.
   1340      *
   1341      * @param oldState Old data being edited.
   1342      * @param oldAccount Old account associated with oldState.
   1343      * @param newAccount New account to be used.
   1344      */
   1345     private void rebindEditorsForNewContact(
   1346             RawContactDelta oldState, AccountWithDataSet oldAccount,
   1347             AccountWithDataSet newAccount) {
   1348         AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
   1349         AccountType oldAccountType = accountTypes.getAccountTypeForAccount(oldAccount);
   1350         AccountType newAccountType = accountTypes.getAccountTypeForAccount(newAccount);
   1351 
   1352         mExistingContactDataReady = false;
   1353         mNewContactDataReady = false;
   1354         mState = new RawContactDeltaList();
   1355         setStateForNewContact(newAccount, newAccountType, oldState, oldAccountType,
   1356                 isEditingUserProfile());
   1357         if (mIsEdit) {
   1358             setStateForExistingContact(isEditingUserProfile(), mRawContacts);
   1359         }
   1360     }
   1361 
   1362     //
   1363     // ContactEditor
   1364     //
   1365 
   1366     @Override
   1367     public void setListener(Listener listener) {
   1368         mListener = listener;
   1369     }
   1370 
   1371     @Override
   1372     public void load(String action, Uri lookupUri, Bundle intentExtras) {
   1373         mAction = action;
   1374         mLookupUri = lookupUri;
   1375         mIntentExtras = intentExtras;
   1376 
   1377         if (mIntentExtras != null) {
   1378             mAutoAddToDefaultGroup =
   1379                     mIntentExtras.containsKey(INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY);
   1380             mNewLocalProfile =
   1381                     mIntentExtras.getBoolean(INTENT_EXTRA_NEW_LOCAL_PROFILE);
   1382             mDisableDeleteMenuOption =
   1383                     mIntentExtras.getBoolean(INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION);
   1384             if (mIntentExtras.containsKey(INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR)
   1385                     && mIntentExtras.containsKey(INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR)) {
   1386                 mMaterialPalette = new MaterialColorMapUtils.MaterialPalette(
   1387                         mIntentExtras.getInt(INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR),
   1388                         mIntentExtras.getInt(INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR));
   1389             }
   1390             mRawContactIdToDisplayAlone = mIntentExtras
   1391                     .getLong(INTENT_EXTRA_RAW_CONTACT_ID_TO_DISPLAY_ALONE);
   1392         }
   1393     }
   1394 
   1395     @Override
   1396     public void setIntentExtras(Bundle extras) {
   1397         getContent().setIntentExtras(extras);
   1398     }
   1399 
   1400     @Override
   1401     public void onJoinCompleted(Uri uri) {
   1402         onSaveCompleted(false, SaveMode.RELOAD, uri != null, uri, /* joinContactId */ null);
   1403     }
   1404 
   1405 
   1406     private String getNameToDisplay(Uri contactUri) {
   1407         // The contact has been deleted or the uri is otherwise no longer right.
   1408         if (contactUri == null) {
   1409             return null;
   1410         }
   1411         final ContentResolver resolver = mContext.getContentResolver();
   1412         final Cursor cursor = resolver.query(contactUri, new String[]{
   1413                 ContactsContract.Contacts.DISPLAY_NAME,
   1414                 ContactsContract.Contacts.DISPLAY_NAME_ALTERNATIVE}, null, null, null);
   1415 
   1416         if (cursor != null) {
   1417             try {
   1418                 if (cursor.moveToFirst()) {
   1419                     final String displayName = cursor.getString(0);
   1420                     final String displayNameAlt = cursor.getString(1);
   1421                     cursor.close();
   1422                     return ContactDisplayUtils.getPreferredDisplayName(displayName, displayNameAlt,
   1423                             new ContactsPreferences(mContext));
   1424                 }
   1425             } finally {
   1426                 cursor.close();
   1427             }
   1428         }
   1429         return null;
   1430     }
   1431 
   1432 
   1433     @Override
   1434     public void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded,
   1435             Uri contactLookupUri, Long joinContactId) {
   1436         if (hadChanges) {
   1437             if (saveSucceeded) {
   1438                 switch (saveMode) {
   1439                     case SaveMode.JOIN:
   1440                         break;
   1441                     case SaveMode.SPLIT:
   1442                         Toast.makeText(mContext, R.string.contactUnlinkedToast, Toast.LENGTH_SHORT)
   1443                                 .show();
   1444                         break;
   1445                     default:
   1446                         final String displayName = getNameToDisplay(contactLookupUri);
   1447                         final String toastMessage;
   1448                         if (!TextUtils.isEmpty(displayName)) {
   1449                             toastMessage = getResources().getString(
   1450                                     R.string.contactSavedNamedToast, displayName);
   1451                         } else {
   1452                             toastMessage = getResources().getString(R.string.contactSavedToast);
   1453                         }
   1454                         Toast.makeText(mContext, toastMessage, Toast.LENGTH_SHORT).show();
   1455                 }
   1456 
   1457             } else {
   1458                 Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
   1459             }
   1460         }
   1461         switch (saveMode) {
   1462             case SaveMode.CLOSE: {
   1463                 final Intent resultIntent;
   1464                 if (saveSucceeded && contactLookupUri != null) {
   1465                     final Uri lookupUri = ContactEditorUtils.maybeConvertToLegacyLookupUri(
   1466                             mContext, contactLookupUri, mLookupUri);
   1467                     resultIntent = ImplicitIntentsUtil.composeQuickContactIntent(
   1468                             mContext, lookupUri, ScreenType.EDITOR);
   1469                     resultIntent.putExtra(QuickContactActivity.EXTRA_CONTACT_EDITED, true);
   1470                 } else {
   1471                     resultIntent = null;
   1472                 }
   1473                 // It is already saved, so prevent it from being saved again
   1474                 mStatus = Status.CLOSING;
   1475                 if (mListener != null) mListener.onSaveFinished(resultIntent);
   1476                 break;
   1477             }
   1478             case SaveMode.EDITOR: {
   1479                 // It is already saved, so prevent it from being saved again
   1480                 mStatus = Status.CLOSING;
   1481                 if (mListener != null) mListener.onSaveFinished(/* resultIntent= */ null);
   1482                 break;
   1483             }
   1484             case SaveMode.JOIN:
   1485                 if (saveSucceeded && contactLookupUri != null && joinContactId != null) {
   1486                     joinAggregate(joinContactId);
   1487                 }
   1488                 break;
   1489             case SaveMode.RELOAD:
   1490                 if (saveSucceeded && contactLookupUri != null) {
   1491                     // If this was in INSERT, we are changing into an EDIT now.
   1492                     // If it already was an EDIT, we are changing to the new Uri now
   1493                     mState = new RawContactDeltaList();
   1494                     load(Intent.ACTION_EDIT, contactLookupUri, null);
   1495                     mStatus = Status.LOADING;
   1496                     getLoaderManager().restartLoader(LOADER_CONTACT, null, mContactLoaderListener);
   1497                 }
   1498                 break;
   1499 
   1500             case SaveMode.SPLIT:
   1501                 mStatus = Status.CLOSING;
   1502                 if (mListener != null) {
   1503                     mListener.onContactSplit(contactLookupUri);
   1504                 } else if (Log.isLoggable(TAG, Log.DEBUG)) {
   1505                     Log.d(TAG, "No listener registered, can not call onSplitFinished");
   1506                 }
   1507                 break;
   1508         }
   1509     }
   1510 
   1511     /**
   1512      * Shows a list of aggregates that can be joined into the currently viewed aggregate.
   1513      *
   1514      * @param contactLookupUri the fresh URI for the currently edited contact (after saving it)
   1515      */
   1516     private void showJoinAggregateActivity(Uri contactLookupUri) {
   1517         if (contactLookupUri == null || !isAdded()) {
   1518             return;
   1519         }
   1520 
   1521         mContactIdForJoin = ContentUris.parseId(contactLookupUri);
   1522         final Intent intent = new Intent(mContext, ContactSelectionActivity.class);
   1523         intent.setAction(UiIntentActions.PICK_JOIN_CONTACT_ACTION);
   1524         intent.putExtra(UiIntentActions.TARGET_CONTACT_ID_EXTRA_KEY, mContactIdForJoin);
   1525         startActivityForResult(intent, REQUEST_CODE_JOIN);
   1526     }
   1527 
   1528     //
   1529     // Aggregation PopupWindow
   1530     //
   1531 
   1532     /**
   1533      * Triggers an asynchronous search for aggregation suggestions.
   1534      */
   1535     protected void acquireAggregationSuggestions(Context context,
   1536             long rawContactId, ValuesDelta valuesDelta) {
   1537         mAggregationSuggestionsRawContactId = rawContactId;
   1538 
   1539         if (mAggregationSuggestionEngine == null) {
   1540             mAggregationSuggestionEngine = new AggregationSuggestionEngine(context);
   1541             mAggregationSuggestionEngine.setListener(this);
   1542             mAggregationSuggestionEngine.start();
   1543         }
   1544 
   1545         mAggregationSuggestionEngine.setContactId(getContactId());
   1546         mAggregationSuggestionEngine.setAccountFilter(
   1547                 getContent().getCurrentRawContactDelta().getAccountWithDataSet());
   1548 
   1549         mAggregationSuggestionEngine.onNameChange(valuesDelta);
   1550     }
   1551 
   1552     /**
   1553      * Returns the contact ID for the currently edited contact or 0 if the contact is new.
   1554      */
   1555     private long getContactId() {
   1556         for (RawContactDelta rawContact : mState) {
   1557             Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID);
   1558             if (contactId != null) {
   1559                 return contactId;
   1560             }
   1561         }
   1562         return 0;
   1563     }
   1564 
   1565     @Override
   1566     public void onAggregationSuggestionChange() {
   1567         final Activity activity = getActivity();
   1568         if ((activity != null && activity.isFinishing())
   1569                 || !isVisible() ||  mState.isEmpty() || mStatus != Status.EDITING) {
   1570             return;
   1571         }
   1572 
   1573         UiClosables.closeQuietly(mAggregationSuggestionPopup);
   1574 
   1575         if (mAggregationSuggestionEngine.getSuggestedContactCount() == 0) {
   1576             return;
   1577         }
   1578 
   1579         final View anchorView = getAggregationAnchorView();
   1580         if (anchorView == null) {
   1581             return; // Raw contact deleted?
   1582         }
   1583         mAggregationSuggestionPopup = new ListPopupWindow(mContext, null);
   1584         mAggregationSuggestionPopup.setAnchorView(anchorView);
   1585         mAggregationSuggestionPopup.setWidth(anchorView.getWidth());
   1586         mAggregationSuggestionPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
   1587         mAggregationSuggestionPopup.setAdapter(
   1588                 new AggregationSuggestionAdapter(
   1589                         getActivity(),
   1590                         /* listener =*/ this,
   1591                         mAggregationSuggestionEngine.getSuggestions()));
   1592         mAggregationSuggestionPopup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
   1593             @Override
   1594             public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
   1595                 final AggregationSuggestionView suggestionView = (AggregationSuggestionView) view;
   1596                 suggestionView.handleItemClickEvent();
   1597                 UiClosables.closeQuietly(mAggregationSuggestionPopup);
   1598                 mAggregationSuggestionPopup = null;
   1599             }
   1600         });
   1601         mAggregationSuggestionPopup.show();
   1602     }
   1603 
   1604     /**
   1605      * Returns the editor view that should be used as the anchor for aggregation suggestions.
   1606      */
   1607     protected View getAggregationAnchorView() {
   1608         return getContent().getAggregationAnchorView();
   1609     }
   1610 
   1611     /**
   1612      * Joins the suggested contact (specified by the id's of constituent raw
   1613      * contacts), save all changes, and stay in the editor.
   1614      */
   1615     public void doJoinSuggestedContact(long[] rawContactIds) {
   1616         if (!hasValidState() || mStatus != Status.EDITING) {
   1617             return;
   1618         }
   1619 
   1620         mState.setJoinWithRawContacts(rawContactIds);
   1621         save(SaveMode.RELOAD);
   1622     }
   1623 
   1624     @Override
   1625     public void onEditAction(Uri contactLookupUri, long rawContactId) {
   1626         SuggestionEditConfirmationDialogFragment.show(this, contactLookupUri, rawContactId);
   1627     }
   1628 
   1629     /**
   1630      * Abandons the currently edited contact and switches to editing the selected raw contact,
   1631      * transferring all the data there
   1632      */
   1633     public void doEditSuggestedContact(Uri contactUri, long rawContactId) {
   1634         if (mListener != null) {
   1635             // make sure we don't save this contact when closing down
   1636             mStatus = Status.CLOSING;
   1637             mListener.onEditOtherRawContactRequested(contactUri, rawContactId,
   1638                     getContent().getCurrentRawContactDelta().getContentValues());
   1639         }
   1640     }
   1641 
   1642     /**
   1643      * Sets group metadata on all bound editors.
   1644      */
   1645     protected void setGroupMetaData() {
   1646         if (mGroupMetaData != null) {
   1647             getContent().setGroupMetaData(mGroupMetaData);
   1648         }
   1649     }
   1650 
   1651     /**
   1652      * Persist the accumulated editor deltas.
   1653      *
   1654      * @param joinContactId the raw contact ID to join the contact being saved to after the save,
   1655      *         may be null.
   1656      */
   1657     protected boolean doSaveAction(int saveMode, Long joinContactId) {
   1658         final Intent intent = ContactSaveService.createSaveContactIntent(mContext, mState,
   1659                 SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(),
   1660                 ((Activity) mContext).getClass(),
   1661                 ContactEditorActivity.ACTION_SAVE_COMPLETED, mUpdatedPhotos,
   1662                 JOIN_CONTACT_ID_EXTRA_KEY, joinContactId);
   1663         return startSaveService(mContext, intent, saveMode);
   1664     }
   1665 
   1666     private boolean startSaveService(Context context, Intent intent, int saveMode) {
   1667         final boolean result = ContactSaveService.startService(
   1668                 context, intent, saveMode);
   1669         if (!result) {
   1670             onCancelEditConfirmed();
   1671         }
   1672         return result;
   1673     }
   1674 
   1675     //
   1676     // Join Activity
   1677     //
   1678 
   1679     /**
   1680      * Performs aggregation with the contact selected by the user from suggestions or A-Z list.
   1681      */
   1682     protected void joinAggregate(final long contactId) {
   1683         final Intent intent = ContactSaveService.createJoinContactsIntent(
   1684                 mContext, mContactIdForJoin, contactId, ContactEditorActivity.class,
   1685                 ContactEditorActivity.ACTION_JOIN_COMPLETED);
   1686         mContext.startService(intent);
   1687     }
   1688 
   1689     public void removePhoto() {
   1690         getContent().removePhoto();
   1691         mUpdatedPhotos.remove(String.valueOf(mPhotoRawContactId));
   1692     }
   1693 
   1694     public void updatePhoto(Uri uri) throws FileNotFoundException {
   1695         final Bitmap bitmap = ContactPhotoUtils.getBitmapFromUri(getActivity(), uri);
   1696         if (bitmap == null || bitmap.getHeight() <= 0 || bitmap.getWidth() <= 0) {
   1697             Toast.makeText(mContext, R.string.contactPhotoSavedErrorToast,
   1698                     Toast.LENGTH_SHORT).show();
   1699             return;
   1700         }
   1701         mUpdatedPhotos.putParcelable(String.valueOf(mPhotoRawContactId), uri);
   1702         getContent().updatePhoto(uri);
   1703     }
   1704 
   1705     public void setPrimaryPhoto() {
   1706         getContent().setPrimaryPhoto();
   1707     }
   1708 
   1709     @Override
   1710     public void onNameFieldChanged(long rawContactId, ValuesDelta valuesDelta) {
   1711         final Activity activity = getActivity();
   1712         if (activity == null || activity.isFinishing()) {
   1713             return;
   1714         }
   1715         acquireAggregationSuggestions(activity, rawContactId, valuesDelta);
   1716     }
   1717 
   1718     @Override
   1719     public void onRebindEditorsForNewContact(RawContactDelta oldState,
   1720             AccountWithDataSet oldAccount, AccountWithDataSet newAccount) {
   1721         mNewContactAccountChanged = true;
   1722         rebindEditorsForNewContact(oldState, oldAccount, newAccount);
   1723     }
   1724 
   1725     @Override
   1726     public void onBindEditorsFailed() {
   1727         final Activity activity = getActivity();
   1728         if (activity != null && !activity.isFinishing()) {
   1729             Toast.makeText(activity, R.string.editor_failed_to_load,
   1730                     Toast.LENGTH_SHORT).show();
   1731             activity.setResult(Activity.RESULT_CANCELED);
   1732             activity.finish();
   1733         }
   1734     }
   1735 
   1736     @Override
   1737     public void onEditorsBound() {
   1738         final Activity activity = getActivity();
   1739         if (activity == null || activity.isFinishing()) {
   1740             return;
   1741         }
   1742         getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupsLoaderListener);
   1743     }
   1744 
   1745     @Override
   1746     public void onPhotoEditorViewClicked() {
   1747         // For contacts composed of a single writable raw contact, or raw contacts have no more
   1748         // than 1 photo, clicking the photo view simply opens the source photo dialog
   1749         getEditorActivity().changePhoto(getPhotoMode());
   1750     }
   1751 
   1752     private int getPhotoMode() {
   1753         return getContent().isWritablePhotoSet() ? PhotoActionPopup.Modes.WRITE_ABLE_PHOTO
   1754                 : PhotoActionPopup.Modes.NO_PHOTO;
   1755     }
   1756 
   1757     private ContactEditorActivity getEditorActivity() {
   1758         return (ContactEditorActivity) getActivity();
   1759     }
   1760 
   1761     private RawContactEditorView getContent() {
   1762         return (RawContactEditorView) mContent;
   1763     }
   1764 }
   1765