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