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