ContactEditorFragment.java revision 637a38ec9de6b1f434d7a13105f2e747faae5107
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.model.Contact;
82import com.android.contacts.model.ContactLoader;
83import com.android.contacts.model.RawContact;
84import com.android.contacts.model.RawContactDelta;
85import com.android.contacts.model.RawContactDeltaList;
86import com.android.contacts.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 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        // Split only if more than one raw profile and not a user profile
1028        splitMenu.setVisible(mState.size() > 1 && !isEditingUserProfile());
1029
1030        // Cannot join a user profile
1031        joinMenu.setVisible(!isEditingUserProfile());
1032
1033        // Discard menu is only available if at least one raw contact is editable
1034        discardMenu.setVisible(mState != null &&
1035                mState.getFirstWritableRawContact(mContext) != null);
1036
1037        // help menu depending on whether this is inserting or editing
1038        if (Intent.ACTION_INSERT.equals(mAction)) {
1039            // inserting
1040            HelpUtils.prepareHelpMenuItem(mContext, helpMenu, R.string.help_url_people_add);
1041        } else if (Intent.ACTION_EDIT.equals(mAction)) {
1042            // editing
1043            HelpUtils.prepareHelpMenuItem(mContext, helpMenu, R.string.help_url_people_edit);
1044        } else {
1045            // something else, so don't show the help menu
1046            helpMenu.setVisible(false);
1047        }
1048
1049        int size = menu.size();
1050        for (int i = 0; i < size; i++) {
1051            menu.getItem(i).setEnabled(mEnabled);
1052        }
1053    }
1054
1055    @Override
1056    public boolean onOptionsItemSelected(MenuItem item) {
1057        switch (item.getItemId()) {
1058            case R.id.menu_done:
1059                return save(SaveMode.CLOSE);
1060            case R.id.menu_discard:
1061                return revert();
1062            case R.id.menu_split:
1063                return doSplitContactAction();
1064            case R.id.menu_join:
1065                return doJoinContactAction();
1066        }
1067        return false;
1068    }
1069
1070    private boolean doSplitContactAction() {
1071        if (!hasValidState()) return false;
1072
1073        final SplitContactConfirmationDialogFragment dialog =
1074                new SplitContactConfirmationDialogFragment();
1075        dialog.setTargetFragment(this, 0);
1076        dialog.show(getFragmentManager(), SplitContactConfirmationDialogFragment.TAG);
1077        return true;
1078    }
1079
1080    private boolean doJoinContactAction() {
1081        if (!hasValidState()) {
1082            return false;
1083        }
1084
1085        // If we just started creating a new contact and haven't added any data, it's too
1086        // early to do a join
1087        if (mState.size() == 1 && mState.get(0).isContactInsert() && !hasPendingChanges()) {
1088            Toast.makeText(mContext, R.string.toast_join_with_empty_contact,
1089                            Toast.LENGTH_LONG).show();
1090            return true;
1091        }
1092
1093        return save(SaveMode.JOIN);
1094    }
1095
1096    /**
1097     * Check if our internal {@link #mState} is valid, usually checked before
1098     * performing user actions.
1099     */
1100    private boolean hasValidState() {
1101        return mState.size() > 0;
1102    }
1103
1104    /**
1105     * Return true if there are any edits to the current contact which need to
1106     * be saved.
1107     */
1108    private boolean hasPendingChanges() {
1109        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
1110        return RawContactModifier.hasChanges(mState, accountTypes);
1111    }
1112
1113    /**
1114     * Saves or creates the contact based on the mode, and if successful
1115     * finishes the activity.
1116     */
1117    public boolean save(int saveMode) {
1118        if (!hasValidState() || mStatus != Status.EDITING) {
1119            return false;
1120        }
1121
1122        // If we are about to close the editor - there is no need to refresh the data
1123        if (saveMode == SaveMode.CLOSE || saveMode == SaveMode.SPLIT) {
1124            getLoaderManager().destroyLoader(LOADER_DATA);
1125        }
1126
1127        mStatus = Status.SAVING;
1128
1129        if (!hasPendingChanges()) {
1130            if (mLookupUri == null && saveMode == SaveMode.RELOAD) {
1131                // We don't have anything to save and there isn't even an existing contact yet.
1132                // Nothing to do, simply go back to editing mode
1133                mStatus = Status.EDITING;
1134                return true;
1135            }
1136            onSaveCompleted(false, saveMode, mLookupUri != null, mLookupUri);
1137            return true;
1138        }
1139
1140        setEnabled(false);
1141
1142        // Store account as default account, only if this is a new contact
1143        saveDefaultAccountIfNecessary();
1144
1145        // Save contact
1146        Intent intent = ContactSaveService.createSaveContactIntent(mContext, mState,
1147                SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(),
1148                ((Activity)mContext).getClass(), ContactEditorActivity.ACTION_SAVE_COMPLETED,
1149                mUpdatedPhotos);
1150        mContext.startService(intent);
1151
1152        // Don't try to save the same photos twice.
1153        mUpdatedPhotos = new Bundle();
1154
1155        return true;
1156    }
1157
1158    public static class CancelEditDialogFragment extends DialogFragment {
1159
1160        public static void show(ContactEditorFragment fragment) {
1161            CancelEditDialogFragment dialog = new CancelEditDialogFragment();
1162            dialog.setTargetFragment(fragment, 0);
1163            dialog.show(fragment.getFragmentManager(), "cancelEditor");
1164        }
1165
1166        @Override
1167        public Dialog onCreateDialog(Bundle savedInstanceState) {
1168            AlertDialog dialog = new AlertDialog.Builder(getActivity())
1169                    .setIconAttribute(android.R.attr.alertDialogIcon)
1170                    .setMessage(R.string.cancel_confirmation_dialog_message)
1171                    .setPositiveButton(android.R.string.ok,
1172                        new DialogInterface.OnClickListener() {
1173                            @Override
1174                            public void onClick(DialogInterface dialogInterface, int whichButton) {
1175                                ((ContactEditorFragment)getTargetFragment()).doRevertAction();
1176                            }
1177                        }
1178                    )
1179                    .setNegativeButton(android.R.string.cancel, null)
1180                    .create();
1181            return dialog;
1182        }
1183    }
1184
1185    private boolean revert() {
1186        if (mState.isEmpty() || !hasPendingChanges()) {
1187            doRevertAction();
1188        } else {
1189            CancelEditDialogFragment.show(this);
1190        }
1191        return true;
1192    }
1193
1194    private void doRevertAction() {
1195        // When this Fragment is closed we don't want it to auto-save
1196        mStatus = Status.CLOSING;
1197        if (mListener != null) mListener.onReverted();
1198    }
1199
1200    public void doSaveAction() {
1201        save(SaveMode.CLOSE);
1202    }
1203
1204    public void onJoinCompleted(Uri uri) {
1205        onSaveCompleted(false, SaveMode.RELOAD, uri != null, uri);
1206    }
1207
1208    public void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded,
1209            Uri contactLookupUri) {
1210        if (hadChanges) {
1211            if (saveSucceeded) {
1212                if (saveMode != SaveMode.JOIN) {
1213                    Toast.makeText(mContext, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
1214                }
1215            } else {
1216                Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
1217            }
1218        }
1219        switch (saveMode) {
1220            case SaveMode.CLOSE:
1221            case SaveMode.HOME:
1222                final Intent resultIntent;
1223                if (saveSucceeded && contactLookupUri != null) {
1224                    final String requestAuthority =
1225                            mLookupUri == null ? null : mLookupUri.getAuthority();
1226
1227                    final String legacyAuthority = "contacts";
1228
1229                    resultIntent = new Intent();
1230                    resultIntent.setAction(Intent.ACTION_VIEW);
1231                    if (legacyAuthority.equals(requestAuthority)) {
1232                        // Build legacy Uri when requested by caller
1233                        final long contactId = ContentUris.parseId(Contacts.lookupContact(
1234                                mContext.getContentResolver(), contactLookupUri));
1235                        final Uri legacyContentUri = Uri.parse("content://contacts/people");
1236                        final Uri legacyUri = ContentUris.withAppendedId(
1237                                legacyContentUri, contactId);
1238                        resultIntent.setData(legacyUri);
1239                    } else {
1240                        // Otherwise pass back a lookup-style Uri
1241                        resultIntent.setData(contactLookupUri);
1242                    }
1243
1244                } else {
1245                    resultIntent = null;
1246                }
1247                // It is already saved, so prevent that it is saved again
1248                mStatus = Status.CLOSING;
1249                if (mListener != null) mListener.onSaveFinished(resultIntent);
1250                break;
1251
1252            case SaveMode.RELOAD:
1253            case SaveMode.JOIN:
1254                if (saveSucceeded && contactLookupUri != null) {
1255                    // If it was a JOIN, we are now ready to bring up the join activity.
1256                    if (saveMode == SaveMode.JOIN && hasValidState()) {
1257                        showJoinAggregateActivity(contactLookupUri);
1258                    }
1259
1260                    // If this was in INSERT, we are changing into an EDIT now.
1261                    // If it already was an EDIT, we are changing to the new Uri now
1262                    mState = new RawContactDeltaList();
1263                    load(Intent.ACTION_EDIT, contactLookupUri, null);
1264                    mStatus = Status.LOADING;
1265                    getLoaderManager().restartLoader(LOADER_DATA, null, mDataLoaderListener);
1266                }
1267                break;
1268
1269            case SaveMode.SPLIT:
1270                mStatus = Status.CLOSING;
1271                if (mListener != null) {
1272                    mListener.onContactSplit(contactLookupUri);
1273                } else {
1274                    Log.d(TAG, "No listener registered, can not call onSplitFinished");
1275                }
1276                break;
1277        }
1278    }
1279
1280    /**
1281     * Shows a list of aggregates that can be joined into the currently viewed aggregate.
1282     *
1283     * @param contactLookupUri the fresh URI for the currently edited contact (after saving it)
1284     */
1285    private void showJoinAggregateActivity(Uri contactLookupUri) {
1286        if (contactLookupUri == null || !isAdded()) {
1287            return;
1288        }
1289
1290        mContactIdForJoin = ContentUris.parseId(contactLookupUri);
1291        mContactWritableForJoin = isContactWritable();
1292        final Intent intent = new Intent(JoinContactActivity.JOIN_CONTACT);
1293        intent.putExtra(JoinContactActivity.EXTRA_TARGET_CONTACT_ID, mContactIdForJoin);
1294        startActivityForResult(intent, REQUEST_CODE_JOIN);
1295    }
1296
1297    /**
1298     * Performs aggregation with the contact selected by the user from suggestions or A-Z list.
1299     */
1300    private void joinAggregate(final long contactId) {
1301        Intent intent = ContactSaveService.createJoinContactsIntent(mContext, mContactIdForJoin,
1302                contactId, mContactWritableForJoin,
1303                ContactEditorActivity.class, ContactEditorActivity.ACTION_JOIN_COMPLETED);
1304        mContext.startService(intent);
1305    }
1306
1307    /**
1308     * Returns true if there is at least one writable raw contact in the current contact.
1309     */
1310    private boolean isContactWritable() {
1311        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
1312        int size = mState.size();
1313        for (int i = 0; i < size; i++) {
1314            RawContactDelta entity = mState.get(i);
1315            final AccountType type = entity.getAccountType(accountTypes);
1316            if (type.areContactsWritable()) {
1317                return true;
1318            }
1319        }
1320        return false;
1321    }
1322
1323    private boolean isEditingUserProfile() {
1324        return mNewLocalProfile || mIsUserProfile;
1325    }
1326
1327    public static interface Listener {
1328        /**
1329         * Contact was not found, so somehow close this fragment. This is raised after a contact
1330         * is removed via Menu/Delete (unless it was a new contact)
1331         */
1332        void onContactNotFound();
1333
1334        /**
1335         * Contact was split, so we can close now.
1336         * @param newLookupUri The lookup uri of the new contact that should be shown to the user.
1337         * The editor tries best to chose the most natural contact here.
1338         */
1339        void onContactSplit(Uri newLookupUri);
1340
1341        /**
1342         * User has tapped Revert, close the fragment now.
1343         */
1344        void onReverted();
1345
1346        /**
1347         * Contact was saved and the Fragment can now be closed safely.
1348         */
1349        void onSaveFinished(Intent resultIntent);
1350
1351        /**
1352         * User switched to editing a different contact (a suggestion from the
1353         * aggregation engine).
1354         */
1355        void onEditOtherContactRequested(
1356                Uri contactLookupUri, ArrayList<ContentValues> contentValues);
1357
1358        /**
1359         * Contact is being created for an external account that provides its own
1360         * new contact activity.
1361         */
1362        void onCustomCreateContactActivityRequested(AccountWithDataSet account,
1363                Bundle intentExtras);
1364
1365        /**
1366         * The edited raw contact belongs to an external account that provides
1367         * its own edit activity.
1368         *
1369         * @param redirect indicates that the current editor should be closed
1370         *            before the custom editor is shown.
1371         */
1372        void onCustomEditContactActivityRequested(AccountWithDataSet account, Uri rawContactUri,
1373                Bundle intentExtras, boolean redirect);
1374    }
1375
1376    private class EntityDeltaComparator implements Comparator<RawContactDelta> {
1377        /**
1378         * Compare EntityDeltas for sorting the stack of editors.
1379         */
1380        @Override
1381        public int compare(RawContactDelta one, RawContactDelta two) {
1382            // Check direct equality
1383            if (one.equals(two)) {
1384                return 0;
1385            }
1386
1387            final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
1388            String accountType1 = one.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
1389            String dataSet1 = one.getValues().getAsString(RawContacts.DATA_SET);
1390            final AccountType type1 = accountTypes.getAccountType(accountType1, dataSet1);
1391            String accountType2 = two.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
1392            String dataSet2 = two.getValues().getAsString(RawContacts.DATA_SET);
1393            final AccountType type2 = accountTypes.getAccountType(accountType2, dataSet2);
1394
1395            // Check read-only
1396            if (!type1.areContactsWritable() && type2.areContactsWritable()) {
1397                return 1;
1398            } else if (type1.areContactsWritable() && !type2.areContactsWritable()) {
1399                return -1;
1400            }
1401
1402            // Check account type
1403            boolean skipAccountTypeCheck = false;
1404            boolean isGoogleAccount1 = type1 instanceof GoogleAccountType;
1405            boolean isGoogleAccount2 = type2 instanceof GoogleAccountType;
1406            if (isGoogleAccount1 && !isGoogleAccount2) {
1407                return -1;
1408            } else if (!isGoogleAccount1 && isGoogleAccount2) {
1409                return 1;
1410            } else if (isGoogleAccount1 && isGoogleAccount2){
1411                skipAccountTypeCheck = true;
1412            }
1413
1414            int value;
1415            if (!skipAccountTypeCheck) {
1416                if (type1.accountType == null) {
1417                    return 1;
1418                }
1419                value = type1.accountType.compareTo(type2.accountType);
1420                if (value != 0) {
1421                    return value;
1422                } else {
1423                    // Fall back to data set.
1424                    if (type1.dataSet != null) {
1425                        value = type1.dataSet.compareTo(type2.dataSet);
1426                        if (value != 0) {
1427                            return value;
1428                        }
1429                    } else if (type2.dataSet != null) {
1430                        return 1;
1431                    }
1432                }
1433            }
1434
1435            // Check account name
1436            String oneAccount = one.getAccountName();
1437            if (oneAccount == null) oneAccount = "";
1438            String twoAccount = two.getAccountName();
1439            if (twoAccount == null) twoAccount = "";
1440            value = oneAccount.compareTo(twoAccount);
1441            if (value != 0) {
1442                return value;
1443            }
1444
1445            // Both are in the same account, fall back to contact ID
1446            Long oneId = one.getRawContactId();
1447            Long twoId = two.getRawContactId();
1448            if (oneId == null) {
1449                return -1;
1450            } else if (twoId == null) {
1451                return 1;
1452            }
1453
1454            return (int)(oneId - twoId);
1455        }
1456    }
1457
1458    /**
1459     * Returns the contact ID for the currently edited contact or 0 if the contact is new.
1460     */
1461    protected long getContactId() {
1462        for (RawContactDelta rawContact : mState) {
1463            Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID);
1464            if (contactId != null) {
1465                return contactId;
1466            }
1467        }
1468        return 0;
1469    }
1470
1471    /**
1472     * Triggers an asynchronous search for aggregation suggestions.
1473     */
1474    private void acquireAggregationSuggestions(Context context,
1475            RawContactEditorView rawContactEditor) {
1476        long rawContactId = rawContactEditor.getRawContactId();
1477        if (mAggregationSuggestionsRawContactId != rawContactId
1478                && mAggregationSuggestionView != null) {
1479            mAggregationSuggestionView.setVisibility(View.GONE);
1480            mAggregationSuggestionView = null;
1481            mAggregationSuggestionEngine.reset();
1482        }
1483
1484        mAggregationSuggestionsRawContactId = rawContactId;
1485
1486        if (mAggregationSuggestionEngine == null) {
1487            mAggregationSuggestionEngine = new AggregationSuggestionEngine(context);
1488            mAggregationSuggestionEngine.setListener(this);
1489            mAggregationSuggestionEngine.start();
1490        }
1491
1492        mAggregationSuggestionEngine.setContactId(getContactId());
1493
1494        LabeledEditorView nameEditor = rawContactEditor.getNameEditor();
1495        mAggregationSuggestionEngine.onNameChange(nameEditor.getValues());
1496    }
1497
1498    @Override
1499    public void onAggregationSuggestionChange() {
1500        Activity activity = getActivity();
1501        if ((activity != null && activity.isFinishing())
1502                || !isVisible() ||  mState.isEmpty() || mStatus != Status.EDITING) {
1503            return;
1504        }
1505
1506        UiClosables.closeQuietly(mAggregationSuggestionPopup);
1507
1508        if (mAggregationSuggestionEngine.getSuggestedContactCount() == 0) {
1509            return;
1510        }
1511
1512        final RawContactEditorView rawContactView =
1513                (RawContactEditorView)getRawContactEditorView(mAggregationSuggestionsRawContactId);
1514        if (rawContactView == null) {
1515            return; // Raw contact deleted?
1516        }
1517        final View anchorView = rawContactView.findViewById(R.id.anchor_view);
1518        mAggregationSuggestionPopup = new ListPopupWindow(mContext, null);
1519        mAggregationSuggestionPopup.setAnchorView(anchorView);
1520        mAggregationSuggestionPopup.setWidth(anchorView.getWidth());
1521        mAggregationSuggestionPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
1522        mAggregationSuggestionPopup.setAdapter(
1523                new AggregationSuggestionAdapter(getActivity(),
1524                        mState.size() == 1 && mState.get(0).isContactInsert(),
1525                        this, mAggregationSuggestionEngine.getSuggestions()));
1526        mAggregationSuggestionPopup.setOnItemClickListener(mAggregationSuggestionItemClickListener);
1527        mAggregationSuggestionPopup.show();
1528    }
1529
1530    @Override
1531    public void onJoinAction(long contactId, List<Long> rawContactIdList) {
1532        long rawContactIds[] = new long[rawContactIdList.size()];
1533        for (int i = 0; i < rawContactIds.length; i++) {
1534            rawContactIds[i] = rawContactIdList.get(i);
1535        }
1536        JoinSuggestedContactDialogFragment dialog =
1537                new JoinSuggestedContactDialogFragment();
1538        Bundle args = new Bundle();
1539        args.putLongArray("rawContactIds", rawContactIds);
1540        dialog.setArguments(args);
1541        dialog.setTargetFragment(this, 0);
1542        try {
1543            dialog.show(getFragmentManager(), "join");
1544        } catch (Exception ex) {
1545            // No problem - the activity is no longer available to display the dialog
1546        }
1547    }
1548
1549    public static class JoinSuggestedContactDialogFragment extends DialogFragment {
1550
1551        @Override
1552        public Dialog onCreateDialog(Bundle savedInstanceState) {
1553            return new AlertDialog.Builder(getActivity())
1554                    .setIconAttribute(android.R.attr.alertDialogIcon)
1555                    .setMessage(R.string.aggregation_suggestion_join_dialog_message)
1556                    .setPositiveButton(android.R.string.yes,
1557                        new DialogInterface.OnClickListener() {
1558                            @Override
1559                            public void onClick(DialogInterface dialog, int whichButton) {
1560                                ContactEditorFragment targetFragment =
1561                                        (ContactEditorFragment) getTargetFragment();
1562                                long rawContactIds[] =
1563                                        getArguments().getLongArray("rawContactIds");
1564                                targetFragment.doJoinSuggestedContact(rawContactIds);
1565                            }
1566                        }
1567                    )
1568                    .setNegativeButton(android.R.string.no, null)
1569                    .create();
1570        }
1571    }
1572
1573    /**
1574     * Joins the suggested contact (specified by the id's of constituent raw
1575     * contacts), save all changes, and stay in the editor.
1576     */
1577    protected void doJoinSuggestedContact(long[] rawContactIds) {
1578        if (!hasValidState() || mStatus != Status.EDITING) {
1579            return;
1580        }
1581
1582        mState.setJoinWithRawContacts(rawContactIds);
1583        save(SaveMode.RELOAD);
1584    }
1585
1586    @Override
1587    public void onEditAction(Uri contactLookupUri) {
1588        SuggestionEditConfirmationDialogFragment dialog =
1589                new SuggestionEditConfirmationDialogFragment();
1590        Bundle args = new Bundle();
1591        args.putParcelable("contactUri", contactLookupUri);
1592        dialog.setArguments(args);
1593        dialog.setTargetFragment(this, 0);
1594        dialog.show(getFragmentManager(), "edit");
1595    }
1596
1597    public static class SuggestionEditConfirmationDialogFragment extends DialogFragment {
1598
1599        @Override
1600        public Dialog onCreateDialog(Bundle savedInstanceState) {
1601            return new AlertDialog.Builder(getActivity())
1602                    .setIconAttribute(android.R.attr.alertDialogIcon)
1603                    .setMessage(R.string.aggregation_suggestion_edit_dialog_message)
1604                    .setPositiveButton(android.R.string.yes,
1605                        new DialogInterface.OnClickListener() {
1606                            @Override
1607                            public void onClick(DialogInterface dialog, int whichButton) {
1608                                ContactEditorFragment targetFragment =
1609                                        (ContactEditorFragment) getTargetFragment();
1610                                Uri contactUri =
1611                                        getArguments().getParcelable("contactUri");
1612                                targetFragment.doEditSuggestedContact(contactUri);
1613                            }
1614                        }
1615                    )
1616                    .setNegativeButton(android.R.string.no, null)
1617                    .create();
1618        }
1619    }
1620
1621    /**
1622     * Abandons the currently edited contact and switches to editing the suggested
1623     * one, transferring all the data there
1624     */
1625    protected void doEditSuggestedContact(Uri contactUri) {
1626        if (mListener != null) {
1627            // make sure we don't save this contact when closing down
1628            mStatus = Status.CLOSING;
1629            mListener.onEditOtherContactRequested(
1630                    contactUri, mState.get(0).getContentValues());
1631        }
1632    }
1633
1634    public void setAggregationSuggestionViewEnabled(boolean enabled) {
1635        if (mAggregationSuggestionView == null) {
1636            return;
1637        }
1638
1639        LinearLayout itemList = (LinearLayout) mAggregationSuggestionView.findViewById(
1640                R.id.aggregation_suggestions);
1641        int count = itemList.getChildCount();
1642        for (int i = 0; i < count; i++) {
1643            itemList.getChildAt(i).setEnabled(enabled);
1644        }
1645    }
1646
1647    @Override
1648    public void onSaveInstanceState(Bundle outState) {
1649        outState.putParcelable(KEY_URI, mLookupUri);
1650        outState.putString(KEY_ACTION, mAction);
1651
1652        if (hasValidState()) {
1653            // Store entities with modifications
1654            outState.putParcelable(KEY_EDIT_STATE, mState);
1655        }
1656        outState.putLong(KEY_RAW_CONTACT_ID_REQUESTING_PHOTO, mRawContactIdRequestingPhoto);
1657        outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator);
1658        outState.putParcelable(KEY_CURRENT_PHOTO_URI, mCurrentPhotoUri);
1659        outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin);
1660        outState.putBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN, mContactWritableForJoin);
1661        outState.putLong(KEY_SHOW_JOIN_SUGGESTIONS, mAggregationSuggestionsRawContactId);
1662        outState.putBoolean(KEY_ENABLED, mEnabled);
1663        outState.putBoolean(KEY_NEW_LOCAL_PROFILE, mNewLocalProfile);
1664        outState.putBoolean(KEY_IS_USER_PROFILE, mIsUserProfile);
1665        outState.putInt(KEY_STATUS, mStatus);
1666        outState.putParcelable(KEY_UPDATED_PHOTOS, mUpdatedPhotos);
1667        outState.putBoolean(KEY_HAS_NEW_CONTACT, mHasNewContact);
1668        outState.putBoolean(KEY_IS_EDIT, mIsEdit);
1669        outState.putBoolean(KEY_NEW_CONTACT_READY, mNewContactDataReady);
1670        outState.putBoolean(KEY_EXISTING_CONTACT_READY, mExistingContactDataReady);
1671        outState.putParcelableArrayList(KEY_RAW_CONTACTS,
1672                mRawContacts == null ?
1673                Lists.<RawContact> newArrayList() :  Lists.newArrayList(mRawContacts));
1674
1675        super.onSaveInstanceState(outState);
1676    }
1677
1678    @Override
1679    public void onActivityResult(int requestCode, int resultCode, Intent data) {
1680        if (mStatus == Status.SUB_ACTIVITY) {
1681            mStatus = Status.EDITING;
1682        }
1683
1684        // See if the photo selection handler handles this result.
1685        if (mCurrentPhotoHandler != null && mCurrentPhotoHandler.handlePhotoActivityResult(
1686                requestCode, resultCode, data)) {
1687            return;
1688        }
1689
1690        switch (requestCode) {
1691            case REQUEST_CODE_JOIN: {
1692                // Ignore failed requests
1693                if (resultCode != Activity.RESULT_OK) return;
1694                if (data != null) {
1695                    final long contactId = ContentUris.parseId(data.getData());
1696                    joinAggregate(contactId);
1697                }
1698                break;
1699            }
1700            case REQUEST_CODE_ACCOUNTS_CHANGED: {
1701                // Bail if the account selector was not successful.
1702                if (resultCode != Activity.RESULT_OK) {
1703                    mListener.onReverted();
1704                    return;
1705                }
1706                // If there's an account specified, use it.
1707                if (data != null) {
1708                    AccountWithDataSet account = data.getParcelableExtra(Intents.Insert.ACCOUNT);
1709                    if (account != null) {
1710                        createContact(account);
1711                        return;
1712                    }
1713                }
1714                // If there isn't an account specified, then this is likely a phone-local
1715                // contact, so we should continue setting up the editor by automatically selecting
1716                // the most appropriate account.
1717                createContact();
1718                break;
1719            }
1720        }
1721    }
1722
1723    /**
1724     * Sets the photo stored in mPhoto and writes it to the RawContact with the given id
1725     */
1726    private void setPhoto(long rawContact, Bitmap photo, Uri photoUri) {
1727        BaseRawContactEditorView requestingEditor = getRawContactEditorView(rawContact);
1728
1729        if (photo == null || photo.getHeight() < 0 || photo.getWidth() < 0) {
1730            // This is unexpected.
1731            Log.w(TAG, "Invalid bitmap passed to setPhoto()");
1732        }
1733
1734        if (requestingEditor != null) {
1735            requestingEditor.setPhotoBitmap(photo);
1736        } else {
1737            Log.w(TAG, "The contact that requested the photo is no longer present.");
1738        }
1739
1740        mUpdatedPhotos.putParcelable(String.valueOf(rawContact), photoUri);
1741    }
1742
1743    /**
1744     * Finds raw contact editor view for the given rawContactId.
1745     */
1746    public BaseRawContactEditorView getRawContactEditorView(long rawContactId) {
1747        for (int i = 0; i < mContent.getChildCount(); i++) {
1748            final View childView = mContent.getChildAt(i);
1749            if (childView instanceof BaseRawContactEditorView) {
1750                final BaseRawContactEditorView editor = (BaseRawContactEditorView) childView;
1751                if (editor.getRawContactId() == rawContactId) {
1752                    return editor;
1753                }
1754            }
1755        }
1756        return null;
1757    }
1758
1759    /**
1760     * Returns true if there is currently more than one photo on screen.
1761     */
1762    private boolean hasMoreThanOnePhoto() {
1763        int countWithPicture = 0;
1764        final int numEntities = mState.size();
1765        for (int i = 0; i < numEntities; i++) {
1766            final RawContactDelta entity = mState.get(i);
1767            if (entity.isVisible()) {
1768                final ValuesDelta primary = entity.getPrimaryEntry(Photo.CONTENT_ITEM_TYPE);
1769                if (primary != null && primary.getPhoto() != null) {
1770                    countWithPicture++;
1771                } else {
1772                    final long rawContactId = entity.getRawContactId();
1773                    final Uri uri = mUpdatedPhotos.getParcelable(String.valueOf(rawContactId));
1774                    if (uri != null) {
1775                        try {
1776                            mContext.getContentResolver().openInputStream(uri);
1777                            countWithPicture++;
1778                        } catch (FileNotFoundException e) {
1779                        }
1780                    }
1781                }
1782
1783                if (countWithPicture > 1) {
1784                    return true;
1785                }
1786            }
1787        }
1788        return false;
1789    }
1790
1791    /**
1792     * The listener for the data loader
1793     */
1794    private final LoaderManager.LoaderCallbacks<Contact> mDataLoaderListener =
1795            new LoaderCallbacks<Contact>() {
1796        @Override
1797        public Loader<Contact> onCreateLoader(int id, Bundle args) {
1798            mLoaderStartTime = SystemClock.elapsedRealtime();
1799            return new ContactLoader(mContext, mLookupUri, true);
1800        }
1801
1802        @Override
1803        public void onLoadFinished(Loader<Contact> loader, Contact data) {
1804            final long loaderCurrentTime = SystemClock.elapsedRealtime();
1805            Log.v(TAG, "Time needed for loading: " + (loaderCurrentTime-mLoaderStartTime));
1806            if (!data.isLoaded()) {
1807                // Item has been deleted
1808                Log.i(TAG, "No contact found. Closing activity");
1809                if (mListener != null) mListener.onContactNotFound();
1810                return;
1811            }
1812
1813            mStatus = Status.EDITING;
1814            mLookupUri = data.getLookupUri();
1815            final long setDataStartTime = SystemClock.elapsedRealtime();
1816            setData(data);
1817            final long setDataEndTime = SystemClock.elapsedRealtime();
1818
1819            Log.v(TAG, "Time needed for setting UI: " + (setDataEndTime-setDataStartTime));
1820        }
1821
1822        @Override
1823        public void onLoaderReset(Loader<Contact> loader) {
1824        }
1825    };
1826
1827    /**
1828     * The listener for the group meta data loader for all groups.
1829     */
1830    private final LoaderManager.LoaderCallbacks<Cursor> mGroupLoaderListener =
1831            new LoaderCallbacks<Cursor>() {
1832
1833        @Override
1834        public CursorLoader onCreateLoader(int id, Bundle args) {
1835            return new GroupMetaDataLoader(mContext, Groups.CONTENT_URI);
1836        }
1837
1838        @Override
1839        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
1840            mGroupMetaData = data;
1841            bindGroupMetaData();
1842        }
1843
1844        @Override
1845        public void onLoaderReset(Loader<Cursor> loader) {
1846        }
1847    };
1848
1849    @Override
1850    public void onSplitContactConfirmed() {
1851        if (mState.isEmpty()) {
1852            // This may happen when this Fragment is recreated by the system during users
1853            // confirming the split action (and thus this method is called just before onCreate()),
1854            // for example.
1855            Log.e(TAG, "mState became null during the user's confirming split action. " +
1856                    "Cannot perform the save action.");
1857            return;
1858        }
1859
1860        mState.markRawContactsForSplitting();
1861        save(SaveMode.SPLIT);
1862    }
1863
1864    /**
1865     * Custom photo handler for the editor.  The inner listener that this creates also has a
1866     * reference to the editor and acts as an {@link EditorListener}, and uses that editor to hold
1867     * state information in several of the listener methods.
1868     */
1869    private final class PhotoHandler extends PhotoSelectionHandler {
1870
1871        final long mRawContactId;
1872        private final BaseRawContactEditorView mEditor;
1873        private final PhotoActionListener mPhotoEditorListener;
1874
1875        public PhotoHandler(Context context, BaseRawContactEditorView editor, int photoMode,
1876                RawContactDeltaList state) {
1877            super(context, editor.getPhotoEditor(), photoMode, false, state);
1878            mEditor = editor;
1879            mRawContactId = editor.getRawContactId();
1880            mPhotoEditorListener = new PhotoEditorListener();
1881        }
1882
1883        @Override
1884        public PhotoActionListener getListener() {
1885            return mPhotoEditorListener;
1886        }
1887
1888        @Override
1889        public void startPhotoActivity(Intent intent, int requestCode, Uri photoUri) {
1890            mRawContactIdRequestingPhoto = mEditor.getRawContactId();
1891            mCurrentPhotoHandler = this;
1892            mStatus = Status.SUB_ACTIVITY;
1893            mCurrentPhotoUri = photoUri;
1894            ContactEditorFragment.this.startActivityForResult(intent, requestCode);
1895        }
1896
1897        private final class PhotoEditorListener extends PhotoSelectionHandler.PhotoActionListener
1898                implements EditorListener {
1899
1900            @Override
1901            public void onRequest(int request) {
1902                if (!hasValidState()) return;
1903
1904                if (request == EditorListener.REQUEST_PICK_PHOTO) {
1905                    onClick(mEditor.getPhotoEditor());
1906                }
1907            }
1908
1909            @Override
1910            public void onDeleteRequested(Editor removedEditor) {
1911                // The picture cannot be deleted, it can only be removed, which is handled by
1912                // onRemovePictureChosen()
1913            }
1914
1915            /**
1916             * User has chosen to set the selected photo as the (super) primary photo
1917             */
1918            @Override
1919            public void onUseAsPrimaryChosen() {
1920                // Set the IsSuperPrimary for each editor
1921                int count = mContent.getChildCount();
1922                for (int i = 0; i < count; i++) {
1923                    final View childView = mContent.getChildAt(i);
1924                    if (childView instanceof BaseRawContactEditorView) {
1925                        final BaseRawContactEditorView editor =
1926                                (BaseRawContactEditorView) childView;
1927                        final PhotoEditorView photoEditor = editor.getPhotoEditor();
1928                        photoEditor.setSuperPrimary(editor == mEditor);
1929                    }
1930                }
1931                bindEditors();
1932            }
1933
1934            /**
1935             * User has chosen to remove a picture
1936             */
1937            @Override
1938            public void onRemovePictureChosen() {
1939                mEditor.setPhotoBitmap(null);
1940
1941                // Prevent bitmap from being restored if rotate the device.
1942                // (only if we first chose a new photo before removing it)
1943                mUpdatedPhotos.remove(String.valueOf(mRawContactId));
1944                bindEditors();
1945            }
1946
1947            @Override
1948            public void onPhotoSelected(Uri uri) throws FileNotFoundException {
1949                final Bitmap bitmap = ContactPhotoUtils.getBitmapFromUri(mContext, uri);
1950                setPhoto(mRawContactId, bitmap, uri);
1951                mCurrentPhotoHandler = null;
1952                bindEditors();
1953            }
1954
1955            @Override
1956            public Uri getCurrentPhotoUri() {
1957                return mCurrentPhotoUri;
1958            }
1959
1960            @Override
1961            public void onPhotoSelectionDismissed() {
1962                // Nothing to do.
1963            }
1964        }
1965    }
1966}
1967