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