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