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