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