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