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