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