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