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