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