ContactEditorFragment.java revision 4a1c574cd62eb7ca1e0fcc3a61e5378e5e0787fe
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
17package com.android.contacts.editor;
18
19import android.accounts.Account;
20import android.app.Activity;
21import android.app.AlertDialog;
22import android.app.Dialog;
23import android.app.DialogFragment;
24import android.app.Fragment;
25import android.app.LoaderManager;
26import android.app.LoaderManager.LoaderCallbacks;
27import android.content.ActivityNotFoundException;
28import android.content.ContentUris;
29import android.content.ContentValues;
30import android.content.Context;
31import android.content.CursorLoader;
32import android.content.DialogInterface;
33import android.content.Intent;
34import android.content.Loader;
35import android.database.Cursor;
36import android.graphics.Bitmap;
37import android.graphics.Rect;
38import android.media.RingtoneManager;
39import android.net.Uri;
40import android.os.Bundle;
41import android.os.SystemClock;
42import android.provider.ContactsContract.CommonDataKinds.Email;
43import android.provider.ContactsContract.CommonDataKinds.Event;
44import android.provider.ContactsContract.CommonDataKinds.Organization;
45import android.provider.ContactsContract.CommonDataKinds.Phone;
46import android.provider.ContactsContract.CommonDataKinds.Photo;
47import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
48import android.provider.ContactsContract.Contacts;
49import android.provider.ContactsContract.Groups;
50import android.provider.ContactsContract.Intents;
51import android.provider.ContactsContract.QuickContact;
52import android.provider.ContactsContract.RawContacts;
53import android.text.TextUtils;
54import android.util.Log;
55import android.view.LayoutInflater;
56import android.view.Menu;
57import android.view.MenuInflater;
58import android.view.MenuItem;
59import android.view.View;
60import android.view.ViewGroup;
61import android.widget.AdapterView;
62import android.widget.AdapterView.OnItemClickListener;
63import android.widget.BaseAdapter;
64import android.widget.LinearLayout;
65import android.widget.ListPopupWindow;
66import android.widget.Toast;
67
68import com.android.contacts.ContactSaveService;
69import com.android.contacts.GroupMetaDataLoader;
70import com.android.contacts.R;
71import com.android.contacts.activities.ContactEditorAccountsChangedActivity;
72import com.android.contacts.activities.ContactEditorActivity;
73import com.android.contacts.common.model.AccountTypeManager;
74import com.android.contacts.common.model.ValuesDelta;
75import com.android.contacts.common.model.account.AccountType;
76import com.android.contacts.common.model.account.AccountWithDataSet;
77import com.android.contacts.common.model.account.GoogleAccountType;
78import com.android.contacts.common.util.AccountsListAdapter;
79import com.android.contacts.common.util.AccountsListAdapter.AccountListFilter;
80import com.android.contacts.detail.PhotoSelectionHandler;
81import com.android.contacts.editor.AggregationSuggestionEngine.Suggestion;
82import com.android.contacts.editor.Editor.EditorListener;
83import com.android.contacts.common.model.Contact;
84import com.android.contacts.common.model.ContactLoader;
85import com.android.contacts.common.model.RawContact;
86import com.android.contacts.common.model.RawContactDelta;
87import com.android.contacts.common.model.RawContactDeltaList;
88import com.android.contacts.common.model.RawContactModifier;
89import com.android.contacts.list.UiIntentActions;
90import com.android.contacts.quickcontact.QuickContactActivity;
91import com.android.contacts.util.ContactPhotoUtils;
92import com.android.contacts.util.HelpUtils;
93import com.android.contacts.util.PhoneCapabilityTester;
94import com.android.contacts.util.UiClosables;
95import com.google.common.collect.ImmutableList;
96import com.google.common.collect.Lists;
97
98import java.io.FileNotFoundException;
99import java.util.ArrayList;
100import java.util.Collections;
101import java.util.Comparator;
102import java.util.HashMap;
103import java.util.List;
104
105public 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.EXTRA_ACCOUNT);
438                final String dataSet = mIntentExtras == null ? null :
439                        mIntentExtras.getString(Intents.Insert.EXTRA_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            createContact(defaultAccount);
686        }
687    }
688
689    /**
690     * Create a contact by automatically selecting the first account. If there's no available
691     * account, a device-local contact should be created.
692     */
693    private void createContact() {
694        final List<AccountWithDataSet> accounts =
695                AccountTypeManager.getInstance(mContext).getAccounts(true);
696        // No Accounts available. Create a phone-local contact.
697        if (accounts.isEmpty()) {
698            createContact(null);
699            return;
700        }
701
702        // We have an account switcher in "create-account" screen, so don't need to ask a user to
703        // select an account here.
704        createContact(accounts.get(0));
705    }
706
707    /**
708     * Shows account creation screen associated with a given account.
709     *
710     * @param account may be null to signal a device-local contact should be created.
711     */
712    private void createContact(AccountWithDataSet account) {
713        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
714        final AccountType accountType = accountTypes.getAccountTypeForAccount(account);
715
716        if (accountType.getCreateContactActivityClassName() != null) {
717            if (mListener != null) {
718                mListener.onCustomCreateContactActivityRequested(account, mIntentExtras);
719            }
720        } else {
721            bindEditorsForNewContact(account, accountType);
722        }
723    }
724
725    /**
726     * Removes a current editor ({@link #mState}) and rebinds new editor for a new account.
727     * Some of old data are reused with new restriction enforced by the new account.
728     *
729     * @param oldState Old data being edited.
730     * @param oldAccount Old account associated with oldState.
731     * @param newAccount New account to be used.
732     */
733    private void rebindEditorsForNewContact(
734            RawContactDelta oldState, AccountWithDataSet oldAccount,
735            AccountWithDataSet newAccount) {
736        AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
737        AccountType oldAccountType = accountTypes.getAccountTypeForAccount(oldAccount);
738        AccountType newAccountType = accountTypes.getAccountTypeForAccount(newAccount);
739
740        if (newAccountType.getCreateContactActivityClassName() != null) {
741            Log.w(TAG, "external activity called in rebind situation");
742            if (mListener != null) {
743                mListener.onCustomCreateContactActivityRequested(newAccount, mIntentExtras);
744            }
745        } else {
746            mExistingContactDataReady = false;
747            mNewContactDataReady = false;
748            mState = new RawContactDeltaList();
749            bindEditorsForNewContact(newAccount, newAccountType, oldState, oldAccountType);
750            if (mIsEdit) {
751                bindEditorsForExistingContact(mDefaultDisplayName, mIsUserProfile, mRawContacts);
752            }
753        }
754    }
755
756    private void bindEditorsForNewContact(AccountWithDataSet account,
757            final AccountType accountType) {
758        bindEditorsForNewContact(account, accountType, null, null);
759    }
760
761    private void bindEditorsForNewContact(AccountWithDataSet newAccount,
762            final AccountType newAccountType, RawContactDelta oldState,
763            AccountType oldAccountType) {
764        mStatus = Status.EDITING;
765
766        final RawContact rawContact = new RawContact();
767        rawContact.setAccount(newAccount);
768
769        final ValuesDelta valuesDelta = ValuesDelta.fromAfter(rawContact.getValues());
770        final RawContactDelta insert = new RawContactDelta(valuesDelta);
771        if (oldState == null) {
772            // Parse any values from incoming intent
773            RawContactModifier.parseExtras(mContext, newAccountType, insert, mIntentExtras);
774        } else {
775            RawContactModifier.migrateStateForNewContact(mContext, oldState, insert,
776                    oldAccountType, newAccountType);
777        }
778
779        // Ensure we have some default fields (if the account type does not support a field,
780        // ensureKind will not add it, so it is safe to add e.g. Event)
781        RawContactModifier.ensureKindExists(insert, newAccountType, Phone.CONTENT_ITEM_TYPE);
782        RawContactModifier.ensureKindExists(insert, newAccountType, Email.CONTENT_ITEM_TYPE);
783        RawContactModifier.ensureKindExists(insert, newAccountType, Organization.CONTENT_ITEM_TYPE);
784        RawContactModifier.ensureKindExists(insert, newAccountType, Event.CONTENT_ITEM_TYPE);
785        RawContactModifier.ensureKindExists(insert, newAccountType,
786                StructuredPostal.CONTENT_ITEM_TYPE);
787
788        // Set the correct URI for saving the contact as a profile
789        if (mNewLocalProfile) {
790            insert.setProfileQueryUri();
791        }
792
793        mState.add(insert);
794
795        mRequestFocus = true;
796
797        mNewContactDataReady = true;
798        bindEditors();
799    }
800
801    private void bindEditors() {
802        // bindEditors() can only bind views if there is data in mState, so immediately return
803        // if mState is null
804        if (mState.isEmpty()) {
805            return;
806        }
807
808        // Check if delta list is ready.  Delta list is populated from existing data and when
809        // editing an read-only contact, it's also populated with newly created data for the
810        // blank form.  When the data is not ready, skip. This method will be called multiple times.
811        if ((mIsEdit && !mExistingContactDataReady) || (mHasNewContact && !mNewContactDataReady)) {
812            return;
813        }
814
815        // Sort the editors
816        Collections.sort(mState, mComparator);
817
818        // Remove any existing editors and rebuild any visible
819        mContent.removeAllViews();
820
821        final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
822                Context.LAYOUT_INFLATER_SERVICE);
823        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
824        int numRawContacts = mState.size();
825
826        for (int i = 0; i < numRawContacts; i++) {
827            // TODO ensure proper ordering of entities in the list
828            final RawContactDelta rawContactDelta = mState.get(i);
829            if (!rawContactDelta.isVisible()) continue;
830
831            final AccountType type = rawContactDelta.getAccountType(accountTypes);
832            final long rawContactId = rawContactDelta.getRawContactId();
833
834            final BaseRawContactEditorView editor;
835            if (!type.areContactsWritable()) {
836                editor = (BaseRawContactEditorView) inflater.inflate(
837                        R.layout.raw_contact_readonly_editor_view, mContent, false);
838            } else {
839                editor = (RawContactEditorView) inflater.inflate(R.layout.raw_contact_editor_view,
840                        mContent, false);
841            }
842            editor.setListener(this);
843            final List<AccountWithDataSet> accounts = AccountTypeManager.getInstance(mContext)
844                    .getAccounts(true);
845            if (mHasNewContact && !mNewLocalProfile && accounts.size() > 1) {
846                addAccountSwitcher(mState.get(0), editor);
847            }
848
849            editor.setEnabled(mEnabled);
850
851            if (mExpandedEditors.containsKey(rawContactId)) {
852                editor.setCollapsed(mExpandedEditors.get(rawContactId));
853            } else {
854                // By default, only the first editor will be expanded.
855                editor.setCollapsed(i != 0);
856            }
857
858            mContent.addView(editor);
859
860            editor.setState(rawContactDelta, type, mViewIdGenerator, isEditingUserProfile());
861            editor.setCollapsible(numRawContacts > 1);
862
863            // Set up the photo handler.
864            bindPhotoHandler(editor, type, mState);
865
866            // If a new photo was chosen but not yet saved, we need to update the UI to
867            // reflect this.
868            final Uri photoUri = updatedPhotoUriForRawContact(rawContactId);
869            if (photoUri != null) editor.setFullSizedPhoto(photoUri);
870
871            if (editor instanceof RawContactEditorView) {
872                final Activity activity = getActivity();
873                final RawContactEditorView rawContactEditor = (RawContactEditorView) editor;
874                EditorListener listener = new EditorListener() {
875
876                    @Override
877                    public void onRequest(int request) {
878                        if (activity.isFinishing()) { // Make sure activity is still running.
879                            return;
880                        }
881                        if (request == EditorListener.FIELD_CHANGED && !isEditingUserProfile()) {
882                            acquireAggregationSuggestions(activity, rawContactEditor);
883                        } else if (request == EditorListener.EDITOR_FOCUS_CHANGED) {
884                            adjustNameFieldsHintDarkness(rawContactEditor);
885                        }
886                    }
887
888                    @Override
889                    public void onDeleteRequested(Editor removedEditor) {
890                    }
891                };
892
893                final StructuredNameEditorView nameEditor = rawContactEditor.getNameEditor();
894                if (mRequestFocus) {
895                    nameEditor.requestFocus();
896                    mRequestFocus = false;
897                }
898                nameEditor.setEditorListener(listener);
899                if (!TextUtils.isEmpty(mDefaultDisplayName)) {
900                    nameEditor.setDisplayName(mDefaultDisplayName);
901                }
902
903                final TextFieldsEditorView phoneticNameEditor =
904                        rawContactEditor.getPhoneticNameEditor();
905                phoneticNameEditor.setEditorListener(listener);
906                rawContactEditor.setAutoAddToDefaultGroup(mAutoAddToDefaultGroup);
907
908                final TextFieldsEditorView nickNameEditor =
909                        rawContactEditor.getNickNameEditor();
910                nickNameEditor.setEditorListener(listener);
911
912                if (rawContactId == mAggregationSuggestionsRawContactId) {
913                    acquireAggregationSuggestions(activity, rawContactEditor);
914                }
915
916                adjustNameFieldsHintDarkness(rawContactEditor);
917            }
918        }
919
920        mRequestFocus = false;
921
922        bindGroupMetaData();
923
924        // Show editor now that we've loaded state
925        mContent.setVisibility(View.VISIBLE);
926
927        // Refresh Action Bar as the visibility of the join command
928        // Activity can be null if we have been detached from the Activity
929        final Activity activity = getActivity();
930        if (activity != null) activity.invalidateOptionsMenu();
931
932        updatedExpandedEditorsMap();
933    }
934
935    /**
936     * Adjust how dark the hint text should be on all the names' text fields.
937     *
938     * @param rawContactEditor editor to update
939     */
940    private void adjustNameFieldsHintDarkness(RawContactEditorView rawContactEditor) {
941        // Check whether fields contain focus by calling findFocus() instead of hasFocus().
942        // The hasFocus() value is not necessarily up to date.
943        final boolean nameFieldsAreNotFocused
944                = rawContactEditor.getNameEditor().findFocus() == null
945                && rawContactEditor.getPhoneticNameEditor().findFocus() == null
946                && rawContactEditor.getNickNameEditor().findFocus() == null;
947        rawContactEditor.getNameEditor().setHintColorDark(!nameFieldsAreNotFocused);
948        rawContactEditor.getPhoneticNameEditor().setHintColorDark(!nameFieldsAreNotFocused);
949        rawContactEditor.getNickNameEditor().setHintColorDark(!nameFieldsAreNotFocused);
950    }
951
952    /**
953     * Update the values in {@link #mExpandedEditors}.
954     */
955    private void updatedExpandedEditorsMap() {
956        for (int i = 0; i < mContent.getChildCount(); i++) {
957            final View childView = mContent.getChildAt(i);
958            if (childView instanceof BaseRawContactEditorView) {
959                BaseRawContactEditorView childEditor = (BaseRawContactEditorView) childView;
960                mExpandedEditors.put(childEditor.getRawContactId(), childEditor.isCollapsed());
961            }
962        }
963    }
964
965    /**
966     * If we've stashed a temporary file containing a contact's new photo, return its URI.
967     * @param rawContactId identifies the raw-contact whose Bitmap we'll try to return.
968     * @return Uru of photo for specified raw-contact, or null
969     */
970    private Uri updatedPhotoUriForRawContact(long rawContactId) {
971        return (Uri) mUpdatedPhotos.get(String.valueOf(rawContactId));
972    }
973
974    private void bindPhotoHandler(BaseRawContactEditorView editor, AccountType type,
975            RawContactDeltaList state) {
976        final int mode;
977        final boolean showIsPrimaryOption;
978        if (type.areContactsWritable()) {
979            if (editor.hasSetPhoto()) {
980                mode = PhotoActionPopup.Modes.WRITE_ABLE_PHOTO;
981                showIsPrimaryOption = hasMoreThanOnePhoto();
982            } else {
983                mode = PhotoActionPopup.Modes.NO_PHOTO;
984                showIsPrimaryOption = false;
985            }
986        } else if (editor.hasSetPhoto() && hasMoreThanOnePhoto()) {
987            mode = PhotoActionPopup.Modes.READ_ONLY_PHOTO;
988            showIsPrimaryOption = true;
989        } else {
990            // Read-only and either no photo or the only photo ==> no options
991            editor.getPhotoEditor().setEditorListener(null);
992            editor.getPhotoEditor().setShowPrimary(false);
993            return;
994        }
995        final PhotoHandler photoHandler = new PhotoHandler(mContext, editor, mode, state);
996        editor.getPhotoEditor().setEditorListener(
997                (PhotoHandler.PhotoEditorListener) photoHandler.getListener());
998        editor.getPhotoEditor().setShowPrimary(showIsPrimaryOption);
999
1000        // Note a newly created raw contact gets some random negative ID, so any value is valid
1001        // here. (i.e. don't check against -1 or anything.)
1002        if (mRawContactIdRequestingPhoto == editor.getRawContactId()) {
1003            mCurrentPhotoHandler = photoHandler;
1004        }
1005    }
1006
1007    private void bindGroupMetaData() {
1008        if (mGroupMetaData == null) {
1009            return;
1010        }
1011
1012        int editorCount = mContent.getChildCount();
1013        for (int i = 0; i < editorCount; i++) {
1014            BaseRawContactEditorView editor = (BaseRawContactEditorView) mContent.getChildAt(i);
1015            editor.setGroupMetaData(mGroupMetaData);
1016        }
1017    }
1018
1019    private void saveDefaultAccountIfNecessary() {
1020        // Verify that this is a newly created contact, that the contact is composed of only
1021        // 1 raw contact, and that the contact is not a user profile.
1022        if (!Intent.ACTION_INSERT.equals(mAction) && mState.size() == 1 &&
1023                !isEditingUserProfile()) {
1024            return;
1025        }
1026
1027        // Find the associated account for this contact (retrieve it here because there are
1028        // multiple paths to creating a contact and this ensures we always have the correct
1029        // account).
1030        final RawContactDelta rawContactDelta = mState.get(0);
1031        String name = rawContactDelta.getAccountName();
1032        String type = rawContactDelta.getAccountType();
1033        String dataSet = rawContactDelta.getDataSet();
1034
1035        AccountWithDataSet account = (name == null || type == null) ? null :
1036                new AccountWithDataSet(name, type, dataSet);
1037        mEditorUtils.saveDefaultAndAllAccounts(account);
1038    }
1039
1040    private void addAccountSwitcher(
1041            final RawContactDelta currentState, BaseRawContactEditorView editor) {
1042        final AccountWithDataSet currentAccount = new AccountWithDataSet(
1043                currentState.getAccountName(),
1044                currentState.getAccountType(),
1045                currentState.getDataSet());
1046        final View accountView = editor.findViewById(R.id.account);
1047        final View anchorView = editor.findViewById(R.id.account_selector_container);
1048        if (accountView == null) {
1049            return;
1050        }
1051        anchorView.setVisibility(View.VISIBLE);
1052        accountView.setOnClickListener(new View.OnClickListener() {
1053            @Override
1054            public void onClick(View v) {
1055                final ListPopupWindow popup = new ListPopupWindow(mContext, null);
1056                final AccountsListAdapter adapter =
1057                        new AccountsListAdapter(mContext,
1058                        AccountListFilter.ACCOUNTS_CONTACT_WRITABLE, currentAccount);
1059                popup.setWidth(anchorView.getWidth());
1060                popup.setAnchorView(anchorView);
1061                popup.setAdapter(adapter);
1062                popup.setModal(true);
1063                popup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
1064                popup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
1065                    @Override
1066                    public void onItemClick(AdapterView<?> parent, View view, int position,
1067                            long id) {
1068                        UiClosables.closeQuietly(popup);
1069                        AccountWithDataSet newAccount = adapter.getItem(position);
1070                        if (!newAccount.equals(currentAccount)) {
1071                            rebindEditorsForNewContact(currentState, currentAccount, newAccount);
1072                        }
1073                    }
1074                });
1075                popup.show();
1076            }
1077        });
1078    }
1079
1080    @Override
1081    public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
1082        inflater.inflate(R.menu.edit_contact, menu);
1083    }
1084
1085    @Override
1086    public void onPrepareOptionsMenu(Menu menu) {
1087        // This supports the keyboard shortcut to save changes to a contact but shouldn't be visible
1088        // because the custom action bar contains the "save" button now (not the overflow menu).
1089        // TODO: Find a better way to handle shortcuts, i.e. onKeyDown()?
1090        final MenuItem doneMenu = menu.findItem(R.id.menu_done);
1091        final MenuItem splitMenu = menu.findItem(R.id.menu_split);
1092        final MenuItem joinMenu = menu.findItem(R.id.menu_join);
1093        final MenuItem helpMenu = menu.findItem(R.id.menu_help);
1094        final MenuItem discardMenu = menu.findItem(R.id.menu_discard);
1095        final MenuItem sendToVoiceMailMenu = menu.findItem(R.id.menu_send_to_voicemail);
1096        final MenuItem ringToneMenu = menu.findItem(R.id.menu_set_ringtone);
1097        final MenuItem deleteMenu = menu.findItem(R.id.menu_delete);
1098        deleteMenu.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
1099        deleteMenu.setIcon(R.drawable.ic_delete_white_24dp);
1100
1101        // Set visibility of menus
1102        doneMenu.setVisible(false);
1103
1104        // Discard menu is only available if at least one raw contact is editable
1105        discardMenu.setVisible(mState != null &&
1106                mState.getFirstWritableRawContact(mContext) != null);
1107
1108        // help menu depending on whether this is inserting or editing
1109        if (Intent.ACTION_INSERT.equals(mAction)) {
1110            HelpUtils.prepareHelpMenuItem(mContext, helpMenu, R.string.help_url_people_add);
1111            splitMenu.setVisible(false);
1112            joinMenu.setVisible(false);
1113            deleteMenu.setVisible(false);
1114        } else if (Intent.ACTION_EDIT.equals(mAction)) {
1115            HelpUtils.prepareHelpMenuItem(mContext, helpMenu, R.string.help_url_people_edit);
1116            // Split only if more than one raw profile and not a user profile
1117            splitMenu.setVisible(mState.size() > 1 && !isEditingUserProfile());
1118            // Cannot join a user profile
1119            joinMenu.setVisible(!isEditingUserProfile());
1120            deleteMenu.setVisible(!mDisableDeleteMenuOption);
1121        } else {
1122            // something else, so don't show the help menu
1123            helpMenu.setVisible(false);
1124        }
1125
1126        // Hide telephony-related settings (ringtone, send to voicemail)
1127        // if we don't have a telephone or are editing a new contact.
1128        sendToVoiceMailMenu.setChecked(mSendToVoicemailState);
1129        sendToVoiceMailMenu.setVisible(mArePhoneOptionsChangable);
1130        ringToneMenu.setVisible(mArePhoneOptionsChangable);
1131
1132        int size = menu.size();
1133        for (int i = 0; i < size; i++) {
1134            menu.getItem(i).setEnabled(mEnabled);
1135        }
1136    }
1137
1138    @Override
1139    public boolean onOptionsItemSelected(MenuItem item) {
1140        switch (item.getItemId()) {
1141            case android.R.id.home:
1142            case R.id.menu_done:
1143                return save(SaveMode.CLOSE);
1144            case R.id.menu_discard:
1145                return revert();
1146            case R.id.menu_delete:
1147                if (mListener != null) mListener.onDeleteRequested(mLookupUri);
1148                return true;
1149            case R.id.menu_split:
1150                return doSplitContactAction();
1151            case R.id.menu_join:
1152                return doJoinContactAction();
1153            case R.id.menu_set_ringtone:
1154                doPickRingtone();
1155                return true;
1156            case R.id.menu_send_to_voicemail:
1157                // Update state and save
1158                mSendToVoicemailState = !mSendToVoicemailState;
1159                item.setChecked(mSendToVoicemailState);
1160                final Intent intent = ContactSaveService.createSetSendToVoicemail(
1161                        mContext, mLookupUri, mSendToVoicemailState);
1162                mContext.startService(intent);
1163                return true;
1164        }
1165
1166        return false;
1167    }
1168
1169    private boolean doSplitContactAction() {
1170        if (!hasValidState()) return false;
1171
1172        final SplitContactConfirmationDialogFragment dialog =
1173                new SplitContactConfirmationDialogFragment();
1174        dialog.setTargetFragment(this, 0);
1175        dialog.show(getFragmentManager(), SplitContactConfirmationDialogFragment.TAG);
1176        return true;
1177    }
1178
1179    private boolean doJoinContactAction() {
1180        if (!hasValidState()) {
1181            return false;
1182        }
1183
1184        // If we just started creating a new contact and haven't added any data, it's too
1185        // early to do a join
1186        if (mState.size() == 1 && mState.get(0).isContactInsert() && !hasPendingChanges()) {
1187            Toast.makeText(mContext, R.string.toast_join_with_empty_contact,
1188                            Toast.LENGTH_LONG).show();
1189            return true;
1190        }
1191
1192        return save(SaveMode.JOIN);
1193    }
1194
1195    /**
1196     * Check if our internal {@link #mState} is valid, usually checked before
1197     * performing user actions.
1198     */
1199    private boolean hasValidState() {
1200        return mState.size() > 0;
1201    }
1202
1203    /**
1204     * Return true if there are any edits to the current contact which need to
1205     * be saved.
1206     */
1207    private boolean hasPendingChanges() {
1208        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
1209        return RawContactModifier.hasChanges(mState, accountTypes);
1210    }
1211
1212    /**
1213     * Saves or creates the contact based on the mode, and if successful
1214     * finishes the activity.
1215     */
1216    public boolean save(int saveMode) {
1217        if (!hasValidState() || mStatus != Status.EDITING) {
1218            return false;
1219        }
1220
1221        // If we are about to close the editor - there is no need to refresh the data
1222        if (saveMode == SaveMode.CLOSE || saveMode == SaveMode.SPLIT) {
1223            getLoaderManager().destroyLoader(LOADER_DATA);
1224        }
1225
1226        mStatus = Status.SAVING;
1227
1228        if (!hasPendingChanges()) {
1229            if (mLookupUri == null && saveMode == SaveMode.RELOAD) {
1230                // We don't have anything to save and there isn't even an existing contact yet.
1231                // Nothing to do, simply go back to editing mode
1232                mStatus = Status.EDITING;
1233                return true;
1234            }
1235            onSaveCompleted(false, saveMode, mLookupUri != null, mLookupUri);
1236            return true;
1237        }
1238
1239        setEnabled(false);
1240
1241        // Store account as default account, only if this is a new contact
1242        saveDefaultAccountIfNecessary();
1243
1244        // Save contact
1245        Intent intent = ContactSaveService.createSaveContactIntent(mContext, mState,
1246                SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(),
1247                ((Activity)mContext).getClass(), ContactEditorActivity.ACTION_SAVE_COMPLETED,
1248                mUpdatedPhotos);
1249        mContext.startService(intent);
1250
1251        // Don't try to save the same photos twice.
1252        mUpdatedPhotos = new Bundle();
1253
1254        return true;
1255    }
1256
1257    private void doPickRingtone() {
1258
1259        final Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
1260        // Allow user to pick 'Default'
1261        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
1262        // Show only ringtones
1263        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_RINGTONE);
1264        // Allow the user to pick a silent ringtone
1265        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
1266
1267        final Uri ringtoneUri;
1268        if (mCustomRingtone != null) {
1269            ringtoneUri = Uri.parse(mCustomRingtone);
1270        } else {
1271            // Otherwise pick default ringtone Uri so that something is selected.
1272            ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
1273        }
1274
1275        // Put checkmark next to the current ringtone for this contact
1276        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, ringtoneUri);
1277
1278        // Launch!
1279        try {
1280            startActivityForResult(intent, REQUEST_CODE_PICK_RINGTONE);
1281        } catch (ActivityNotFoundException ex) {
1282            Toast.makeText(mContext, R.string.missing_app, Toast.LENGTH_SHORT).show();
1283        }
1284    }
1285
1286    private void handleRingtonePicked(Uri pickedUri) {
1287        if (pickedUri == null || RingtoneManager.isDefault(pickedUri)) {
1288            mCustomRingtone = null;
1289        } else {
1290            mCustomRingtone = pickedUri.toString();
1291        }
1292        Intent intent = ContactSaveService.createSetRingtone(
1293                mContext, mLookupUri, mCustomRingtone);
1294        mContext.startService(intent);
1295    }
1296
1297    public static class CancelEditDialogFragment extends DialogFragment {
1298
1299        public static void show(ContactEditorFragment fragment) {
1300            CancelEditDialogFragment dialog = new CancelEditDialogFragment();
1301            dialog.setTargetFragment(fragment, 0);
1302            dialog.show(fragment.getFragmentManager(), "cancelEditor");
1303        }
1304
1305        @Override
1306        public Dialog onCreateDialog(Bundle savedInstanceState) {
1307            AlertDialog dialog = new AlertDialog.Builder(getActivity())
1308                    .setIconAttribute(android.R.attr.alertDialogIcon)
1309                    .setMessage(R.string.cancel_confirmation_dialog_message)
1310                    .setPositiveButton(android.R.string.ok,
1311                        new DialogInterface.OnClickListener() {
1312                            @Override
1313                            public void onClick(DialogInterface dialogInterface, int whichButton) {
1314                                ((ContactEditorFragment)getTargetFragment()).doRevertAction();
1315                            }
1316                        }
1317                    )
1318                    .setNegativeButton(android.R.string.cancel, null)
1319                    .create();
1320            return dialog;
1321        }
1322    }
1323
1324    private boolean revert() {
1325        if (mState.isEmpty() || !hasPendingChanges()) {
1326            doRevertAction();
1327        } else {
1328            CancelEditDialogFragment.show(this);
1329        }
1330        return true;
1331    }
1332
1333    private void doRevertAction() {
1334        // When this Fragment is closed we don't want it to auto-save
1335        mStatus = Status.CLOSING;
1336        if (mListener != null) mListener.onReverted();
1337    }
1338
1339    public void doSaveAction() {
1340        save(SaveMode.CLOSE);
1341    }
1342
1343    public void onJoinCompleted(Uri uri) {
1344        onSaveCompleted(false, SaveMode.RELOAD, uri != null, uri);
1345    }
1346
1347    public void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded,
1348            Uri contactLookupUri) {
1349        if (hadChanges) {
1350            if (saveSucceeded) {
1351                if (saveMode != SaveMode.JOIN) {
1352                    Toast.makeText(mContext, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
1353                }
1354            } else {
1355                Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
1356            }
1357        }
1358        switch (saveMode) {
1359            case SaveMode.CLOSE:
1360            case SaveMode.HOME:
1361                final Intent resultIntent;
1362                if (saveSucceeded && contactLookupUri != null) {
1363                    final String requestAuthority =
1364                            mLookupUri == null ? null : mLookupUri.getAuthority();
1365
1366                    final String legacyAuthority = "contacts";
1367                    final Uri lookupUri;
1368                    if (legacyAuthority.equals(requestAuthority)) {
1369                        // Build legacy Uri when requested by caller
1370                        final long contactId = ContentUris.parseId(Contacts.lookupContact(
1371                                mContext.getContentResolver(), contactLookupUri));
1372                        final Uri legacyContentUri = Uri.parse("content://contacts/people");
1373                        final Uri legacyUri = ContentUris.withAppendedId(
1374                                legacyContentUri, contactId);
1375                        lookupUri = legacyUri;
1376                    } else {
1377                        // Otherwise pass back a lookup-style Uri
1378                        lookupUri = contactLookupUri;
1379                    }
1380                    resultIntent = QuickContact.composeQuickContactsIntent(getActivity(),
1381                            (Rect) null, lookupUri, QuickContactActivity.MODE_FULLY_EXPANDED, null);
1382                    // Make sure not to show QuickContacts on top of another QuickContacts.
1383                    resultIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
1384                } else {
1385                    resultIntent = null;
1386                }
1387                // It is already saved, so prevent that it is saved again
1388                mStatus = Status.CLOSING;
1389                if (mListener != null) mListener.onSaveFinished(resultIntent);
1390                break;
1391
1392            case SaveMode.RELOAD:
1393            case SaveMode.JOIN:
1394                if (saveSucceeded && contactLookupUri != null) {
1395                    // If it was a JOIN, we are now ready to bring up the join activity.
1396                    if (saveMode == SaveMode.JOIN && hasValidState()) {
1397                        showJoinAggregateActivity(contactLookupUri);
1398                    }
1399
1400                    // If this was in INSERT, we are changing into an EDIT now.
1401                    // If it already was an EDIT, we are changing to the new Uri now
1402                    mState = new RawContactDeltaList();
1403                    load(Intent.ACTION_EDIT, contactLookupUri, null);
1404                    mStatus = Status.LOADING;
1405                    getLoaderManager().restartLoader(LOADER_DATA, null, mDataLoaderListener);
1406                }
1407                break;
1408
1409            case SaveMode.SPLIT:
1410                mStatus = Status.CLOSING;
1411                if (mListener != null) {
1412                    mListener.onContactSplit(contactLookupUri);
1413                } else {
1414                    Log.d(TAG, "No listener registered, can not call onSplitFinished");
1415                }
1416                break;
1417        }
1418    }
1419
1420    /**
1421     * Shows a list of aggregates that can be joined into the currently viewed aggregate.
1422     *
1423     * @param contactLookupUri the fresh URI for the currently edited contact (after saving it)
1424     */
1425    private void showJoinAggregateActivity(Uri contactLookupUri) {
1426        if (contactLookupUri == null || !isAdded()) {
1427            return;
1428        }
1429
1430        mContactIdForJoin = ContentUris.parseId(contactLookupUri);
1431        mContactWritableForJoin = isContactWritable();
1432        final Intent intent = new Intent(UiIntentActions.PICK_JOIN_CONTACT_ACTION);
1433        intent.putExtra(UiIntentActions.TARGET_CONTACT_ID_EXTRA_KEY, mContactIdForJoin);
1434        startActivityForResult(intent, REQUEST_CODE_JOIN);
1435    }
1436
1437    /**
1438     * Performs aggregation with the contact selected by the user from suggestions or A-Z list.
1439     */
1440    private void joinAggregate(final long contactId) {
1441        Intent intent = ContactSaveService.createJoinContactsIntent(mContext, mContactIdForJoin,
1442                contactId, mContactWritableForJoin,
1443                ContactEditorActivity.class, ContactEditorActivity.ACTION_JOIN_COMPLETED);
1444        mContext.startService(intent);
1445    }
1446
1447    /**
1448     * Returns true if there is at least one writable raw contact in the current contact.
1449     */
1450    private boolean isContactWritable() {
1451        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
1452        int size = mState.size();
1453        for (int i = 0; i < size; i++) {
1454            RawContactDelta entity = mState.get(i);
1455            final AccountType type = entity.getAccountType(accountTypes);
1456            if (type.areContactsWritable()) {
1457                return true;
1458            }
1459        }
1460        return false;
1461    }
1462
1463    private boolean isEditingUserProfile() {
1464        return mNewLocalProfile || mIsUserProfile;
1465    }
1466
1467    public static interface Listener {
1468        /**
1469         * Contact was not found, so somehow close this fragment. This is raised after a contact
1470         * is removed via Menu/Delete
1471         */
1472        void onContactNotFound();
1473
1474        /**
1475         * Contact was split, so we can close now.
1476         * @param newLookupUri The lookup uri of the new contact that should be shown to the user.
1477         * The editor tries best to chose the most natural contact here.
1478         */
1479        void onContactSplit(Uri newLookupUri);
1480
1481        /**
1482         * User has tapped Revert, close the fragment now.
1483         */
1484        void onReverted();
1485
1486        /**
1487         * Contact was saved and the Fragment can now be closed safely.
1488         */
1489        void onSaveFinished(Intent resultIntent);
1490
1491        /**
1492         * User switched to editing a different contact (a suggestion from the
1493         * aggregation engine).
1494         */
1495        void onEditOtherContactRequested(
1496                Uri contactLookupUri, ArrayList<ContentValues> contentValues);
1497
1498        /**
1499         * Contact is being created for an external account that provides its own
1500         * new contact activity.
1501         */
1502        void onCustomCreateContactActivityRequested(AccountWithDataSet account,
1503                Bundle intentExtras);
1504
1505        /**
1506         * The edited raw contact belongs to an external account that provides
1507         * its own edit activity.
1508         *
1509         * @param redirect indicates that the current editor should be closed
1510         *            before the custom editor is shown.
1511         */
1512        void onCustomEditContactActivityRequested(AccountWithDataSet account, Uri rawContactUri,
1513                Bundle intentExtras, boolean redirect);
1514
1515        void onDeleteRequested(Uri contactUri);
1516    }
1517
1518    private class EntityDeltaComparator implements Comparator<RawContactDelta> {
1519        /**
1520         * Compare EntityDeltas for sorting the stack of editors.
1521         */
1522        @Override
1523        public int compare(RawContactDelta one, RawContactDelta two) {
1524            // Check direct equality
1525            if (one.equals(two)) {
1526                return 0;
1527            }
1528
1529            final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
1530            String accountType1 = one.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
1531            String dataSet1 = one.getValues().getAsString(RawContacts.DATA_SET);
1532            final AccountType type1 = accountTypes.getAccountType(accountType1, dataSet1);
1533            String accountType2 = two.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
1534            String dataSet2 = two.getValues().getAsString(RawContacts.DATA_SET);
1535            final AccountType type2 = accountTypes.getAccountType(accountType2, dataSet2);
1536
1537            // Check read-only. Sort read/write before read-only.
1538            if (!type1.areContactsWritable() && type2.areContactsWritable()) {
1539                return 1;
1540            } else if (type1.areContactsWritable() && !type2.areContactsWritable()) {
1541                return -1;
1542            }
1543
1544            // Check account type. Sort Google before non-Google.
1545            boolean skipAccountTypeCheck = false;
1546            boolean isGoogleAccount1 = type1 instanceof GoogleAccountType;
1547            boolean isGoogleAccount2 = type2 instanceof GoogleAccountType;
1548            if (isGoogleAccount1 && !isGoogleAccount2) {
1549                return -1;
1550            } else if (!isGoogleAccount1 && isGoogleAccount2) {
1551                return 1;
1552            } else if (isGoogleAccount1 && isGoogleAccount2){
1553                skipAccountTypeCheck = true;
1554            }
1555
1556            int value;
1557            if (!skipAccountTypeCheck) {
1558                // Sort accounts with type before accounts without types.
1559                if (type1.accountType != null && type2.accountType == null) {
1560                    return -1;
1561                } else if (type1.accountType == null && type2.accountType != null) {
1562                    return 1;
1563                }
1564
1565                if (type1.accountType != null && type2.accountType != null) {
1566                    value = type1.accountType.compareTo(type2.accountType);
1567                    if (value != 0) {
1568                        return value;
1569                    }
1570                }
1571
1572                // Fall back to data set. Sort accounts with data sets before
1573                // those without.
1574                if (type1.dataSet != null && type2.dataSet == null) {
1575                    return -1;
1576                } else if (type1.dataSet == null && type2.dataSet != null) {
1577                    return 1;
1578                }
1579
1580                if (type1.dataSet != null && type2.dataSet != null) {
1581                    value = type1.dataSet.compareTo(type2.dataSet);
1582                    if (value != 0) {
1583                        return value;
1584                    }
1585                }
1586            }
1587
1588            // Check account name
1589            String oneAccount = one.getAccountName();
1590            if (oneAccount == null) oneAccount = "";
1591            String twoAccount = two.getAccountName();
1592            if (twoAccount == null) twoAccount = "";
1593            value = oneAccount.compareTo(twoAccount);
1594            if (value != 0) {
1595                return value;
1596            }
1597
1598            // Both are in the same account, fall back to contact ID
1599            Long oneId = one.getRawContactId();
1600            Long twoId = two.getRawContactId();
1601            if (oneId == null) {
1602                return -1;
1603            } else if (twoId == null) {
1604                return 1;
1605            }
1606
1607            return (int)(oneId - twoId);
1608        }
1609    }
1610
1611    /**
1612     * Returns the contact ID for the currently edited contact or 0 if the contact is new.
1613     */
1614    protected long getContactId() {
1615        for (RawContactDelta rawContact : mState) {
1616            Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID);
1617            if (contactId != null) {
1618                return contactId;
1619            }
1620        }
1621        return 0;
1622    }
1623
1624    /**
1625     * Triggers an asynchronous search for aggregation suggestions.
1626     */
1627    private void acquireAggregationSuggestions(Context context,
1628            RawContactEditorView rawContactEditor) {
1629        long rawContactId = rawContactEditor.getRawContactId();
1630        if (mAggregationSuggestionsRawContactId != rawContactId
1631                && mAggregationSuggestionView != null) {
1632            mAggregationSuggestionView.setVisibility(View.GONE);
1633            mAggregationSuggestionView = null;
1634            mAggregationSuggestionEngine.reset();
1635        }
1636
1637        mAggregationSuggestionsRawContactId = rawContactId;
1638
1639        if (mAggregationSuggestionEngine == null) {
1640            mAggregationSuggestionEngine = new AggregationSuggestionEngine(context);
1641            mAggregationSuggestionEngine.setListener(this);
1642            mAggregationSuggestionEngine.start();
1643        }
1644
1645        mAggregationSuggestionEngine.setContactId(getContactId());
1646
1647        LabeledEditorView nameEditor = rawContactEditor.getNameEditor();
1648        mAggregationSuggestionEngine.onNameChange(nameEditor.getValues());
1649    }
1650
1651    @Override
1652    public void onAggregationSuggestionChange() {
1653        Activity activity = getActivity();
1654        if ((activity != null && activity.isFinishing())
1655                || !isVisible() ||  mState.isEmpty() || mStatus != Status.EDITING) {
1656            return;
1657        }
1658
1659        UiClosables.closeQuietly(mAggregationSuggestionPopup);
1660
1661        if (mAggregationSuggestionEngine.getSuggestedContactCount() == 0) {
1662            return;
1663        }
1664
1665        final RawContactEditorView rawContactView =
1666                (RawContactEditorView)getRawContactEditorView(mAggregationSuggestionsRawContactId);
1667        if (rawContactView == null) {
1668            return; // Raw contact deleted?
1669        }
1670        final View anchorView = rawContactView.findViewById(R.id.anchor_view);
1671        mAggregationSuggestionPopup = new ListPopupWindow(mContext, null);
1672        mAggregationSuggestionPopup.setAnchorView(anchorView);
1673        mAggregationSuggestionPopup.setWidth(anchorView.getWidth());
1674        mAggregationSuggestionPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
1675        mAggregationSuggestionPopup.setAdapter(
1676                new AggregationSuggestionAdapter(getActivity(),
1677                        mState.size() == 1 && mState.get(0).isContactInsert(),
1678                        this, mAggregationSuggestionEngine.getSuggestions()));
1679        mAggregationSuggestionPopup.setOnItemClickListener(mAggregationSuggestionItemClickListener);
1680        mAggregationSuggestionPopup.show();
1681    }
1682
1683    @Override
1684    public void onJoinAction(long contactId, List<Long> rawContactIdList) {
1685        long rawContactIds[] = new long[rawContactIdList.size()];
1686        for (int i = 0; i < rawContactIds.length; i++) {
1687            rawContactIds[i] = rawContactIdList.get(i);
1688        }
1689        JoinSuggestedContactDialogFragment dialog =
1690                new JoinSuggestedContactDialogFragment();
1691        Bundle args = new Bundle();
1692        args.putLongArray("rawContactIds", rawContactIds);
1693        dialog.setArguments(args);
1694        dialog.setTargetFragment(this, 0);
1695        try {
1696            dialog.show(getFragmentManager(), "join");
1697        } catch (Exception ex) {
1698            // No problem - the activity is no longer available to display the dialog
1699        }
1700    }
1701
1702    public static class JoinSuggestedContactDialogFragment extends DialogFragment {
1703
1704        @Override
1705        public Dialog onCreateDialog(Bundle savedInstanceState) {
1706            return new AlertDialog.Builder(getActivity())
1707                    .setIconAttribute(android.R.attr.alertDialogIcon)
1708                    .setMessage(R.string.aggregation_suggestion_join_dialog_message)
1709                    .setPositiveButton(android.R.string.yes,
1710                        new DialogInterface.OnClickListener() {
1711                            @Override
1712                            public void onClick(DialogInterface dialog, int whichButton) {
1713                                ContactEditorFragment targetFragment =
1714                                        (ContactEditorFragment) getTargetFragment();
1715                                long rawContactIds[] =
1716                                        getArguments().getLongArray("rawContactIds");
1717                                targetFragment.doJoinSuggestedContact(rawContactIds);
1718                            }
1719                        }
1720                    )
1721                    .setNegativeButton(android.R.string.no, null)
1722                    .create();
1723        }
1724    }
1725
1726    /**
1727     * Joins the suggested contact (specified by the id's of constituent raw
1728     * contacts), save all changes, and stay in the editor.
1729     */
1730    protected void doJoinSuggestedContact(long[] rawContactIds) {
1731        if (!hasValidState() || mStatus != Status.EDITING) {
1732            return;
1733        }
1734
1735        mState.setJoinWithRawContacts(rawContactIds);
1736        save(SaveMode.RELOAD);
1737    }
1738
1739    @Override
1740    public void onEditAction(Uri contactLookupUri) {
1741        SuggestionEditConfirmationDialogFragment dialog =
1742                new SuggestionEditConfirmationDialogFragment();
1743        Bundle args = new Bundle();
1744        args.putParcelable("contactUri", contactLookupUri);
1745        dialog.setArguments(args);
1746        dialog.setTargetFragment(this, 0);
1747        dialog.show(getFragmentManager(), "edit");
1748    }
1749
1750    public static class SuggestionEditConfirmationDialogFragment extends DialogFragment {
1751
1752        @Override
1753        public Dialog onCreateDialog(Bundle savedInstanceState) {
1754            return new AlertDialog.Builder(getActivity())
1755                    .setIconAttribute(android.R.attr.alertDialogIcon)
1756                    .setMessage(R.string.aggregation_suggestion_edit_dialog_message)
1757                    .setPositiveButton(android.R.string.yes,
1758                        new DialogInterface.OnClickListener() {
1759                            @Override
1760                            public void onClick(DialogInterface dialog, int whichButton) {
1761                                ContactEditorFragment targetFragment =
1762                                        (ContactEditorFragment) getTargetFragment();
1763                                Uri contactUri =
1764                                        getArguments().getParcelable("contactUri");
1765                                targetFragment.doEditSuggestedContact(contactUri);
1766                            }
1767                        }
1768                    )
1769                    .setNegativeButton(android.R.string.no, null)
1770                    .create();
1771        }
1772    }
1773
1774    /**
1775     * Abandons the currently edited contact and switches to editing the suggested
1776     * one, transferring all the data there
1777     */
1778    protected void doEditSuggestedContact(Uri contactUri) {
1779        if (mListener != null) {
1780            // make sure we don't save this contact when closing down
1781            mStatus = Status.CLOSING;
1782            mListener.onEditOtherContactRequested(
1783                    contactUri, mState.get(0).getContentValues());
1784        }
1785    }
1786
1787    public void setAggregationSuggestionViewEnabled(boolean enabled) {
1788        if (mAggregationSuggestionView == null) {
1789            return;
1790        }
1791
1792        LinearLayout itemList = (LinearLayout) mAggregationSuggestionView.findViewById(
1793                R.id.aggregation_suggestions);
1794        int count = itemList.getChildCount();
1795        for (int i = 0; i < count; i++) {
1796            itemList.getChildAt(i).setEnabled(enabled);
1797        }
1798    }
1799
1800    @Override
1801    public void onSaveInstanceState(Bundle outState) {
1802        outState.putParcelable(KEY_URI, mLookupUri);
1803        outState.putString(KEY_ACTION, mAction);
1804
1805        if (hasValidState()) {
1806            // Store entities with modifications
1807            outState.putParcelable(KEY_EDIT_STATE, mState);
1808        }
1809        outState.putLong(KEY_RAW_CONTACT_ID_REQUESTING_PHOTO, mRawContactIdRequestingPhoto);
1810        outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator);
1811        outState.putParcelable(KEY_CURRENT_PHOTO_URI, mCurrentPhotoUri);
1812        outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin);
1813        outState.putBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN, mContactWritableForJoin);
1814        outState.putLong(KEY_SHOW_JOIN_SUGGESTIONS, mAggregationSuggestionsRawContactId);
1815        outState.putBoolean(KEY_ENABLED, mEnabled);
1816        outState.putBoolean(KEY_NEW_LOCAL_PROFILE, mNewLocalProfile);
1817        outState.putBoolean(KEY_DISABLE_DELETE_MENU_OPTION, mDisableDeleteMenuOption);
1818        outState.putBoolean(KEY_IS_USER_PROFILE, mIsUserProfile);
1819        outState.putInt(KEY_STATUS, mStatus);
1820        outState.putParcelable(KEY_UPDATED_PHOTOS, mUpdatedPhotos);
1821        outState.putBoolean(KEY_HAS_NEW_CONTACT, mHasNewContact);
1822        outState.putBoolean(KEY_IS_EDIT, mIsEdit);
1823        outState.putBoolean(KEY_NEW_CONTACT_READY, mNewContactDataReady);
1824        outState.putBoolean(KEY_EXISTING_CONTACT_READY, mExistingContactDataReady);
1825        outState.putParcelableArrayList(KEY_RAW_CONTACTS,
1826                mRawContacts == null ?
1827                Lists.<RawContact>newArrayList() : Lists.newArrayList(mRawContacts));
1828        outState.putBoolean(KEY_SEND_TO_VOICE_MAIL_STATE, mSendToVoicemailState);
1829        outState.putString(KEY_CUSTOM_RINGTONE, mCustomRingtone);
1830        outState.putBoolean(KEY_ARE_PHONE_OPTIONS_CHANGEABLE, mArePhoneOptionsChangable);
1831        outState.putSerializable(KEY_EXPANDED_EDITORS, mExpandedEditors);
1832
1833        super.onSaveInstanceState(outState);
1834    }
1835
1836    @Override
1837    public void onActivityResult(int requestCode, int resultCode, Intent data) {
1838        if (mStatus == Status.SUB_ACTIVITY) {
1839            mStatus = Status.EDITING;
1840        }
1841
1842        // See if the photo selection handler handles this result.
1843        if (mCurrentPhotoHandler != null && mCurrentPhotoHandler.handlePhotoActivityResult(
1844                requestCode, resultCode, data)) {
1845            return;
1846        }
1847
1848        switch (requestCode) {
1849            case REQUEST_CODE_JOIN: {
1850                // Ignore failed requests
1851                if (resultCode != Activity.RESULT_OK) return;
1852                if (data != null) {
1853                    final long contactId = ContentUris.parseId(data.getData());
1854                    joinAggregate(contactId);
1855                }
1856                break;
1857            }
1858            case REQUEST_CODE_ACCOUNTS_CHANGED: {
1859                // Bail if the account selector was not successful.
1860                if (resultCode != Activity.RESULT_OK) {
1861                    mListener.onReverted();
1862                    return;
1863                }
1864                // If there's an account specified, use it.
1865                if (data != null) {
1866                    AccountWithDataSet account = data.getParcelableExtra(
1867                            Intents.Insert.EXTRA_ACCOUNT);
1868                    if (account != null) {
1869                        createContact(account);
1870                        return;
1871                    }
1872                }
1873                // If there isn't an account specified, then this is likely a phone-local
1874                // contact, so we should continue setting up the editor by automatically selecting
1875                // the most appropriate account.
1876                createContact();
1877                break;
1878            }
1879            case REQUEST_CODE_PICK_RINGTONE: {
1880                if (data != null) {
1881                    final Uri pickedUri = data.getParcelableExtra(
1882                            RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
1883                    handleRingtonePicked(pickedUri);
1884                }
1885                break;
1886            }
1887        }
1888    }
1889
1890    /**
1891     * Sets the photo stored in mPhoto and writes it to the RawContact with the given id
1892     */
1893    private void setPhoto(long rawContact, Bitmap photo, Uri photoUri) {
1894        BaseRawContactEditorView requestingEditor = getRawContactEditorView(rawContact);
1895
1896        if (photo == null || photo.getHeight() < 0 || photo.getWidth() < 0) {
1897            // This is unexpected.
1898            Log.w(TAG, "Invalid bitmap passed to setPhoto()");
1899        }
1900
1901        if (requestingEditor != null) {
1902            requestingEditor.setPhotoEntry(photo);
1903            // Immediately set all other photos as non-primary. Otherwise the UI can display
1904            // multiple photos as "Primary photo".
1905            for (int i = 0; i < mContent.getChildCount(); i++) {
1906                final View childView = mContent.getChildAt(i);
1907                if (childView instanceof BaseRawContactEditorView
1908                        && childView != requestingEditor) {
1909                    final BaseRawContactEditorView rawContactEditor
1910                            = (BaseRawContactEditorView) childView;
1911                    rawContactEditor.getPhotoEditor().setSuperPrimary(false);
1912                }
1913            }
1914        } else {
1915            Log.w(TAG, "The contact that requested the photo is no longer present.");
1916        }
1917
1918        mUpdatedPhotos.putParcelable(String.valueOf(rawContact), photoUri);
1919    }
1920
1921    /**
1922     * Finds raw contact editor view for the given rawContactId.
1923     */
1924    public BaseRawContactEditorView getRawContactEditorView(long rawContactId) {
1925        for (int i = 0; i < mContent.getChildCount(); i++) {
1926            final View childView = mContent.getChildAt(i);
1927            if (childView instanceof BaseRawContactEditorView) {
1928                final BaseRawContactEditorView editor = (BaseRawContactEditorView) childView;
1929                if (editor.getRawContactId() == rawContactId) {
1930                    return editor;
1931                }
1932            }
1933        }
1934        return null;
1935    }
1936
1937    /**
1938     * Returns true if there is currently more than one photo on screen.
1939     */
1940    private boolean hasMoreThanOnePhoto() {
1941        int countWithPicture = 0;
1942        final int numEntities = mState.size();
1943        for (int i = 0; i < numEntities; i++) {
1944            final RawContactDelta entity = mState.get(i);
1945            if (entity.isVisible()) {
1946                final ValuesDelta primary = entity.getPrimaryEntry(Photo.CONTENT_ITEM_TYPE);
1947                if (primary != null && primary.getPhoto() != null) {
1948                    countWithPicture++;
1949                } else {
1950                    final long rawContactId = entity.getRawContactId();
1951                    final Uri uri = mUpdatedPhotos.getParcelable(String.valueOf(rawContactId));
1952                    if (uri != null) {
1953                        try {
1954                            mContext.getContentResolver().openInputStream(uri);
1955                            countWithPicture++;
1956                        } catch (FileNotFoundException e) {
1957                        }
1958                    }
1959                }
1960
1961                if (countWithPicture > 1) {
1962                    return true;
1963                }
1964            }
1965        }
1966        return false;
1967    }
1968
1969    /**
1970     * The listener for the data loader
1971     */
1972    private final LoaderManager.LoaderCallbacks<Contact> mDataLoaderListener =
1973            new LoaderCallbacks<Contact>() {
1974        @Override
1975        public Loader<Contact> onCreateLoader(int id, Bundle args) {
1976            mLoaderStartTime = SystemClock.elapsedRealtime();
1977            return new ContactLoader(mContext, mLookupUri, true);
1978        }
1979
1980        @Override
1981        public void onLoadFinished(Loader<Contact> loader, Contact data) {
1982            final long loaderCurrentTime = SystemClock.elapsedRealtime();
1983            Log.v(TAG, "Time needed for loading: " + (loaderCurrentTime-mLoaderStartTime));
1984            if (!data.isLoaded()) {
1985                // Item has been deleted. Close activity without saving again.
1986                Log.i(TAG, "No contact found. Closing activity");
1987                mStatus = Status.CLOSING;
1988                if (mListener != null) mListener.onContactNotFound();
1989                return;
1990            }
1991
1992            mStatus = Status.EDITING;
1993            mLookupUri = data.getLookupUri();
1994            final long setDataStartTime = SystemClock.elapsedRealtime();
1995            setData(data);
1996            final long setDataEndTime = SystemClock.elapsedRealtime();
1997
1998            Log.v(TAG, "Time needed for setting UI: " + (setDataEndTime-setDataStartTime));
1999        }
2000
2001        @Override
2002        public void onLoaderReset(Loader<Contact> loader) {
2003        }
2004    };
2005
2006    /**
2007     * The listener for the group meta data loader for all groups.
2008     */
2009    private final LoaderManager.LoaderCallbacks<Cursor> mGroupLoaderListener =
2010            new LoaderCallbacks<Cursor>() {
2011
2012        @Override
2013        public CursorLoader onCreateLoader(int id, Bundle args) {
2014            return new GroupMetaDataLoader(mContext, Groups.CONTENT_URI);
2015        }
2016
2017        @Override
2018        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
2019            mGroupMetaData = data;
2020            bindGroupMetaData();
2021        }
2022
2023        @Override
2024        public void onLoaderReset(Loader<Cursor> loader) {
2025        }
2026    };
2027
2028    @Override
2029    public void onSplitContactConfirmed() {
2030        if (mState.isEmpty()) {
2031            // This may happen when this Fragment is recreated by the system during users
2032            // confirming the split action (and thus this method is called just before onCreate()),
2033            // for example.
2034            Log.e(TAG, "mState became null during the user's confirming split action. " +
2035                    "Cannot perform the save action.");
2036            return;
2037        }
2038
2039        mState.markRawContactsForSplitting();
2040        save(SaveMode.SPLIT);
2041    }
2042
2043    /**
2044     * Custom photo handler for the editor.  The inner listener that this creates also has a
2045     * reference to the editor and acts as an {@link EditorListener}, and uses that editor to hold
2046     * state information in several of the listener methods.
2047     */
2048    private final class PhotoHandler extends PhotoSelectionHandler {
2049
2050        final long mRawContactId;
2051        private final BaseRawContactEditorView mEditor;
2052        private final PhotoActionListener mPhotoEditorListener;
2053
2054        public PhotoHandler(Context context, BaseRawContactEditorView editor, int photoMode,
2055                RawContactDeltaList state) {
2056            super(context, editor.getPhotoEditor().getChangeAnchorView(), photoMode, false, state);
2057            mEditor = editor;
2058            mRawContactId = editor.getRawContactId();
2059            mPhotoEditorListener = new PhotoEditorListener();
2060        }
2061
2062        @Override
2063        public PhotoActionListener getListener() {
2064            return mPhotoEditorListener;
2065        }
2066
2067        @Override
2068        public void startPhotoActivity(Intent intent, int requestCode, Uri photoUri) {
2069            mRawContactIdRequestingPhoto = mEditor.getRawContactId();
2070            mCurrentPhotoHandler = this;
2071            mStatus = Status.SUB_ACTIVITY;
2072            mCurrentPhotoUri = photoUri;
2073            ContactEditorFragment.this.startActivityForResult(intent, requestCode);
2074        }
2075
2076        private final class PhotoEditorListener extends PhotoSelectionHandler.PhotoActionListener
2077                implements EditorListener {
2078
2079            @Override
2080            public void onRequest(int request) {
2081                if (!hasValidState()) return;
2082
2083                if (request == EditorListener.REQUEST_PICK_PHOTO) {
2084                    onClick(mEditor.getPhotoEditor());
2085                }
2086                if (request == EditorListener.REQUEST_PICK_PRIMARY_PHOTO) {
2087                    useAsPrimaryChosen();
2088                }
2089            }
2090
2091            @Override
2092            public void onDeleteRequested(Editor removedEditor) {
2093                // The picture cannot be deleted, it can only be removed, which is handled by
2094                // onRemovePictureChosen()
2095            }
2096
2097            /**
2098             * User has chosen to set the selected photo as the (super) primary photo
2099             */
2100            public void useAsPrimaryChosen() {
2101                // Set the IsSuperPrimary for each editor
2102                int count = mContent.getChildCount();
2103                for (int i = 0; i < count; i++) {
2104                    final View childView = mContent.getChildAt(i);
2105                    if (childView instanceof BaseRawContactEditorView) {
2106                        final BaseRawContactEditorView editor =
2107                                (BaseRawContactEditorView) childView;
2108                        final PhotoEditorView photoEditor = editor.getPhotoEditor();
2109                        photoEditor.setSuperPrimary(editor == mEditor);
2110                    }
2111                }
2112                bindEditors();
2113            }
2114
2115            /**
2116             * User has chosen to remove a picture
2117             */
2118            @Override
2119            public void onRemovePictureChosen() {
2120                mEditor.setPhotoEntry(null);
2121
2122                // Prevent bitmap from being restored if rotate the device.
2123                // (only if we first chose a new photo before removing it)
2124                mUpdatedPhotos.remove(String.valueOf(mRawContactId));
2125                bindEditors();
2126            }
2127
2128            @Override
2129            public void onPhotoSelected(Uri uri) throws FileNotFoundException {
2130                final Bitmap bitmap = ContactPhotoUtils.getBitmapFromUri(mContext, uri);
2131                setPhoto(mRawContactId, bitmap, uri);
2132                mCurrentPhotoHandler = null;
2133                bindEditors();
2134            }
2135
2136            @Override
2137            public Uri getCurrentPhotoUri() {
2138                return mCurrentPhotoUri;
2139            }
2140
2141            @Override
2142            public void onPhotoSelectionDismissed() {
2143                // Nothing to do.
2144            }
2145        }
2146    }
2147}
2148