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