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