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