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