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