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