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