Home | History | Annotate | Download | only in editor
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 
     17 package com.android.contacts.editor;
     18 
     19 import com.android.contacts.common.logging.ScreenEvent.ScreenType;
     20 import com.google.common.collect.ImmutableList;
     21 import com.google.common.collect.Lists;
     22 
     23 import com.android.contacts.ContactSaveService;
     24 import com.android.contacts.GroupMetaDataLoader;
     25 import com.android.contacts.R;
     26 import com.android.contacts.activities.ContactEditorAccountsChangedActivity;
     27 import com.android.contacts.activities.ContactEditorBaseActivity;
     28 import com.android.contacts.activities.ContactEditorBaseActivity.ContactEditor;
     29 import com.android.contacts.common.model.AccountTypeManager;
     30 import com.android.contacts.common.model.Contact;
     31 import com.android.contacts.common.model.ContactLoader;
     32 import com.android.contacts.common.model.RawContact;
     33 import com.android.contacts.common.model.RawContactDelta;
     34 import com.android.contacts.common.model.RawContactDeltaList;
     35 import com.android.contacts.common.model.RawContactModifier;
     36 import com.android.contacts.common.model.ValuesDelta;
     37 import com.android.contacts.common.model.account.AccountType;
     38 import com.android.contacts.common.model.account.AccountWithDataSet;
     39 import com.android.contacts.common.util.ImplicitIntentsUtil;
     40 import com.android.contacts.common.util.MaterialColorMapUtils;
     41 import com.android.contacts.editor.AggregationSuggestionEngine.Suggestion;
     42 import com.android.contacts.list.UiIntentActions;
     43 import com.android.contacts.quickcontact.QuickContactActivity;
     44 import com.android.contacts.util.HelpUtils;
     45 import com.android.contacts.util.PhoneCapabilityTester;
     46 import com.android.contacts.util.UiClosables;
     47 
     48 import android.accounts.Account;
     49 import android.app.Activity;
     50 import android.app.Fragment;
     51 import android.app.LoaderManager;
     52 import android.content.ActivityNotFoundException;
     53 import android.content.ContentUris;
     54 import android.content.ContentValues;
     55 import android.content.Context;
     56 import android.content.CursorLoader;
     57 import android.content.Intent;
     58 import android.content.Loader;
     59 import android.database.Cursor;
     60 import android.media.RingtoneManager;
     61 import android.net.Uri;
     62 import android.os.Bundle;
     63 import android.os.SystemClock;
     64 import android.provider.ContactsContract;
     65 import android.provider.ContactsContract.CommonDataKinds.Email;
     66 import android.provider.ContactsContract.CommonDataKinds.Event;
     67 import android.provider.ContactsContract.CommonDataKinds.Organization;
     68 import android.provider.ContactsContract.CommonDataKinds.Phone;
     69 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
     70 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
     71 import android.provider.ContactsContract.Contacts;
     72 import android.provider.ContactsContract.Intents;
     73 import android.provider.ContactsContract.RawContacts;
     74 import android.util.Log;
     75 import android.view.LayoutInflater;
     76 import android.view.Menu;
     77 import android.view.MenuInflater;
     78 import android.view.MenuItem;
     79 import android.view.View;
     80 import android.view.ViewGroup;
     81 import android.widget.AdapterView;
     82 import android.widget.BaseAdapter;
     83 import android.widget.LinearLayout;
     84 import android.widget.ListPopupWindow;
     85 import android.widget.Toast;
     86 
     87 import java.util.ArrayList;
     88 import java.util.HashSet;
     89 import java.util.Iterator;
     90 import java.util.List;
     91 import java.util.Set;
     92 
     93 /**
     94  * Base Fragment for contact editors.
     95  */
     96 abstract public class ContactEditorBaseFragment extends Fragment implements
     97         ContactEditor, SplitContactConfirmationDialogFragment.Listener,
     98         JoinContactConfirmationDialogFragment.Listener,
     99         AggregationSuggestionEngine.Listener, AggregationSuggestionView.Listener,
    100         CancelEditDialogFragment.Listener {
    101 
    102     static final String TAG = "ContactEditor";
    103 
    104     protected static final int LOADER_CONTACT = 1;
    105     protected static final int LOADER_GROUPS = 2;
    106 
    107     private static final List<String> VALID_INTENT_ACTIONS = new ArrayList<String>() {{
    108         add(Intent.ACTION_EDIT);
    109         add(Intent.ACTION_INSERT);
    110         add(ContactEditorBaseActivity.ACTION_EDIT);
    111         add(ContactEditorBaseActivity.ACTION_INSERT);
    112         add(ContactEditorBaseActivity.ACTION_SAVE_COMPLETED);
    113     }};
    114 
    115     private static final String KEY_ACTION = "action";
    116     private static final String KEY_URI = "uri";
    117     private static final String KEY_AUTO_ADD_TO_DEFAULT_GROUP = "autoAddToDefaultGroup";
    118     private static final String KEY_DISABLE_DELETE_MENU_OPTION = "disableDeleteMenuOption";
    119     private static final String KEY_NEW_LOCAL_PROFILE = "newLocalProfile";
    120     private static final String KEY_MATERIAL_PALETTE = "materialPalette";
    121     private static final String KEY_PHOTO_ID = "photoId";
    122 
    123     private static final String KEY_VIEW_ID_GENERATOR = "viewidgenerator";
    124 
    125     private static final String KEY_RAW_CONTACTS = "rawContacts";
    126 
    127     private static final String KEY_EDIT_STATE = "state";
    128     private static final String KEY_STATUS = "status";
    129 
    130     private static final String KEY_HAS_NEW_CONTACT = "hasNewContact";
    131     private static final String KEY_NEW_CONTACT_READY = "newContactDataReady";
    132 
    133     private static final String KEY_IS_EDIT = "isEdit";
    134     private static final String KEY_EXISTING_CONTACT_READY = "existingContactDataReady";
    135 
    136     private static final String KEY_RAW_CONTACT_DISPLAY_ALONE_IS_READ_ONLY = "isReadOnly";
    137 
    138     // Phone option menus
    139     private static final String KEY_SEND_TO_VOICE_MAIL_STATE = "sendToVoicemailState";
    140     private static final String KEY_ARE_PHONE_OPTIONS_CHANGEABLE = "arePhoneOptionsChangable";
    141     private static final String KEY_CUSTOM_RINGTONE = "customRingtone";
    142 
    143     private static final String KEY_IS_USER_PROFILE = "isUserProfile";
    144 
    145     private static final String KEY_ENABLED = "enabled";
    146 
    147     // Aggregation PopupWindow
    148     private static final String KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID =
    149             "aggregationSuggestionsRawContactId";
    150 
    151     // Join Activity
    152     private static final String KEY_CONTACT_ID_FOR_JOIN = "contactidforjoin";
    153 
    154     private static final String KEY_READ_ONLY_DISPLAY_NAME = "readOnlyDisplayName";
    155 
    156     protected static final int REQUEST_CODE_JOIN = 0;
    157     protected static final int REQUEST_CODE_ACCOUNTS_CHANGED = 1;
    158     protected static final int REQUEST_CODE_PICK_RINGTONE = 2;
    159 
    160     private static final int CURRENT_API_VERSION = android.os.Build.VERSION.SDK_INT;
    161 
    162     /**
    163      * An intent extra that forces the editor to add the edited contact
    164      * to the default group (e.g. "My Contacts").
    165      */
    166     public static final String INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY = "addToDefaultDirectory";
    167 
    168     public static final String INTENT_EXTRA_NEW_LOCAL_PROFILE = "newLocalProfile";
    169 
    170     public static final String INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION =
    171             "disableDeleteMenuOption";
    172 
    173     /**
    174      * Intent key to pass the photo palette primary color calculated by
    175      * {@link com.android.contacts.quickcontact.QuickContactActivity} to the editor and between
    176      * the compact and fully expanded editors.
    177      */
    178     public static final String INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR =
    179             "material_palette_primary_color";
    180 
    181     /**
    182      * Intent key to pass the photo palette secondary color calculated by
    183      * {@link com.android.contacts.quickcontact.QuickContactActivity} to the editor and between
    184      * the compact and fully expanded editors.
    185      */
    186     public static final String INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR =
    187             "material_palette_secondary_color";
    188 
    189     /**
    190      * Intent key to pass the ID of the photo to display on the editor.
    191      */
    192     public static final String INTENT_EXTRA_PHOTO_ID = "photo_id";
    193 
    194     /**
    195      * Intent key to pass the ID of the raw contact id that should be displayed in the full editor
    196      * by itself.
    197      */
    198     public static final String INTENT_EXTRA_RAW_CONTACT_ID_TO_DISPLAY_ALONE =
    199             "raw_contact_id_to_display_alone";
    200 
    201     /**
    202      * Intent key to pass the boolean value of if the raw contact id that should be displayed
    203      * in the full editor by itself is read-only.
    204      */
    205     public static final String INTENT_EXTRA_RAW_CONTACT_DISPLAY_ALONE_IS_READ_ONLY =
    206             "raw_contact_display_alone_is_read_only";
    207 
    208     /**
    209      * Intent extra to specify a {@link ContactEditor.SaveMode}.
    210      */
    211     public static final String SAVE_MODE_EXTRA_KEY = "saveMode";
    212 
    213     /**
    214      * Intent extra key for the contact ID to join the current contact to after saving.
    215      */
    216     public static final String JOIN_CONTACT_ID_EXTRA_KEY = "joinContactId";
    217 
    218     /**
    219      * Callbacks for Activities that host contact editors Fragments.
    220      */
    221     public interface Listener {
    222 
    223         /**
    224          * Contact was not found, so somehow close this fragment. This is raised after a contact
    225          * is removed via Menu/Delete
    226          */
    227         void onContactNotFound();
    228 
    229         /**
    230          * Contact was split, so we can close now.
    231          *
    232          * @param newLookupUri The lookup uri of the new contact that should be shown to the user.
    233          *                     The editor tries best to chose the most natural contact here.
    234          */
    235         void onContactSplit(Uri newLookupUri);
    236 
    237         /**
    238          * User has tapped Revert, close the fragment now.
    239          */
    240         void onReverted();
    241 
    242         /**
    243          * Contact was saved and the Fragment can now be closed safely.
    244          */
    245         void onSaveFinished(Intent resultIntent);
    246 
    247         /**
    248          * User switched to editing a different contact (a suggestion from the
    249          * aggregation engine).
    250          */
    251         void onEditOtherContactRequested(Uri contactLookupUri,
    252                 ArrayList<ContentValues> contentValues);
    253 
    254         /**
    255          * Contact is being created for an external account that provides its own
    256          * new contact activity.
    257          */
    258         void onCustomCreateContactActivityRequested(AccountWithDataSet account,
    259                 Bundle intentExtras);
    260 
    261         /**
    262          * The edited raw contact belongs to an external account that provides
    263          * its own edit activity.
    264          *
    265          * @param redirect indicates that the current editor should be closed
    266          *                 before the custom editor is shown.
    267          */
    268         void onCustomEditContactActivityRequested(AccountWithDataSet account, Uri rawContactUri,
    269                 Bundle intentExtras, boolean redirect);
    270 
    271         /**
    272          * User has requested that contact be deleted.
    273          */
    274         void onDeleteRequested(Uri contactUri);
    275     }
    276 
    277     /**
    278      * Adapter for aggregation suggestions displayed in a PopupWindow when
    279      * editor fields change.
    280      */
    281     protected static final class AggregationSuggestionAdapter extends BaseAdapter {
    282         private final LayoutInflater mLayoutInflater;
    283         private final boolean mSetNewContact;
    284         private final AggregationSuggestionView.Listener mListener;
    285         private final List<AggregationSuggestionEngine.Suggestion> mSuggestions;
    286 
    287         public AggregationSuggestionAdapter(Activity activity, boolean setNewContact,
    288                 AggregationSuggestionView.Listener listener, List<Suggestion> suggestions) {
    289             mLayoutInflater = activity.getLayoutInflater();
    290             mSetNewContact = setNewContact;
    291             mListener = listener;
    292             mSuggestions = suggestions;
    293         }
    294 
    295         @Override
    296         public View getView(int position, View convertView, ViewGroup parent) {
    297             final Suggestion suggestion = (Suggestion) getItem(position);
    298             final AggregationSuggestionView suggestionView =
    299                     (AggregationSuggestionView) mLayoutInflater.inflate(
    300                             R.layout.aggregation_suggestions_item, null);
    301             suggestionView.setNewContact(mSetNewContact);
    302             suggestionView.setListener(mListener);
    303             suggestionView.bindSuggestion(suggestion);
    304             return suggestionView;
    305         }
    306 
    307         @Override
    308         public long getItemId(int position) {
    309             return position;
    310         }
    311 
    312         @Override
    313         public Object getItem(int position) {
    314             return mSuggestions.get(position);
    315         }
    316 
    317         @Override
    318         public int getCount() {
    319             return mSuggestions.size();
    320         }
    321     }
    322 
    323     protected Context mContext;
    324     protected Listener mListener;
    325 
    326     //
    327     // Views
    328     //
    329     protected LinearLayout mContent;
    330     protected View mAggregationSuggestionView;
    331     protected ListPopupWindow mAggregationSuggestionPopup;
    332 
    333     //
    334     // Parameters passed in on {@link #load}
    335     //
    336     protected String mAction;
    337     protected Uri mLookupUri;
    338     protected Bundle mIntentExtras;
    339     protected boolean mAutoAddToDefaultGroup;
    340     protected boolean mDisableDeleteMenuOption;
    341     protected boolean mNewLocalProfile;
    342     protected MaterialColorMapUtils.MaterialPalette mMaterialPalette;
    343     protected long mPhotoId = -1;
    344 
    345     //
    346     // Helpers
    347     //
    348     protected ContactEditorUtils mEditorUtils;
    349     protected RawContactDeltaComparator mComparator;
    350     protected ViewIdGenerator mViewIdGenerator;
    351     private AggregationSuggestionEngine mAggregationSuggestionEngine;
    352 
    353     //
    354     // Loaded data
    355     //
    356     // Used to store existing contact data so it can be re-applied during a rebind call,
    357     // i.e. account switch.  Only used in {@link ContactEditorFragment}.
    358     protected ImmutableList<RawContact> mRawContacts;
    359     protected Cursor mGroupMetaData;
    360 
    361     //
    362     // Editor state
    363     //
    364     protected RawContactDeltaList mState;
    365     protected int mStatus;
    366     protected long mRawContactIdToDisplayAlone = -1;
    367     protected boolean mRawContactDisplayAloneIsReadOnly = false;
    368 
    369     // Whether to show the new contact blank form and if it's corresponding delta is ready.
    370     protected boolean mHasNewContact;
    371     protected AccountWithDataSet mAccountWithDataSet;
    372     protected boolean mNewContactDataReady;
    373     protected boolean mNewContactAccountChanged;
    374 
    375     // Whether it's an edit of existing contact and if it's corresponding delta is ready.
    376     protected boolean mIsEdit;
    377     protected boolean mExistingContactDataReady;
    378 
    379     // Whether we are editing the "me" profile
    380     protected boolean mIsUserProfile;
    381 
    382     // Phone specific option menu items
    383     private boolean mSendToVoicemailState;
    384     private boolean mArePhoneOptionsChangable;
    385     private String mCustomRingtone;
    386 
    387     // Whether editor views and options menu items should be enabled
    388     private boolean mEnabled = true;
    389 
    390     // Aggregation PopupWindow
    391     private long mAggregationSuggestionsRawContactId;
    392 
    393     // Join Activity
    394     protected long mContactIdForJoin;
    395 
    396     // Used to pre-populate the editor with a display name when a user edits a read-only contact.
    397     protected String mReadOnlyDisplayName;
    398 
    399     //
    400     // Not saved/restored on rotates
    401     //
    402 
    403     // The name editor view for the new raw contact that was created so that the user can
    404     // edit a read-only contact (to which the new raw contact was joined)
    405     protected StructuredNameEditorView mReadOnlyNameEditorView;
    406 
    407     /**
    408      * The contact data loader listener.
    409      */
    410     protected final LoaderManager.LoaderCallbacks<Contact> mContactLoaderListener =
    411             new LoaderManager.LoaderCallbacks<Contact>() {
    412 
    413                 protected long mLoaderStartTime;
    414 
    415                 @Override
    416                 public Loader<Contact> onCreateLoader(int id, Bundle args) {
    417                     mLoaderStartTime = SystemClock.elapsedRealtime();
    418                     return new ContactLoader(mContext, mLookupUri, true);
    419                 }
    420 
    421                 @Override
    422                 public void onLoadFinished(Loader<Contact> loader, Contact contact) {
    423                     final long loaderCurrentTime = SystemClock.elapsedRealtime();
    424                     Log.v(TAG, "Time needed for loading: " + (loaderCurrentTime-mLoaderStartTime));
    425                     if (!contact.isLoaded()) {
    426                         // Item has been deleted. Close activity without saving again.
    427                         Log.i(TAG, "No contact found. Closing activity");
    428                         mStatus = Status.CLOSING;
    429                         if (mListener != null) mListener.onContactNotFound();
    430                         return;
    431                     }
    432 
    433                     mStatus = Status.EDITING;
    434                     mLookupUri = contact.getLookupUri();
    435                     final long setDataStartTime = SystemClock.elapsedRealtime();
    436                     setState(contact);
    437                     setStateForPhoneMenuItems(contact);
    438                     final long setDataEndTime = SystemClock.elapsedRealtime();
    439 
    440                     Log.v(TAG, "Time needed for setting UI: " + (setDataEndTime - setDataStartTime));
    441                 }
    442 
    443                 @Override
    444                 public void onLoaderReset(Loader<Contact> loader) {
    445                 }
    446             };
    447 
    448     /**
    449      * The groups meta data loader listener.
    450      */
    451     protected final LoaderManager.LoaderCallbacks<Cursor> mGroupsLoaderListener =
    452             new LoaderManager.LoaderCallbacks<Cursor>() {
    453 
    454                 @Override
    455                 public CursorLoader onCreateLoader(int id, Bundle args) {
    456                     return new GroupMetaDataLoader(mContext, ContactsContract.Groups.CONTENT_URI);
    457                 }
    458 
    459                 @Override
    460                 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    461                     mGroupMetaData = data;
    462                     setGroupMetaData();
    463                 }
    464 
    465                 @Override
    466                 public void onLoaderReset(Loader<Cursor> loader) {
    467                 }
    468             };
    469 
    470     @Override
    471     public void onAttach(Activity activity) {
    472         super.onAttach(activity);
    473         mContext = activity;
    474         mEditorUtils = ContactEditorUtils.getInstance(mContext);
    475         mComparator = new RawContactDeltaComparator(mContext);
    476     }
    477 
    478     @Override
    479     public void onCreate(Bundle savedState) {
    480         if (savedState != null) {
    481             // Restore mUri before calling super.onCreate so that onInitializeLoaders
    482             // would already have a uri and an action to work with
    483             mAction = savedState.getString(KEY_ACTION);
    484             mLookupUri = savedState.getParcelable(KEY_URI);
    485         }
    486 
    487         super.onCreate(savedState);
    488 
    489         if (savedState == null) {
    490             mViewIdGenerator = new ViewIdGenerator();
    491         } else {
    492             mViewIdGenerator = savedState.getParcelable(KEY_VIEW_ID_GENERATOR);
    493 
    494             mAutoAddToDefaultGroup = savedState.getBoolean(KEY_AUTO_ADD_TO_DEFAULT_GROUP);
    495             mDisableDeleteMenuOption = savedState.getBoolean(KEY_DISABLE_DELETE_MENU_OPTION);
    496             mNewLocalProfile = savedState.getBoolean(KEY_NEW_LOCAL_PROFILE);
    497             mMaterialPalette = savedState.getParcelable(KEY_MATERIAL_PALETTE);
    498             mPhotoId = savedState.getLong(KEY_PHOTO_ID);
    499 
    500             mRawContacts = ImmutableList.copyOf(savedState.<RawContact>getParcelableArrayList(
    501                     KEY_RAW_CONTACTS));
    502             // NOTE: mGroupMetaData is not saved/restored
    503 
    504             // Read state from savedState. No loading involved here
    505             mState = savedState.<RawContactDeltaList> getParcelable(KEY_EDIT_STATE);
    506             mStatus = savedState.getInt(KEY_STATUS);
    507             mRawContactDisplayAloneIsReadOnly = savedState.getBoolean(
    508                     KEY_RAW_CONTACT_DISPLAY_ALONE_IS_READ_ONLY);
    509 
    510             mHasNewContact = savedState.getBoolean(KEY_HAS_NEW_CONTACT);
    511             mNewContactDataReady = savedState.getBoolean(KEY_NEW_CONTACT_READY);
    512 
    513             mIsEdit = savedState.getBoolean(KEY_IS_EDIT);
    514             mExistingContactDataReady = savedState.getBoolean(KEY_EXISTING_CONTACT_READY);
    515 
    516             mIsUserProfile = savedState.getBoolean(KEY_IS_USER_PROFILE);
    517 
    518             // Phone specific options menus
    519             mSendToVoicemailState = savedState.getBoolean(KEY_SEND_TO_VOICE_MAIL_STATE);
    520             mArePhoneOptionsChangable = savedState.getBoolean(KEY_ARE_PHONE_OPTIONS_CHANGEABLE);
    521             mCustomRingtone = savedState.getString(KEY_CUSTOM_RINGTONE);
    522 
    523             mEnabled = savedState.getBoolean(KEY_ENABLED);
    524 
    525             // Aggregation PopupWindow
    526             mAggregationSuggestionsRawContactId = savedState.getLong(
    527                     KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID);
    528 
    529             // Join Activity
    530             mContactIdForJoin = savedState.getLong(KEY_CONTACT_ID_FOR_JOIN);
    531 
    532             mReadOnlyDisplayName = savedState.getString(KEY_READ_ONLY_DISPLAY_NAME);
    533         }
    534 
    535         // mState can still be null because it may not have have finished loading before
    536         // onSaveInstanceState was called.
    537         if (mState == null) {
    538             mState = new RawContactDeltaList();
    539         }
    540     }
    541 
    542     @Override
    543     public void onActivityCreated(Bundle savedInstanceState) {
    544         super.onActivityCreated(savedInstanceState);
    545 
    546         validateAction(mAction);
    547 
    548         if (mState.isEmpty()) {
    549             // The delta list may not have finished loading before orientation change happens.
    550             // In this case, there will be a saved state but deltas will be missing.  Reload from
    551             // database.
    552             if (Intent.ACTION_EDIT.equals(mAction) ||
    553                     ContactEditorBaseActivity.ACTION_EDIT.equals(mAction)) {
    554                 // Either
    555                 // 1) orientation change but load never finished.
    556                 // 2) not an orientation change so data needs to be loaded for first time.
    557                 getLoaderManager().initLoader(LOADER_CONTACT, null, mContactLoaderListener);
    558                 getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupsLoaderListener);
    559             }
    560         } else {
    561             // Orientation change, we already have mState, it was loaded by onCreate
    562             bindEditors();
    563         }
    564 
    565         // Handle initial actions only when existing state missing
    566         if (savedInstanceState == null) {
    567             final Account account = mIntentExtras == null ? null :
    568                     (Account) mIntentExtras.getParcelable(Intents.Insert.EXTRA_ACCOUNT);
    569             final String dataSet = mIntentExtras == null ? null :
    570                     mIntentExtras.getString(Intents.Insert.EXTRA_DATA_SET);
    571             if (account != null) {
    572                 mAccountWithDataSet = new AccountWithDataSet(account.name, account.type, dataSet);
    573             }
    574 
    575             if (Intent.ACTION_EDIT.equals(mAction) ||
    576                     ContactEditorBaseActivity.ACTION_EDIT.equals(mAction)) {
    577                 mIsEdit = true;
    578             } else if (Intent.ACTION_INSERT.equals(mAction) ||
    579                     ContactEditorBaseActivity.ACTION_INSERT.equals(mAction)) {
    580                 mHasNewContact = true;
    581                 if (mAccountWithDataSet != null) {
    582                     createContact(mAccountWithDataSet);
    583                 } else {
    584                     // No Account specified. Let the user choose
    585                     // Load Accounts async so that we can present them
    586                     selectAccountAndCreateContact();
    587                 }
    588             }
    589         }
    590     }
    591 
    592     /**
    593      * Checks if the requested action is valid.
    594      *
    595      * @param action The action to test.
    596      * @throws IllegalArgumentException when the action is invalid.
    597      */
    598     private static void validateAction(String action) {
    599         if (VALID_INTENT_ACTIONS.contains(action)) {
    600             return;
    601         }
    602         throw new IllegalArgumentException(
    603                 "Unknown action " + action + "; Supported actions: " + VALID_INTENT_ACTIONS);
    604     }
    605 
    606     @Override
    607     public void onSaveInstanceState(Bundle outState) {
    608         outState.putString(KEY_ACTION, mAction);
    609         outState.putParcelable(KEY_URI, mLookupUri);
    610         outState.putBoolean(KEY_AUTO_ADD_TO_DEFAULT_GROUP, mAutoAddToDefaultGroup);
    611         outState.putBoolean(KEY_DISABLE_DELETE_MENU_OPTION, mDisableDeleteMenuOption);
    612         outState.putBoolean(KEY_NEW_LOCAL_PROFILE, mNewLocalProfile);
    613         if (mMaterialPalette != null) {
    614             outState.putParcelable(KEY_MATERIAL_PALETTE, mMaterialPalette);
    615         }
    616         outState.putLong(KEY_PHOTO_ID, mPhotoId);
    617 
    618         outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator);
    619 
    620         outState.putParcelableArrayList(KEY_RAW_CONTACTS, mRawContacts == null ?
    621                 Lists.<RawContact>newArrayList() : Lists.newArrayList(mRawContacts));
    622         // NOTE: mGroupMetaData is not saved
    623 
    624         if (hasValidState()) {
    625             // Store entities with modifications
    626             outState.putParcelable(KEY_EDIT_STATE, mState);
    627         }
    628         outState.putInt(KEY_STATUS, mStatus);
    629         outState.putBoolean(KEY_HAS_NEW_CONTACT, mHasNewContact);
    630         outState.putBoolean(KEY_NEW_CONTACT_READY, mNewContactDataReady);
    631         outState.putBoolean(KEY_IS_EDIT, mIsEdit);
    632         outState.putBoolean(KEY_EXISTING_CONTACT_READY, mExistingContactDataReady);
    633         outState.putBoolean(KEY_RAW_CONTACT_DISPLAY_ALONE_IS_READ_ONLY,
    634                 mRawContactDisplayAloneIsReadOnly);
    635 
    636         outState.putBoolean(KEY_IS_USER_PROFILE, mIsUserProfile);
    637 
    638         // Phone specific options
    639         outState.putBoolean(KEY_SEND_TO_VOICE_MAIL_STATE, mSendToVoicemailState);
    640         outState.putBoolean(KEY_ARE_PHONE_OPTIONS_CHANGEABLE, mArePhoneOptionsChangable);
    641         outState.putString(KEY_CUSTOM_RINGTONE, mCustomRingtone);
    642 
    643         outState.putBoolean(KEY_ENABLED, mEnabled);
    644 
    645         // Aggregation PopupWindow
    646         outState.putLong(KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID,
    647                 mAggregationSuggestionsRawContactId);
    648 
    649         // Join Activity
    650         outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin);
    651 
    652         outState.putString(KEY_READ_ONLY_DISPLAY_NAME, mReadOnlyDisplayName);
    653 
    654         super.onSaveInstanceState(outState);
    655     }
    656 
    657     @Override
    658     public void onStop() {
    659         super.onStop();
    660         UiClosables.closeQuietly(mAggregationSuggestionPopup);
    661     }
    662 
    663     @Override
    664     public void onDestroy() {
    665         super.onDestroy();
    666         if (mAggregationSuggestionEngine != null) {
    667             mAggregationSuggestionEngine.quit();
    668         }
    669     }
    670 
    671     @Override
    672     public void onActivityResult(int requestCode, int resultCode, Intent data) {
    673         switch (requestCode) {
    674             case REQUEST_CODE_JOIN: {
    675                 // Ignore failed requests
    676                 if (resultCode != Activity.RESULT_OK) return;
    677                 if (data != null) {
    678                     final long contactId = ContentUris.parseId(data.getData());
    679                     if (hasPendingChanges()) {
    680                         // Ask the user if they want to save changes before doing the join
    681                         JoinContactConfirmationDialogFragment.show(this, contactId);
    682                     } else {
    683                         // Do the join immediately
    684                         joinAggregate(contactId);
    685                     }
    686                 }
    687                 break;
    688             }
    689             case REQUEST_CODE_ACCOUNTS_CHANGED: {
    690                 // Bail if the account selector was not successful.
    691                 if (resultCode != Activity.RESULT_OK) {
    692                     if (mListener != null) {
    693                         mListener.onReverted();
    694                     }
    695                     return;
    696                 }
    697                 // If there's an account specified, use it.
    698                 if (data != null) {
    699                     AccountWithDataSet account = data.getParcelableExtra(
    700                             Intents.Insert.EXTRA_ACCOUNT);
    701                     if (account != null) {
    702                         createContact(account);
    703                         return;
    704                     }
    705                 }
    706                 // If there isn't an account specified, then this is likely a phone-local
    707                 // contact, so we should continue setting up the editor by automatically selecting
    708                 // the most appropriate account.
    709                 createContact();
    710                 break;
    711             }
    712             case REQUEST_CODE_PICK_RINGTONE: {
    713                 if (data != null) {
    714                     final Uri pickedUri = data.getParcelableExtra(
    715                             RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
    716                     onRingtonePicked(pickedUri);
    717                 }
    718                 break;
    719             }
    720         }
    721     }
    722 
    723     private void onRingtonePicked(Uri pickedUri) {
    724         mCustomRingtone = EditorUiUtils.getRingtoneStringFromUri(pickedUri, CURRENT_API_VERSION);
    725         Intent intent = ContactSaveService.createSetRingtone(
    726                 mContext, mLookupUri, mCustomRingtone);
    727         mContext.startService(intent);
    728     }
    729 
    730     //
    731     // Options menu
    732     //
    733 
    734     private void setStateForPhoneMenuItems(Contact contact) {
    735         if (contact != null) {
    736             mSendToVoicemailState = contact.isSendToVoicemail();
    737             mCustomRingtone = contact.getCustomRingtone();
    738             mArePhoneOptionsChangable = !contact.isDirectoryEntry()
    739                     && PhoneCapabilityTester.isPhone(mContext);
    740         }
    741     }
    742 
    743     /**
    744      * Invalidates the options menu if we are still associated with an Activity.
    745      */
    746     protected void invalidateOptionsMenu() {
    747         final Activity activity = getActivity();
    748         if (activity != null) {
    749             activity.invalidateOptionsMenu();
    750         }
    751     }
    752 
    753     @Override
    754     public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
    755         inflater.inflate(R.menu.edit_contact, menu);
    756     }
    757 
    758     @Override
    759     public void onPrepareOptionsMenu(Menu menu) {
    760         // This supports the keyboard shortcut to save changes to a contact but shouldn't be visible
    761         // because the custom action bar contains the "save" button now (not the overflow menu).
    762         // TODO: Find a better way to handle shortcuts, i.e. onKeyDown()?
    763         final MenuItem saveMenu = menu.findItem(R.id.menu_save);
    764         final MenuItem splitMenu = menu.findItem(R.id.menu_split);
    765         final MenuItem joinMenu = menu.findItem(R.id.menu_join);
    766         final MenuItem helpMenu = menu.findItem(R.id.menu_help);
    767         final MenuItem sendToVoiceMailMenu = menu.findItem(R.id.menu_send_to_voicemail);
    768         final MenuItem ringToneMenu = menu.findItem(R.id.menu_set_ringtone);
    769         final MenuItem deleteMenu = menu.findItem(R.id.menu_delete);
    770 
    771         // Set visibility of menus
    772 
    773         // help menu depending on whether this is inserting or editing
    774         if (isInsert(mAction) || mRawContactIdToDisplayAlone != -1) {
    775             HelpUtils.prepareHelpMenuItem(mContext, helpMenu, R.string.help_url_people_add);
    776             splitMenu.setVisible(false);
    777             joinMenu.setVisible(false);
    778             deleteMenu.setVisible(false);
    779         } else if (isEdit(mAction)) {
    780             HelpUtils.prepareHelpMenuItem(mContext, helpMenu, R.string.help_url_people_edit);
    781             splitMenu.setVisible(canUnlinkRawContacts());
    782             // Cannot join a user profile
    783             joinMenu.setVisible(!isEditingUserProfile());
    784             deleteMenu.setVisible(!mDisableDeleteMenuOption && !isEditingUserProfile());
    785         } else {
    786             // something else, so don't show the help menu
    787             helpMenu.setVisible(false);
    788         }
    789 
    790         // Save menu is invisible when there's only one read only contact in the editor.
    791         saveMenu.setVisible(!mRawContactDisplayAloneIsReadOnly);
    792 
    793         if (mRawContactIdToDisplayAlone != -1 || mIsUserProfile) {
    794             sendToVoiceMailMenu.setVisible(false);
    795             ringToneMenu.setVisible(false);
    796         } else {
    797             // Hide telephony-related settings (ringtone, send to voicemail)
    798             // if we don't have a telephone or are editing a new contact.
    799             sendToVoiceMailMenu.setChecked(mSendToVoicemailState);
    800             sendToVoiceMailMenu.setVisible(mArePhoneOptionsChangable);
    801             ringToneMenu.setVisible(mArePhoneOptionsChangable);
    802         }
    803 
    804         int size = menu.size();
    805         for (int i = 0; i < size; i++) {
    806             menu.getItem(i).setEnabled(mEnabled);
    807         }
    808     }
    809 
    810     @Override
    811     public boolean onOptionsItemSelected(MenuItem item) {
    812         final Activity activity = getActivity();
    813         if (activity == null || activity.isFinishing() || activity.isDestroyed()) {
    814             // If we no longer are attached to a running activity want to
    815             // drain this event.
    816             return true;
    817         }
    818 
    819         switch (item.getItemId()) {
    820             case R.id.menu_save:
    821                 return save(SaveMode.CLOSE);
    822             case R.id.menu_delete:
    823                 if (mListener != null) mListener.onDeleteRequested(mLookupUri);
    824                 return true;
    825             case R.id.menu_split:
    826                 return doSplitContactAction();
    827             case R.id.menu_join:
    828                 return doJoinContactAction();
    829             case R.id.menu_set_ringtone:
    830                 doPickRingtone();
    831                 return true;
    832             case R.id.menu_send_to_voicemail:
    833                 // Update state and save
    834                 mSendToVoicemailState = !mSendToVoicemailState;
    835                 item.setChecked(mSendToVoicemailState);
    836                 final Intent intent = ContactSaveService.createSetSendToVoicemail(
    837                         mContext, mLookupUri, mSendToVoicemailState);
    838                 mContext.startService(intent);
    839                 return true;
    840         }
    841 
    842         return false;
    843     }
    844 
    845     @Override
    846     public boolean revert() {
    847         if (mState.isEmpty() || !hasPendingChanges()) {
    848             onCancelEditConfirmed();
    849         } else {
    850             CancelEditDialogFragment.show(this);
    851         }
    852         return true;
    853     }
    854 
    855     @Override
    856     public void onCancelEditConfirmed() {
    857         // When this Fragment is closed we don't want it to auto-save
    858         mStatus = Status.CLOSING;
    859         if (mListener != null) {
    860             mListener.onReverted();
    861         }
    862     }
    863 
    864     @Override
    865     public void onSplitContactConfirmed(boolean hasPendingChanges) {
    866         if (mState.isEmpty()) {
    867             // This may happen when this Fragment is recreated by the system during users
    868             // confirming the split action (and thus this method is called just before onCreate()),
    869             // for example.
    870             Log.e(TAG, "mState became null during the user's confirming split action. " +
    871                     "Cannot perform the save action.");
    872             return;
    873         }
    874 
    875         if (!hasPendingChanges && mHasNewContact) {
    876             // If the user didn't add anything new, we don't want to split out the newly created
    877             // raw contact into a name-only contact so remove them.
    878             final Iterator<RawContactDelta> iterator = mState.iterator();
    879             while (iterator.hasNext()) {
    880                 final RawContactDelta rawContactDelta = iterator.next();
    881                 if (rawContactDelta.getRawContactId() < 0) {
    882                     iterator.remove();
    883                 }
    884             }
    885         }
    886         mState.markRawContactsForSplitting();
    887         save(SaveMode.SPLIT);
    888     }
    889 
    890     private boolean doSplitContactAction() {
    891         if (!hasValidState()) return false;
    892 
    893         SplitContactConfirmationDialogFragment.show(this, hasPendingChanges());
    894         return true;
    895     }
    896 
    897     private boolean doJoinContactAction() {
    898         if (!hasValidState() || mLookupUri == null) {
    899             return false;
    900         }
    901 
    902         // If we just started creating a new contact and haven't added any data, it's too
    903         // early to do a join
    904         if (mState.size() == 1 && mState.get(0).isContactInsert()
    905                 && !hasPendingChanges()) {
    906             Toast.makeText(mContext, R.string.toast_join_with_empty_contact,
    907                     Toast.LENGTH_LONG).show();
    908             return true;
    909         }
    910 
    911         showJoinAggregateActivity(mLookupUri);
    912         return true;
    913     }
    914 
    915     @Override
    916     public void onJoinContactConfirmed(long joinContactId) {
    917         doSaveAction(SaveMode.JOIN, joinContactId);
    918     }
    919 
    920     private void doPickRingtone() {
    921         final Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
    922         // Allow user to pick 'Default'
    923         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
    924         // Show only ringtones
    925         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_RINGTONE);
    926         // Allow the user to pick a silent ringtone
    927         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
    928 
    929         final Uri ringtoneUri = EditorUiUtils.getRingtoneUriFromString(mCustomRingtone,
    930                 CURRENT_API_VERSION);
    931 
    932         // Put checkmark next to the current ringtone for this contact
    933         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, ringtoneUri);
    934 
    935         // Launch!
    936         try {
    937             startActivityForResult(intent, REQUEST_CODE_PICK_RINGTONE);
    938         } catch (ActivityNotFoundException ex) {
    939             Toast.makeText(mContext, R.string.missing_app, Toast.LENGTH_SHORT).show();
    940         }
    941     }
    942 
    943     @Override
    944     public boolean save(int saveMode) {
    945         if (!hasValidState() || mStatus != Status.EDITING) {
    946             return false;
    947         }
    948 
    949         // If we are about to close the editor - there is no need to refresh the data
    950         if (saveMode == SaveMode.CLOSE || saveMode == SaveMode.COMPACT
    951                 || saveMode == SaveMode.SPLIT) {
    952             getLoaderManager().destroyLoader(LOADER_CONTACT);
    953         }
    954 
    955         mStatus = Status.SAVING;
    956 
    957         if (!hasPendingChanges()) {
    958             if (mLookupUri == null && saveMode == SaveMode.RELOAD) {
    959                 // We don't have anything to save and there isn't even an existing contact yet.
    960                 // Nothing to do, simply go back to editing mode
    961                 mStatus = Status.EDITING;
    962                 return true;
    963             }
    964             onSaveCompleted(/* hadChanges =*/ false, saveMode,
    965                     /* saveSucceeded =*/ mLookupUri != null, mLookupUri, /* joinContactId =*/ null);
    966             return true;
    967         }
    968 
    969         setEnabled(false);
    970 
    971         return doSaveAction(saveMode, /* joinContactId */ null);
    972     }
    973 
    974     /**
    975      * Persist the accumulated editor deltas.
    976      *
    977      * @param joinContactId the raw contact ID to join the contact being saved to after the save,
    978      *         may be null.
    979      */
    980     abstract protected boolean doSaveAction(int saveMode, Long joinContactId);
    981 
    982     protected boolean startSaveService(Context context, Intent intent, int saveMode) {
    983         final boolean result = ContactSaveService.startService(
    984                 context, intent, saveMode);
    985         if (!result) {
    986             onCancelEditConfirmed();
    987         }
    988         return result;
    989     }
    990 
    991     //
    992     // State accessor methods
    993     //
    994 
    995     /**
    996      * Check if our internal {@link #mState} is valid, usually checked before
    997      * performing user actions.
    998      */
    999     protected boolean hasValidState() {
   1000         return mState.size() > 0;
   1001     }
   1002 
   1003     protected boolean isEditingUserProfile() {
   1004         return mNewLocalProfile || mIsUserProfile;
   1005     }
   1006 
   1007     /**
   1008      * Whether the contact being edited spans multiple raw contacts.
   1009      * The may also span multiple accounts.
   1010      */
   1011     public boolean isEditingMultipleRawContacts() {
   1012         return mState.size() > 1;
   1013     }
   1014 
   1015     /**
   1016      * Whether the contact being edited is composed of a single read-only raw contact
   1017      * aggregated with a newly created writable raw contact.
   1018      */
   1019     protected boolean isEditingReadOnlyRawContactWithNewContact() {
   1020         return mHasNewContact && mState.size() == 2;
   1021     }
   1022 
   1023     /**
   1024      * Return true if there are any edits to the current contact which need to
   1025      * be saved.
   1026      */
   1027     protected boolean hasPendingRawContactChanges(Set<String> excludedMimeTypes) {
   1028         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
   1029         return RawContactModifier.hasChanges(mState, accountTypes, excludedMimeTypes);
   1030     }
   1031 
   1032     /**
   1033      * We allow unlinking only if there is more than one raw contact, it is not a user-profile,
   1034      * and unlinking won't result in an empty contact.  For the empty contact case, we only guard
   1035      * against this when there is a single read-only contact in the aggregate.  If the user
   1036      * has joined >1 read-only contacts together, we allow them to unlink it, even if they have
   1037      * never added their own information and unlinking will create a name only contact.
   1038      */
   1039     protected boolean canUnlinkRawContacts() {
   1040         return isEditingMultipleRawContacts()
   1041                 && !isEditingUserProfile()
   1042                 && !isEditingReadOnlyRawContactWithNewContact();
   1043     }
   1044 
   1045     /**
   1046      * Determines if changes were made in the editor that need to be saved, while taking into
   1047      * account that name changes are not real for read-only contacts.
   1048      * See go/editing-read-only-contacts
   1049      */
   1050     protected boolean hasPendingChanges() {
   1051         if (mReadOnlyNameEditorView != null && mReadOnlyDisplayName != null) {
   1052             // We created a new raw contact delta with a default display name.
   1053             // We must test for pending changes while ignoring the default display name.
   1054             final String displayName = mReadOnlyNameEditorView.getDisplayName();
   1055             if (mReadOnlyDisplayName.equals(displayName)) {
   1056                 final Set<String> excludedMimeTypes = new HashSet<>();
   1057                 excludedMimeTypes.add(StructuredName.CONTENT_ITEM_TYPE);
   1058                 return hasPendingRawContactChanges(excludedMimeTypes);
   1059             }
   1060             return true;
   1061         }
   1062         return hasPendingRawContactChanges(/* excludedMimeTypes =*/ null);
   1063     }
   1064 
   1065     /**
   1066      * Whether editor inputs and the options menu should be enabled.
   1067      */
   1068     protected boolean isEnabled() {
   1069         return mEnabled;
   1070     }
   1071 
   1072     /**
   1073      * Returns the palette extra that was passed in.
   1074      */
   1075     protected MaterialColorMapUtils.MaterialPalette getMaterialPalette() {
   1076         return mMaterialPalette;
   1077     }
   1078 
   1079     //
   1080     // Account creation
   1081     //
   1082 
   1083     private void selectAccountAndCreateContact() {
   1084         // If this is a local profile, then skip the logic about showing the accounts changed
   1085         // activity and create a phone-local contact.
   1086         if (mNewLocalProfile) {
   1087             createContact(null);
   1088             return;
   1089         }
   1090 
   1091         // If there is no default account or the accounts have changed such that we need to
   1092         // prompt the user again, then launch the account prompt.
   1093         if (mEditorUtils.shouldShowAccountChangedNotification()) {
   1094             Intent intent = new Intent(mContext, ContactEditorAccountsChangedActivity.class);
   1095             // Prevent a second instance from being started on rotates
   1096             intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
   1097             mStatus = Status.SUB_ACTIVITY;
   1098             startActivityForResult(intent, REQUEST_CODE_ACCOUNTS_CHANGED);
   1099         } else {
   1100             // Otherwise, there should be a default account. Then either create a local contact
   1101             // (if default account is null) or create a contact with the specified account.
   1102             AccountWithDataSet defaultAccount = mEditorUtils.getDefaultAccount();
   1103             createContact(defaultAccount);
   1104         }
   1105     }
   1106 
   1107     /**
   1108      * Create a contact by automatically selecting the first account. If there's no available
   1109      * account, a device-local contact should be created.
   1110      */
   1111     protected void createContact() {
   1112         final List<AccountWithDataSet> accounts =
   1113                 AccountTypeManager.getInstance(mContext).getAccounts(true);
   1114         // No Accounts available. Create a phone-local contact.
   1115         if (accounts.isEmpty()) {
   1116             createContact(null);
   1117             return;
   1118         }
   1119 
   1120         // We have an account switcher in "create-account" screen, so don't need to ask a user to
   1121         // select an account here.
   1122         createContact(accounts.get(0));
   1123     }
   1124 
   1125     /**
   1126      * Shows account creation screen associated with a given account.
   1127      *
   1128      * @param account may be null to signal a device-local contact should be created.
   1129      */
   1130     protected void createContact(AccountWithDataSet account) {
   1131         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
   1132         final AccountType accountType = accountTypes.getAccountTypeForAccount(account);
   1133 
   1134         if (accountType.getCreateContactActivityClassName() != null) {
   1135             if (mListener != null) {
   1136                 mListener.onCustomCreateContactActivityRequested(account, mIntentExtras);
   1137             }
   1138         } else {
   1139             setStateForNewContact(account, accountType, isEditingUserProfile());
   1140         }
   1141     }
   1142 
   1143     //
   1144     // Data binding
   1145     //
   1146 
   1147     private void setState(Contact contact) {
   1148         // If we have already loaded data, we do not want to change it here to not confuse the user
   1149         if (!mState.isEmpty()) {
   1150             Log.v(TAG, "Ignoring background change. This will have to be rebased later");
   1151             return;
   1152         }
   1153 
   1154         // See if this edit operation needs to be redirected to a custom editor
   1155         mRawContacts = contact.getRawContacts();
   1156         if (mRawContacts.size() == 1) {
   1157             RawContact rawContact = mRawContacts.get(0);
   1158             String type = rawContact.getAccountTypeString();
   1159             String dataSet = rawContact.getDataSet();
   1160             AccountType accountType = rawContact.getAccountType(mContext);
   1161             if (accountType.getEditContactActivityClassName() != null &&
   1162                     !accountType.areContactsWritable()) {
   1163                 if (mListener != null) {
   1164                     String name = rawContact.getAccountName();
   1165                     long rawContactId = rawContact.getId();
   1166                     mListener.onCustomEditContactActivityRequested(
   1167                             new AccountWithDataSet(name, type, dataSet),
   1168                             ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
   1169                             mIntentExtras, true);
   1170                 }
   1171                 return;
   1172             }
   1173         }
   1174 
   1175         String readOnlyDisplayName = null;
   1176         // Check for writable raw contacts.  If there are none, then we need to create one so user
   1177         // can edit.  For the user profile case, there is already an editable contact.
   1178         if (!contact.isUserProfile() && !contact.isWritableContact(mContext)) {
   1179             mHasNewContact = true;
   1180 
   1181             // This is potentially an asynchronous call and will add deltas to list.
   1182             selectAccountAndCreateContact();
   1183 
   1184             readOnlyDisplayName = contact.getDisplayName();
   1185         } else {
   1186             mHasNewContact = false;
   1187         }
   1188 
   1189         // This also adds deltas to list.  If readOnlyDisplayName is null at this point it is
   1190         // simply ignored later on by the editor.
   1191         setStateForExistingContact(readOnlyDisplayName, contact.isUserProfile(), mRawContacts);
   1192     }
   1193 
   1194     /**
   1195      * Prepare {@link #mState} for a newly created phone-local contact.
   1196      */
   1197     private void setStateForNewContact(AccountWithDataSet account, AccountType accountType,
   1198             boolean isUserProfile) {
   1199         setStateForNewContact(account, accountType, /* oldState =*/ null,
   1200                 /* oldAccountType =*/ null, isUserProfile);
   1201     }
   1202 
   1203     /**
   1204      * Prepare {@link #mState} for a newly created phone-local contact, migrating the state
   1205      * specified by oldState and oldAccountType.
   1206      */
   1207     protected void setStateForNewContact(AccountWithDataSet account, AccountType accountType,
   1208             RawContactDelta oldState, AccountType oldAccountType, boolean isUserProfile) {
   1209         mStatus = Status.EDITING;
   1210         mState.add(createNewRawContactDelta(account, accountType, oldState, oldAccountType));
   1211         mIsUserProfile = isUserProfile;
   1212         mNewContactDataReady = true;
   1213         bindEditors();
   1214     }
   1215 
   1216     /**
   1217      * Returns a {@link RawContactDelta} for a new contact suitable for addition into
   1218      * {@link #mState}.
   1219      *
   1220      * If oldState and oldAccountType are specified, the state specified by those parameters
   1221      * is migrated to the result {@link RawContactDelta}.
   1222      */
   1223     private RawContactDelta createNewRawContactDelta(AccountWithDataSet account,
   1224             AccountType accountType, RawContactDelta oldState, AccountType oldAccountType) {
   1225         final RawContact rawContact = new RawContact();
   1226         if (account != null) {
   1227             rawContact.setAccount(account);
   1228         } else {
   1229             rawContact.setAccountToLocal();
   1230         }
   1231 
   1232         final RawContactDelta result = new RawContactDelta(
   1233                 ValuesDelta.fromAfter(rawContact.getValues()));
   1234         if (oldState == null) {
   1235             // Parse any values from incoming intent
   1236             RawContactModifier.parseExtras(mContext, accountType, result, mIntentExtras);
   1237         } else {
   1238             RawContactModifier.migrateStateForNewContact(
   1239                     mContext, oldState, result, oldAccountType, accountType);
   1240         }
   1241 
   1242         // Ensure we have some default fields (if the account type does not support a field,
   1243         // ensureKind will not add it, so it is safe to add e.g. Event)
   1244         RawContactModifier.ensureKindExists(result, accountType, Phone.CONTENT_ITEM_TYPE);
   1245         RawContactModifier.ensureKindExists(result, accountType, Email.CONTENT_ITEM_TYPE);
   1246         RawContactModifier.ensureKindExists(result, accountType, Organization.CONTENT_ITEM_TYPE);
   1247         RawContactModifier.ensureKindExists(result, accountType, Event.CONTENT_ITEM_TYPE);
   1248         RawContactModifier.ensureKindExists(result, accountType,
   1249                 StructuredPostal.CONTENT_ITEM_TYPE);
   1250 
   1251         // Set the correct URI for saving the contact as a profile
   1252         if (mNewLocalProfile) {
   1253             result.setProfileQueryUri();
   1254         }
   1255 
   1256         return result;
   1257     }
   1258 
   1259     /**
   1260      * Prepare {@link #mState} for an existing contact.
   1261      */
   1262     protected void setStateForExistingContact(String readOnlyDisplayName, boolean isUserProfile,
   1263             ImmutableList<RawContact> rawContacts) {
   1264         setEnabled(true);
   1265         mReadOnlyDisplayName = readOnlyDisplayName;
   1266 
   1267         mState.addAll(rawContacts.iterator());
   1268         setIntentExtras(mIntentExtras);
   1269         mIntentExtras = null;
   1270 
   1271         // For user profile, change the contacts query URI
   1272         mIsUserProfile = isUserProfile;
   1273         boolean localProfileExists = false;
   1274 
   1275         if (mIsUserProfile) {
   1276             for (RawContactDelta rawContactDelta : mState) {
   1277                 // For profile contacts, we need a different query URI
   1278                 rawContactDelta.setProfileQueryUri();
   1279                 // Try to find a local profile contact
   1280                 if (rawContactDelta.getValues().getAsString(RawContacts.ACCOUNT_TYPE) == null) {
   1281                     localProfileExists = true;
   1282                 }
   1283             }
   1284             // Editor should always present a local profile for editing
   1285             if (!localProfileExists) {
   1286                 mState.add(createLocalRawContactDelta());
   1287             }
   1288         }
   1289         mExistingContactDataReady = true;
   1290         bindEditors();
   1291     }
   1292 
   1293     /**
   1294      * Returns a {@link RawContactDelta} for a local contact suitable for addition into
   1295      * {@link #mState}.
   1296      */
   1297     private static RawContactDelta createLocalRawContactDelta() {
   1298         final RawContact rawContact = new RawContact();
   1299         rawContact.setAccountToLocal();
   1300 
   1301         final RawContactDelta result = new RawContactDelta(
   1302                 ValuesDelta.fromAfter(rawContact.getValues()));
   1303         result.setProfileQueryUri();
   1304 
   1305         return result;
   1306     }
   1307 
   1308     /**
   1309      * Sets group metadata on all bound editors.
   1310      */
   1311     abstract protected void setGroupMetaData();
   1312 
   1313     /**
   1314      * Bind editors using {@link #mState} and other members initialized from the loaded (or new)
   1315      * Contact.
   1316      */
   1317     abstract protected void bindEditors();
   1318 
   1319     /**
   1320      * Set the enabled state of editors.
   1321      */
   1322     private void setEnabled(boolean enabled) {
   1323         if (mEnabled != enabled) {
   1324             mEnabled = enabled;
   1325 
   1326             // Enable/disable editors
   1327             if (mContent != null) {
   1328                 int count = mContent.getChildCount();
   1329                 for (int i = 0; i < count; i++) {
   1330                     mContent.getChildAt(i).setEnabled(enabled);
   1331                 }
   1332             }
   1333 
   1334             // Enable/disable aggregation suggestion vies
   1335             if (mAggregationSuggestionView != null) {
   1336                 LinearLayout itemList = (LinearLayout) mAggregationSuggestionView.findViewById(
   1337                         R.id.aggregation_suggestions);
   1338                 int count = itemList.getChildCount();
   1339                 for (int i = 0; i < count; i++) {
   1340                     itemList.getChildAt(i).setEnabled(enabled);
   1341                 }
   1342             }
   1343 
   1344             // Maybe invalidate the options menu
   1345             final Activity activity = getActivity();
   1346             if (activity != null) activity.invalidateOptionsMenu();
   1347         }
   1348     }
   1349 
   1350     /**
   1351      * Removes a current editor ({@link #mState}) and rebinds new editor for a new account.
   1352      * Some of old data are reused with new restriction enforced by the new account.
   1353      *
   1354      * @param oldState Old data being edited.
   1355      * @param oldAccount Old account associated with oldState.
   1356      * @param newAccount New account to be used.
   1357      */
   1358     protected void rebindEditorsForNewContact(
   1359             RawContactDelta oldState, AccountWithDataSet oldAccount,
   1360             AccountWithDataSet newAccount) {
   1361         AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
   1362         AccountType oldAccountType = accountTypes.getAccountTypeForAccount(oldAccount);
   1363         AccountType newAccountType = accountTypes.getAccountTypeForAccount(newAccount);
   1364 
   1365         if (newAccountType.getCreateContactActivityClassName() != null) {
   1366             Log.w(TAG, "external activity called in rebind situation");
   1367             if (mListener != null) {
   1368                 mListener.onCustomCreateContactActivityRequested(newAccount, mIntentExtras);
   1369             }
   1370         } else {
   1371             mExistingContactDataReady = false;
   1372             mNewContactDataReady = false;
   1373             mState = new RawContactDeltaList();
   1374             setStateForNewContact(newAccount, newAccountType, oldState, oldAccountType,
   1375                     isEditingUserProfile());
   1376             if (mIsEdit) {
   1377                 setStateForExistingContact(mReadOnlyDisplayName, isEditingUserProfile(),
   1378                         mRawContacts);
   1379             }
   1380         }
   1381     }
   1382 
   1383     //
   1384     // ContactEditor
   1385     //
   1386 
   1387     @Override
   1388     public void setListener(Listener listener) {
   1389         mListener = listener;
   1390     }
   1391 
   1392     @Override
   1393     public void load(String action, Uri lookupUri, Bundle intentExtras) {
   1394         mAction = action;
   1395         mLookupUri = lookupUri;
   1396         mIntentExtras = intentExtras;
   1397 
   1398         if (mIntentExtras != null) {
   1399             mAutoAddToDefaultGroup =
   1400                     mIntentExtras.containsKey(INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY);
   1401             mNewLocalProfile =
   1402                     mIntentExtras.getBoolean(INTENT_EXTRA_NEW_LOCAL_PROFILE);
   1403             mDisableDeleteMenuOption =
   1404                     mIntentExtras.getBoolean(INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION);
   1405             if (mIntentExtras.containsKey(INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR)
   1406                     && mIntentExtras.containsKey(INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR)) {
   1407                 mMaterialPalette = new MaterialColorMapUtils.MaterialPalette(
   1408                         mIntentExtras.getInt(INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR),
   1409                         mIntentExtras.getInt(INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR));
   1410             }
   1411             // If the user selected a different photo, don't restore the one from the Intent
   1412             if (mPhotoId < 0) {
   1413                 mPhotoId = mIntentExtras.getLong(INTENT_EXTRA_PHOTO_ID);
   1414             }
   1415             mRawContactIdToDisplayAlone = mIntentExtras.getLong(
   1416                     INTENT_EXTRA_RAW_CONTACT_ID_TO_DISPLAY_ALONE, -1);
   1417             mRawContactDisplayAloneIsReadOnly = mIntentExtras.getBoolean(
   1418                     INTENT_EXTRA_RAW_CONTACT_DISPLAY_ALONE_IS_READ_ONLY);
   1419         }
   1420     }
   1421 
   1422     @Override
   1423     public void setIntentExtras(Bundle extras) {
   1424         if (extras == null || extras.size() == 0) {
   1425             return;
   1426         }
   1427 
   1428         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
   1429         for (RawContactDelta state : mState) {
   1430             final AccountType type = state.getAccountType(accountTypes);
   1431             if (type.areContactsWritable()) {
   1432                 // Apply extras to the first writable raw contact only
   1433                 RawContactModifier.parseExtras(mContext, type, state, extras);
   1434                 break;
   1435             }
   1436         }
   1437     }
   1438 
   1439     @Override
   1440     public void onJoinCompleted(Uri uri) {
   1441         onSaveCompleted(false, SaveMode.RELOAD, uri != null, uri, /* joinContactId */ null);
   1442     }
   1443 
   1444     @Override
   1445     public void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded,
   1446             Uri contactLookupUri, Long joinContactId) {
   1447         if (hadChanges) {
   1448             if (saveSucceeded) {
   1449                 switch (saveMode) {
   1450                     case SaveMode.JOIN:
   1451                         break;
   1452                     case SaveMode.SPLIT:
   1453                         Toast.makeText(mContext, R.string.contactUnlinkedToast, Toast.LENGTH_SHORT)
   1454                                 .show();
   1455                         break;
   1456                     default:
   1457                         Toast.makeText(mContext, R.string.contactSavedToast, Toast.LENGTH_SHORT)
   1458                                 .show();
   1459                 }
   1460 
   1461             } else {
   1462                 Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
   1463             }
   1464         }
   1465         switch (saveMode) {
   1466             case SaveMode.CLOSE: {
   1467                 final Intent resultIntent;
   1468                 if (saveSucceeded && contactLookupUri != null) {
   1469                     final Uri lookupUri = maybeConvertToLegacyLookupUri(
   1470                             mContext, contactLookupUri, mLookupUri);
   1471                     resultIntent = ImplicitIntentsUtil.composeQuickContactIntent(lookupUri,
   1472                             QuickContactActivity.MODE_FULLY_EXPANDED);
   1473                     resultIntent.putExtra(QuickContactActivity.EXTRA_PREVIOUS_SCREEN_TYPE,
   1474                             ScreenType.EDITOR);
   1475                 } else {
   1476                     resultIntent = null;
   1477                 }
   1478                 // It is already saved, so prevent it from being saved again
   1479                 mStatus = Status.CLOSING;
   1480                 if (mListener != null) mListener.onSaveFinished(resultIntent);
   1481                 break;
   1482             }
   1483             case SaveMode.COMPACT: {
   1484                 // It is already saved, so prevent it from being saved again
   1485                 mStatus = Status.CLOSING;
   1486                 if (mListener != null) mListener.onSaveFinished(/* resultIntent= */ null);
   1487                 break;
   1488             }
   1489             case SaveMode.JOIN:
   1490                 if (saveSucceeded && contactLookupUri != null && joinContactId != null) {
   1491                     joinAggregate(joinContactId);
   1492                 }
   1493                 break;
   1494             case SaveMode.RELOAD:
   1495                 if (saveSucceeded && contactLookupUri != null) {
   1496                     // If this was in INSERT, we are changing into an EDIT now.
   1497                     // If it already was an EDIT, we are changing to the new Uri now
   1498                     mState = new RawContactDeltaList();
   1499                     load(Intent.ACTION_EDIT, contactLookupUri, null);
   1500                     mStatus = Status.LOADING;
   1501                     getLoaderManager().restartLoader(LOADER_CONTACT, null, mContactLoaderListener);
   1502                 }
   1503                 break;
   1504 
   1505             case SaveMode.SPLIT:
   1506                 mStatus = Status.CLOSING;
   1507                 if (mListener != null) {
   1508                     mListener.onContactSplit(contactLookupUri);
   1509                 } else {
   1510                     Log.d(TAG, "No listener registered, can not call onSplitFinished");
   1511                 }
   1512                 break;
   1513         }
   1514     }
   1515 
   1516     /**
   1517      * Shows a list of aggregates that can be joined into the currently viewed aggregate.
   1518      *
   1519      * @param contactLookupUri the fresh URI for the currently edited contact (after saving it)
   1520      */
   1521     private void showJoinAggregateActivity(Uri contactLookupUri) {
   1522         if (contactLookupUri == null || !isAdded()) {
   1523             return;
   1524         }
   1525 
   1526         mContactIdForJoin = ContentUris.parseId(contactLookupUri);
   1527         final Intent intent = new Intent(UiIntentActions.PICK_JOIN_CONTACT_ACTION);
   1528         intent.putExtra(UiIntentActions.TARGET_CONTACT_ID_EXTRA_KEY, mContactIdForJoin);
   1529         startActivityForResult(intent, REQUEST_CODE_JOIN);
   1530     }
   1531 
   1532     //
   1533     // Aggregation PopupWindow
   1534     //
   1535 
   1536     /**
   1537      * Triggers an asynchronous search for aggregation suggestions.
   1538      */
   1539     protected void acquireAggregationSuggestions(Context context,
   1540             long rawContactId, ValuesDelta valuesDelta) {
   1541         if (mAggregationSuggestionsRawContactId != rawContactId
   1542                 && mAggregationSuggestionView != null) {
   1543             mAggregationSuggestionView.setVisibility(View.GONE);
   1544             mAggregationSuggestionView = null;
   1545             mAggregationSuggestionEngine.reset();
   1546         }
   1547 
   1548         mAggregationSuggestionsRawContactId = rawContactId;
   1549 
   1550         if (mAggregationSuggestionEngine == null) {
   1551             mAggregationSuggestionEngine = new AggregationSuggestionEngine(context);
   1552             mAggregationSuggestionEngine.setListener(this);
   1553             mAggregationSuggestionEngine.start();
   1554         }
   1555 
   1556         mAggregationSuggestionEngine.setContactId(getContactId());
   1557 
   1558         mAggregationSuggestionEngine.onNameChange(valuesDelta);
   1559     }
   1560 
   1561     /**
   1562      * Returns the contact ID for the currently edited contact or 0 if the contact is new.
   1563      */
   1564     private long getContactId() {
   1565         for (RawContactDelta rawContact : mState) {
   1566             Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID);
   1567             if (contactId != null) {
   1568                 return contactId;
   1569             }
   1570         }
   1571         return 0;
   1572     }
   1573 
   1574     @Override
   1575     public void onAggregationSuggestionChange() {
   1576         final Activity activity = getActivity();
   1577         if ((activity != null && activity.isFinishing())
   1578                 || !isVisible() ||  mState.isEmpty() || mStatus != Status.EDITING) {
   1579             return;
   1580         }
   1581 
   1582         UiClosables.closeQuietly(mAggregationSuggestionPopup);
   1583 
   1584         if (mAggregationSuggestionEngine.getSuggestedContactCount() == 0) {
   1585             return;
   1586         }
   1587 
   1588         final View anchorView = getAggregationAnchorView(mAggregationSuggestionsRawContactId);
   1589         if (anchorView == null) {
   1590             return; // Raw contact deleted?
   1591         }
   1592         mAggregationSuggestionPopup = new ListPopupWindow(mContext, null);
   1593         mAggregationSuggestionPopup.setAnchorView(anchorView);
   1594         mAggregationSuggestionPopup.setWidth(anchorView.getWidth());
   1595         mAggregationSuggestionPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
   1596         mAggregationSuggestionPopup.setAdapter(
   1597                 new AggregationSuggestionAdapter(
   1598                         getActivity(),
   1599                         mState.size() == 1 && mState.get(0).isContactInsert(),
   1600                         /* listener =*/ this,
   1601                         mAggregationSuggestionEngine.getSuggestions()));
   1602         mAggregationSuggestionPopup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
   1603             @Override
   1604             public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
   1605                 final AggregationSuggestionView suggestionView = (AggregationSuggestionView) view;
   1606                 suggestionView.handleItemClickEvent();
   1607                 UiClosables.closeQuietly(mAggregationSuggestionPopup);
   1608                 mAggregationSuggestionPopup = null;
   1609             }
   1610         });
   1611         mAggregationSuggestionPopup.show();
   1612     }
   1613 
   1614     /**
   1615      * Returns the raw contact editor view for the given rawContactId that should be used as the
   1616      * anchor for aggregation suggestions.
   1617      */
   1618     abstract protected View getAggregationAnchorView(long rawContactId);
   1619 
   1620     /**
   1621      * Whether the given raw contact ID matches the one used to last load aggregation
   1622      * suggestions.
   1623      */
   1624     protected boolean isAggregationSuggestionRawContactId(long rawContactId) {
   1625         return mAggregationSuggestionsRawContactId == rawContactId;
   1626     }
   1627 
   1628     @Override
   1629     public void onJoinAction(long contactId, List<Long> rawContactIdList) {
   1630         final long rawContactIds[] = new long[rawContactIdList.size()];
   1631         for (int i = 0; i < rawContactIds.length; i++) {
   1632             rawContactIds[i] = rawContactIdList.get(i);
   1633         }
   1634         try {
   1635             JoinSuggestedContactDialogFragment.show(this, rawContactIds);
   1636         } catch (Exception ignored) {
   1637             // No problem - the activity is no longer available to display the dialog
   1638         }
   1639     }
   1640 
   1641     /**
   1642      * Joins the suggested contact (specified by the id's of constituent raw
   1643      * contacts), save all changes, and stay in the editor.
   1644      */
   1645     protected void doJoinSuggestedContact(long[] rawContactIds) {
   1646         if (!hasValidState() || mStatus != Status.EDITING) {
   1647             return;
   1648         }
   1649 
   1650         mState.setJoinWithRawContacts(rawContactIds);
   1651         save(SaveMode.RELOAD);
   1652     }
   1653 
   1654     @Override
   1655     public void onEditAction(Uri contactLookupUri) {
   1656         SuggestionEditConfirmationDialogFragment.show(this, contactLookupUri);
   1657     }
   1658 
   1659     /**
   1660      * Abandons the currently edited contact and switches to editing the suggested
   1661      * one, transferring all the data there
   1662      */
   1663     protected void doEditSuggestedContact(Uri contactUri) {
   1664         if (mListener != null) {
   1665             // make sure we don't save this contact when closing down
   1666             mStatus = Status.CLOSING;
   1667             mListener.onEditOtherContactRequested(
   1668                     contactUri, mState.get(0).getContentValues());
   1669         }
   1670     }
   1671 
   1672     //
   1673     // Join Activity
   1674     //
   1675 
   1676     /**
   1677      * Performs aggregation with the contact selected by the user from suggestions or A-Z list.
   1678      */
   1679     abstract protected void joinAggregate(long contactId);
   1680 
   1681     //
   1682     // Utility methods
   1683     //
   1684 
   1685     /**
   1686      * Returns a legacy version of the given contactLookupUri if a legacy Uri was originally
   1687      * passed to the contact editor.
   1688      *
   1689      * @param contactLookupUri The Uri to possibly convert to legacy format.
   1690      * @param requestLookupUri The lookup Uri originally passed to the contact editor
   1691      *                         (via Intent data), may be null.
   1692      */
   1693     protected static Uri maybeConvertToLegacyLookupUri(Context context, Uri contactLookupUri,
   1694             Uri requestLookupUri) {
   1695         final String legacyAuthority = "contacts";
   1696         final String requestAuthority = requestLookupUri == null
   1697                 ? null : requestLookupUri.getAuthority();
   1698         if (legacyAuthority.equals(requestAuthority)) {
   1699             // Build a legacy Uri if that is what was requested by caller
   1700             final long contactId = ContentUris.parseId(Contacts.lookupContact(
   1701                     context.getContentResolver(), contactLookupUri));
   1702             final Uri legacyContentUri = Uri.parse("content://contacts/people");
   1703             return ContentUris.withAppendedId(legacyContentUri, contactId);
   1704         }
   1705         // Otherwise pass back a lookup-style Uri
   1706         return contactLookupUri;
   1707     }
   1708 
   1709     /**
   1710      * Whether the argument Intent requested a contact insert action or not.
   1711      */
   1712     protected static boolean isInsert(Intent intent) {
   1713         return intent == null ? false : isInsert(intent.getAction());
   1714     }
   1715 
   1716     protected static boolean isInsert(String action) {
   1717         return Intent.ACTION_INSERT.equals(action)
   1718                 || ContactEditorBaseActivity.ACTION_INSERT.equals(action);
   1719     }
   1720 
   1721     protected static boolean isEdit(String action) {
   1722         return Intent.ACTION_EDIT.equals(action)
   1723                 || ContactEditorBaseActivity.ACTION_EDIT.equals(action);
   1724     }
   1725 }
   1726