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