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