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