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