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