ContactEditorFragment.java revision 860698b8d4542a55da5ec40032a361433b525aad
1/*
2 * Copyright (C) 2015 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 android.accounts.Account;
20import android.app.Activity;
21import android.app.Fragment;
22import android.app.LoaderManager;
23import android.content.ContentResolver;
24import android.content.ContentUris;
25import android.content.ContentValues;
26import android.content.Context;
27import android.content.CursorLoader;
28import android.content.Intent;
29import android.content.Loader;
30import android.database.Cursor;
31import android.graphics.Bitmap;
32import android.net.Uri;
33import android.os.Bundle;
34import android.os.SystemClock;
35import android.provider.ContactsContract;
36import android.provider.ContactsContract.CommonDataKinds.Email;
37import android.provider.ContactsContract.CommonDataKinds.Event;
38import android.provider.ContactsContract.CommonDataKinds.Organization;
39import android.provider.ContactsContract.CommonDataKinds.Phone;
40import android.provider.ContactsContract.CommonDataKinds.StructuredName;
41import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
42import android.provider.ContactsContract.Intents;
43import android.provider.ContactsContract.RawContacts;
44import android.text.TextUtils;
45import android.util.Log;
46import android.view.LayoutInflater;
47import android.view.Menu;
48import android.view.MenuInflater;
49import android.view.MenuItem;
50import android.view.View;
51import android.view.ViewGroup;
52import android.widget.AdapterView;
53import android.widget.BaseAdapter;
54import android.widget.LinearLayout;
55import android.widget.ListPopupWindow;
56import android.widget.Toast;
57import android.widget.Toolbar;
58
59import com.android.contacts.ContactSaveService;
60import com.android.contacts.GroupMetaDataLoader;
61import com.android.contacts.R;
62import com.android.contacts.activities.ContactEditorAccountsChangedActivity;
63import com.android.contacts.activities.ContactEditorActivity;
64import com.android.contacts.activities.ContactEditorActivity.ContactEditor;
65import com.android.contacts.activities.ContactSelectionActivity;
66import com.android.contacts.common.Experiments;
67import com.android.contacts.common.logging.ScreenEvent.ScreenType;
68import com.android.contacts.common.model.AccountTypeManager;
69import com.android.contacts.common.model.Contact;
70import com.android.contacts.common.model.ContactLoader;
71import com.android.contacts.common.model.RawContact;
72import com.android.contacts.common.model.RawContactDelta;
73import com.android.contacts.common.model.RawContactDeltaList;
74import com.android.contacts.common.model.RawContactModifier;
75import com.android.contacts.common.model.ValuesDelta;
76import com.android.contacts.common.model.account.AccountType;
77import com.android.contacts.common.model.account.AccountWithDataSet;
78import com.android.contacts.common.preference.ContactsPreferences;
79import com.android.contacts.common.util.ContactDisplayUtils;
80import com.android.contacts.common.util.ImplicitIntentsUtil;
81import com.android.contacts.common.util.MaterialColorMapUtils;
82import com.android.contacts.editor.AggregationSuggestionEngine.Suggestion;
83import com.android.contacts.group.GroupUtil;
84import com.android.contacts.list.UiIntentActions;
85import com.android.contacts.quickcontact.InvisibleContactUtil;
86import com.android.contacts.quickcontact.QuickContactActivity;
87import com.android.contacts.util.ContactPhotoUtils;
88import com.android.contacts.util.UiClosables;
89import com.android.contactsbind.HelpUtils;
90import com.android.contactsbind.ObjectFactory;
91import com.android.contactsbind.experiments.Flags;
92
93import com.google.common.collect.ImmutableList;
94import com.google.common.collect.Lists;
95
96import java.io.FileNotFoundException;
97import java.util.ArrayList;
98import java.util.HashSet;
99import java.util.Iterator;
100import java.util.List;
101import java.util.Set;
102
103/**
104 * Contact editor with only the most important fields displayed initially.
105 */
106public class ContactEditorFragment extends Fragment implements
107        ContactEditor, SplitContactConfirmationDialogFragment.Listener,
108        JoinContactConfirmationDialogFragment.Listener,
109        AggregationSuggestionEngine.Listener, AggregationSuggestionView.Listener,
110        CancelEditDialogFragment.Listener,
111        RawContactEditorView.Listener, PhotoEditorView.Listener {
112
113    static final String TAG = "ContactEditor";
114
115    private static final int LOADER_CONTACT = 1;
116    private static final int LOADER_GROUPS = 2;
117
118    private static final String KEY_PHOTO_RAW_CONTACT_ID = "photo_raw_contact_id";
119    private static final String KEY_UPDATED_PHOTOS = "updated_photos";
120
121    private static final List<String> VALID_INTENT_ACTIONS = new ArrayList<String>() {{
122        add(Intent.ACTION_EDIT);
123        add(Intent.ACTION_INSERT);
124        add(ContactEditorActivity.ACTION_SAVE_COMPLETED);
125    }};
126
127    private static final String KEY_ACTION = "action";
128    private static final String KEY_URI = "uri";
129    private static final String KEY_AUTO_ADD_TO_DEFAULT_GROUP = "autoAddToDefaultGroup";
130    private static final String KEY_DISABLE_DELETE_MENU_OPTION = "disableDeleteMenuOption";
131    private static final String KEY_NEW_LOCAL_PROFILE = "newLocalProfile";
132    private static final String KEY_MATERIAL_PALETTE = "materialPalette";
133    private static final String KEY_ACCOUNT = "saveToAccount";
134    private static final String KEY_VIEW_ID_GENERATOR = "viewidgenerator";
135
136    private static final String KEY_RAW_CONTACTS = "rawContacts";
137
138    private static final String KEY_EDIT_STATE = "state";
139    private static final String KEY_STATUS = "status";
140
141    private static final String KEY_HAS_NEW_CONTACT = "hasNewContact";
142    private static final String KEY_NEW_CONTACT_READY = "newContactDataReady";
143
144    private static final String KEY_IS_EDIT = "isEdit";
145    private static final String KEY_EXISTING_CONTACT_READY = "existingContactDataReady";
146
147    private static final String KEY_IS_USER_PROFILE = "isUserProfile";
148
149    private static final String KEY_ENABLED = "enabled";
150
151    // Aggregation PopupWindow
152    private static final String KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID =
153            "aggregationSuggestionsRawContactId";
154
155    // Join Activity
156    private static final String KEY_CONTACT_ID_FOR_JOIN = "contactidforjoin";
157
158    private static final String KEY_READ_ONLY_DISPLAY_NAME_ID = "readOnlyDisplayNameId";
159    private static final String KEY_COPY_READ_ONLY_DISPLAY_NAME = "copyReadOnlyDisplayName";
160
161    protected static final int REQUEST_CODE_JOIN = 0;
162    protected static final int REQUEST_CODE_ACCOUNTS_CHANGED = 1;
163
164    /**
165     * An intent extra that forces the editor to add the edited contact
166     * to the default group (e.g. "My Contacts").
167     */
168    public static final String INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY = "addToDefaultDirectory";
169
170    public static final String INTENT_EXTRA_NEW_LOCAL_PROFILE = "newLocalProfile";
171
172    public static final String INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION =
173            "disableDeleteMenuOption";
174
175    /**
176     * Intent key to pass the photo palette primary color calculated by
177     * {@link com.android.contacts.quickcontact.QuickContactActivity} to the editor.
178     */
179    public static final String INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR =
180            "material_palette_primary_color";
181
182    /**
183     * Intent key to pass the photo palette secondary color calculated by
184     * {@link com.android.contacts.quickcontact.QuickContactActivity} to the editor.
185     */
186    public static final String INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR =
187            "material_palette_secondary_color";
188
189    /**
190     * Intent key to pass the ID of the photo to display on the editor.
191     */
192    // TODO: This can be cleaned up if we decide to not pass the photo id through
193    // QuickContactActivity.
194    public static final String INTENT_EXTRA_PHOTO_ID = "photo_id";
195
196    /**
197     * Intent key to pass the ID of the raw contact id that should be displayed in the full editor
198     * by itself.
199     */
200    public static final String INTENT_EXTRA_RAW_CONTACT_ID_TO_DISPLAY_ALONE =
201            "raw_contact_id_to_display_alone";
202
203    /**
204     * Intent extra to specify a {@link ContactEditor.SaveMode}.
205     */
206    public static final String SAVE_MODE_EXTRA_KEY = "saveMode";
207
208    /**
209     * Intent extra key for the contact ID to join the current contact to after saving.
210     */
211    public static final String JOIN_CONTACT_ID_EXTRA_KEY = "joinContactId";
212
213    /**
214     * Callbacks for Activities that host contact editors Fragments.
215     */
216    public interface Listener {
217
218        /**
219         * Contact was not found, so somehow close this fragment. This is raised after a contact
220         * is removed via Menu/Delete
221         */
222        void onContactNotFound();
223
224        /**
225         * Contact was split, so we can close now.
226         *
227         * @param newLookupUri The lookup uri of the new contact that should be shown to the user.
228         *                     The editor tries best to chose the most natural contact here.
229         */
230        void onContactSplit(Uri newLookupUri);
231
232        /**
233         * User has tapped Revert, close the fragment now.
234         */
235        void onReverted();
236
237        /**
238         * Contact was saved and the Fragment can now be closed safely.
239         */
240        void onSaveFinished(Intent resultIntent);
241
242        /**
243         * User switched to editing a different raw contact (a suggestion from the
244         * aggregation engine).
245         */
246        void onEditOtherRawContactRequested(Uri contactLookupUri, long rawContactId,
247                ArrayList<ContentValues> contentValues);
248
249        /**
250         * User has requested that contact be deleted.
251         */
252        void onDeleteRequested(Uri contactUri);
253    }
254
255    /**
256     * Adapter for aggregation suggestions displayed in a PopupWindow when
257     * editor fields change.
258     */
259    private static final class AggregationSuggestionAdapter extends BaseAdapter {
260        private final LayoutInflater mLayoutInflater;
261        private final AggregationSuggestionView.Listener mListener;
262        private final List<AggregationSuggestionEngine.Suggestion> mSuggestions;
263
264        public AggregationSuggestionAdapter(Activity activity,
265                AggregationSuggestionView.Listener listener, List<Suggestion> suggestions) {
266            mLayoutInflater = activity.getLayoutInflater();
267            mListener = listener;
268            mSuggestions = suggestions;
269        }
270
271        @Override
272        public View getView(int position, View convertView, ViewGroup parent) {
273            final Suggestion suggestion = (Suggestion) getItem(position);
274            final AggregationSuggestionView suggestionView =
275                    (AggregationSuggestionView) mLayoutInflater.inflate(
276                            R.layout.aggregation_suggestions_item, null);
277            suggestionView.setListener(mListener);
278            suggestionView.bindSuggestion(suggestion);
279            return suggestionView;
280        }
281
282        @Override
283        public long getItemId(int position) {
284            return position;
285        }
286
287        @Override
288        public Object getItem(int position) {
289            return mSuggestions.get(position);
290        }
291
292        @Override
293        public int getCount() {
294            return mSuggestions.size();
295        }
296    }
297
298    protected Context mContext;
299    protected Listener mListener;
300
301    //
302    // Views
303    //
304    protected LinearLayout mContent;
305    protected View mAggregationSuggestionView;
306    protected ListPopupWindow mAggregationSuggestionPopup;
307
308    //
309    // Parameters passed in on {@link #load}
310    //
311    protected String mAction;
312    protected Uri mLookupUri;
313    protected Bundle mIntentExtras;
314    protected boolean mAutoAddToDefaultGroup;
315    protected boolean mDisableDeleteMenuOption;
316    protected boolean mNewLocalProfile;
317    protected MaterialColorMapUtils.MaterialPalette mMaterialPalette;
318
319    //
320    // Helpers
321    //
322    protected ContactEditorUtils mEditorUtils;
323    protected RawContactDeltaComparator mComparator;
324    protected ViewIdGenerator mViewIdGenerator;
325    private AggregationSuggestionEngine mAggregationSuggestionEngine;
326
327    //
328    // Loaded data
329    //
330    // Used to store existing contact data so it can be re-applied during a rebind call,
331    // i.e. account switch.
332    protected Contact mContact;
333    protected ImmutableList<RawContact> mRawContacts;
334    protected Cursor mGroupMetaData;
335
336    //
337    // Editor state
338    //
339    protected RawContactDeltaList mState;
340    protected int mStatus;
341    protected long mRawContactIdToDisplayAlone = -1;
342
343    // Whether to show the new contact blank form and if it's corresponding delta is ready.
344    protected boolean mHasNewContact;
345    protected AccountWithDataSet mAccountWithDataSet;
346    protected boolean mNewContactDataReady;
347    protected boolean mNewContactAccountChanged;
348
349    // Whether it's an edit of existing contact and if it's corresponding delta is ready.
350    protected boolean mIsEdit;
351    protected boolean mExistingContactDataReady;
352
353    // Whether we are editing the "me" profile
354    protected boolean mIsUserProfile;
355
356    // Whether editor views and options menu items should be enabled
357    private boolean mEnabled = true;
358
359    // Aggregation PopupWindow
360    private long mAggregationSuggestionsRawContactId;
361
362    // Join Activity
363    protected long mContactIdForJoin;
364
365    // Used to pre-populate the editor with a display name when a user edits a read-only contact.
366    protected long mReadOnlyDisplayNameId;
367    protected boolean mCopyReadOnlyName;
368
369    /**
370     * The contact data loader listener.
371     */
372    protected final LoaderManager.LoaderCallbacks<Contact> mContactLoaderListener =
373            new LoaderManager.LoaderCallbacks<Contact>() {
374
375                protected long mLoaderStartTime;
376
377                @Override
378                public Loader<Contact> onCreateLoader(int id, Bundle args) {
379                    mLoaderStartTime = SystemClock.elapsedRealtime();
380                    return new ContactLoader(mContext, mLookupUri,
381                            /* postViewNotification */ true,
382                            /* loadGroupMetaData */ true);
383                }
384
385                @Override
386                public void onLoadFinished(Loader<Contact> loader, Contact contact) {
387                    final long loaderCurrentTime = SystemClock.elapsedRealtime();
388                    Log.v(TAG, "Time needed for loading: " + (loaderCurrentTime-mLoaderStartTime));
389                    if (!contact.isLoaded()) {
390                        // Item has been deleted. Close activity without saving again.
391                        Log.i(TAG, "No contact found. Closing activity");
392                        mStatus = Status.CLOSING;
393                        if (mListener != null) mListener.onContactNotFound();
394                        return;
395                    }
396
397                    mStatus = Status.EDITING;
398                    mLookupUri = contact.getLookupUri();
399                    final long setDataStartTime = SystemClock.elapsedRealtime();
400                    setState(contact);
401                    final long setDataEndTime = SystemClock.elapsedRealtime();
402
403                    Log.v(TAG, "Time needed for setting UI: " + (setDataEndTime - setDataStartTime));
404                }
405
406                @Override
407                public void onLoaderReset(Loader<Contact> loader) {
408                }
409            };
410
411    /**
412     * The groups meta data loader listener.
413     */
414    protected final LoaderManager.LoaderCallbacks<Cursor> mGroupsLoaderListener =
415            new LoaderManager.LoaderCallbacks<Cursor>() {
416
417                @Override
418                public CursorLoader onCreateLoader(int id, Bundle args) {
419                    return new GroupMetaDataLoader(mContext, ContactsContract.Groups.CONTENT_URI,
420                            GroupUtil.ALL_GROUPS_SELECTION);
421                }
422
423                @Override
424                public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
425                    mGroupMetaData = data;
426                    setGroupMetaData();
427                }
428
429                @Override
430                public void onLoaderReset(Loader<Cursor> loader) {
431                }
432            };
433
434    private long mPhotoRawContactId;
435    private Bundle mUpdatedPhotos = new Bundle();
436
437    @Override
438    public Context getContext() {
439        return getActivity();
440    }
441
442    @Override
443    public void onAttach(Activity activity) {
444        super.onAttach(activity);
445        mContext = activity;
446        mEditorUtils = ContactEditorUtils.create(mContext);
447        mComparator = new RawContactDeltaComparator(mContext);
448    }
449
450    @Override
451    public void onCreate(Bundle savedState) {
452        if (savedState != null) {
453            // Restore mUri before calling super.onCreate so that onInitializeLoaders
454            // would already have a uri and an action to work with
455            mAction = savedState.getString(KEY_ACTION);
456            mLookupUri = savedState.getParcelable(KEY_URI);
457        }
458
459        super.onCreate(savedState);
460
461        if (savedState == null) {
462            mViewIdGenerator = new ViewIdGenerator();
463
464            // mState can still be null because it may not have have finished loading before
465            // onSaveInstanceState was called.
466            mState = new RawContactDeltaList();
467        } else {
468            mViewIdGenerator = savedState.getParcelable(KEY_VIEW_ID_GENERATOR);
469
470            mAutoAddToDefaultGroup = savedState.getBoolean(KEY_AUTO_ADD_TO_DEFAULT_GROUP);
471            mDisableDeleteMenuOption = savedState.getBoolean(KEY_DISABLE_DELETE_MENU_OPTION);
472            mNewLocalProfile = savedState.getBoolean(KEY_NEW_LOCAL_PROFILE);
473            mMaterialPalette = savedState.getParcelable(KEY_MATERIAL_PALETTE);
474            mAccountWithDataSet = savedState.getParcelable(KEY_ACCOUNT);
475            mRawContacts = ImmutableList.copyOf(savedState.<RawContact>getParcelableArrayList(
476                    KEY_RAW_CONTACTS));
477            // NOTE: mGroupMetaData is not saved/restored
478
479            // Read state from savedState. No loading involved here
480            mState = savedState.<RawContactDeltaList> getParcelable(KEY_EDIT_STATE);
481            mStatus = savedState.getInt(KEY_STATUS);
482
483            mHasNewContact = savedState.getBoolean(KEY_HAS_NEW_CONTACT);
484            mNewContactDataReady = savedState.getBoolean(KEY_NEW_CONTACT_READY);
485
486            mIsEdit = savedState.getBoolean(KEY_IS_EDIT);
487            mExistingContactDataReady = savedState.getBoolean(KEY_EXISTING_CONTACT_READY);
488
489            mIsUserProfile = savedState.getBoolean(KEY_IS_USER_PROFILE);
490
491            mEnabled = savedState.getBoolean(KEY_ENABLED);
492
493            // Aggregation PopupWindow
494            mAggregationSuggestionsRawContactId = savedState.getLong(
495                    KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID);
496
497            // Join Activity
498            mContactIdForJoin = savedState.getLong(KEY_CONTACT_ID_FOR_JOIN);
499
500            mReadOnlyDisplayNameId = savedState.getLong(KEY_READ_ONLY_DISPLAY_NAME_ID);
501            mCopyReadOnlyName = savedState.getBoolean(KEY_COPY_READ_ONLY_DISPLAY_NAME, false);
502
503            mPhotoRawContactId = savedState.getLong(KEY_PHOTO_RAW_CONTACT_ID);
504            mUpdatedPhotos = savedState.getParcelable(KEY_UPDATED_PHOTOS);
505        }
506    }
507
508    @Override
509    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
510        setHasOptionsMenu(true);
511
512        final View view = inflater.inflate(
513                R.layout.contact_editor_fragment, container, false);
514        mContent = (LinearLayout) view.findViewById(R.id.raw_contacts_editor_view);
515        return view;
516    }
517
518    @Override
519    public void onActivityCreated(Bundle savedInstanceState) {
520        super.onActivityCreated(savedInstanceState);
521
522        validateAction(mAction);
523
524        if (mState.isEmpty()) {
525            // The delta list may not have finished loading before orientation change happens.
526            // In this case, there will be a saved state but deltas will be missing.  Reload from
527            // database.
528            if (Intent.ACTION_EDIT.equals(mAction)) {
529                // Either
530                // 1) orientation change but load never finished.
531                // 2) not an orientation change so data needs to be loaded for first time.
532                getLoaderManager().initLoader(LOADER_CONTACT, null, mContactLoaderListener);
533                getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupsLoaderListener);
534            }
535        } else {
536            // Orientation change, we already have mState, it was loaded by onCreate
537            bindEditors();
538        }
539
540        // Handle initial actions only when existing state missing
541        if (savedInstanceState == null) {
542            final Account account = mIntentExtras == null ? null :
543                    (Account) mIntentExtras.getParcelable(Intents.Insert.EXTRA_ACCOUNT);
544            final String dataSet = mIntentExtras == null ? null :
545                    mIntentExtras.getString(Intents.Insert.EXTRA_DATA_SET);
546            if (account != null) {
547                mAccountWithDataSet = new AccountWithDataSet(account.name, account.type, dataSet);
548            }
549
550            if (Intent.ACTION_EDIT.equals(mAction)) {
551                mIsEdit = true;
552            } else if (Intent.ACTION_INSERT.equals(mAction)) {
553                mHasNewContact = true;
554                if (mAccountWithDataSet != null) {
555                    createContact(mAccountWithDataSet);
556                } else if (mIntentExtras != null && mIntentExtras.getBoolean(
557                        ContactEditorActivity.EXTRA_SAVE_TO_DEVICE_FLAG, false)) {
558                    createContact(null);
559                } else {
560                    // No Account specified. Let the user choose
561                    // Load Accounts async so that we can present them
562                    selectAccountAndCreateContact();
563                }
564            }
565        }
566    }
567
568    /**
569     * Checks if the requested action is valid.
570     *
571     * @param action The action to test.
572     * @throws IllegalArgumentException when the action is invalid.
573     */
574    private static void validateAction(String action) {
575        if (VALID_INTENT_ACTIONS.contains(action)) {
576            return;
577        }
578        throw new IllegalArgumentException(
579                "Unknown action " + action + "; Supported actions: " + VALID_INTENT_ACTIONS);
580    }
581
582    @Override
583    public void onSaveInstanceState(Bundle outState) {
584        outState.putString(KEY_ACTION, mAction);
585        outState.putParcelable(KEY_URI, mLookupUri);
586        outState.putBoolean(KEY_AUTO_ADD_TO_DEFAULT_GROUP, mAutoAddToDefaultGroup);
587        outState.putBoolean(KEY_DISABLE_DELETE_MENU_OPTION, mDisableDeleteMenuOption);
588        outState.putBoolean(KEY_NEW_LOCAL_PROFILE, mNewLocalProfile);
589        if (mMaterialPalette != null) {
590            outState.putParcelable(KEY_MATERIAL_PALETTE, mMaterialPalette);
591        }
592        outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator);
593
594        outState.putParcelableArrayList(KEY_RAW_CONTACTS, mRawContacts == null ?
595                Lists.<RawContact>newArrayList() : Lists.newArrayList(mRawContacts));
596        // NOTE: mGroupMetaData is not saved
597
598        outState.putParcelable(KEY_EDIT_STATE, mState);
599        outState.putInt(KEY_STATUS, mStatus);
600        outState.putBoolean(KEY_HAS_NEW_CONTACT, mHasNewContact);
601        outState.putBoolean(KEY_NEW_CONTACT_READY, mNewContactDataReady);
602        outState.putBoolean(KEY_IS_EDIT, mIsEdit);
603        outState.putBoolean(KEY_EXISTING_CONTACT_READY, mExistingContactDataReady);
604        outState.putParcelable(KEY_ACCOUNT, mAccountWithDataSet);
605        outState.putBoolean(KEY_IS_USER_PROFILE, mIsUserProfile);
606
607        outState.putBoolean(KEY_ENABLED, mEnabled);
608
609        // Aggregation PopupWindow
610        outState.putLong(KEY_AGGREGATION_SUGGESTIONS_RAW_CONTACT_ID,
611                mAggregationSuggestionsRawContactId);
612
613        // Join Activity
614        outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin);
615
616        outState.putLong(KEY_READ_ONLY_DISPLAY_NAME_ID, mReadOnlyDisplayNameId);
617        outState.putBoolean(KEY_COPY_READ_ONLY_DISPLAY_NAME, mCopyReadOnlyName);
618
619        outState.putLong(KEY_PHOTO_RAW_CONTACT_ID, mPhotoRawContactId);
620        outState.putParcelable(KEY_UPDATED_PHOTOS, mUpdatedPhotos);
621        super.onSaveInstanceState(outState);
622    }
623
624    @Override
625    public void onStop() {
626        super.onStop();
627        UiClosables.closeQuietly(mAggregationSuggestionPopup);
628    }
629
630    @Override
631    public void onDestroy() {
632        super.onDestroy();
633        if (mAggregationSuggestionEngine != null) {
634            mAggregationSuggestionEngine.quit();
635        }
636    }
637
638    @Override
639    public void onActivityResult(int requestCode, int resultCode, Intent data) {
640        switch (requestCode) {
641            case REQUEST_CODE_JOIN: {
642                // Ignore failed requests
643                if (resultCode != Activity.RESULT_OK) return;
644                if (data != null) {
645                    final long contactId = ContentUris.parseId(data.getData());
646                    if (hasPendingChanges()) {
647                        // Ask the user if they want to save changes before doing the join
648                        JoinContactConfirmationDialogFragment.show(this, contactId);
649                    } else {
650                        // Do the join immediately
651                        joinAggregate(contactId);
652                    }
653                }
654                break;
655            }
656            case REQUEST_CODE_ACCOUNTS_CHANGED: {
657                // Bail if the account selector was not successful.
658                if (resultCode != Activity.RESULT_OK) {
659                    if (mListener != null) {
660                        mListener.onReverted();
661                    }
662                    return;
663                }
664                // If there's an account specified, use it.
665                if (data != null) {
666                    AccountWithDataSet account = data.getParcelableExtra(
667                            Intents.Insert.EXTRA_ACCOUNT);
668                    if (account != null) {
669                        createContact(account);
670                        return;
671                    }
672                }
673                // If there isn't an account specified, then this is likely a phone-local
674                // contact, so we should continue setting up the editor by automatically selecting
675                // the most appropriate account.
676                createContact();
677                break;
678            }
679        }
680    }
681
682    //
683    // Options menu
684    //
685
686    @Override
687    public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
688        inflater.inflate(R.menu.edit_contact, menu);
689    }
690
691    @Override
692    public void onPrepareOptionsMenu(Menu menu) {
693        // This supports the keyboard shortcut to save changes to a contact but shouldn't be visible
694        // because the custom action bar contains the "save" button now (not the overflow menu).
695        // TODO: Find a better way to handle shortcuts, i.e. onKeyDown()?
696        final MenuItem saveMenu = menu.findItem(R.id.menu_save);
697        final MenuItem splitMenu = menu.findItem(R.id.menu_split);
698        final MenuItem joinMenu = menu.findItem(R.id.menu_join);
699        final MenuItem deleteMenu = menu.findItem(R.id.menu_delete);
700
701        // TODO: b/30771904, b/31827701, temporarily disable these items until we get them to work
702        // on a raw contact level.
703        joinMenu.setVisible(false);
704        splitMenu.setVisible(false);
705        deleteMenu.setVisible(false);
706        // Save menu is invisible when there's only one read only contact in the editor.
707        saveMenu.setVisible(!isEditingReadOnlyRawContact());
708        if (saveMenu.isVisible()) {
709            // Since we're using a custom action layout we have to manually hook up the handler.
710            saveMenu.getActionView().setOnClickListener(new View.OnClickListener() {
711                @Override
712                public void onClick(View v) {
713                    onOptionsItemSelected(saveMenu);
714                }
715            });
716        }
717
718        int size = menu.size();
719        for (int i = 0; i < size; i++) {
720            menu.getItem(i).setEnabled(mEnabled);
721        }
722    }
723
724    @Override
725    public boolean onOptionsItemSelected(MenuItem item) {
726        if (item.getItemId() == android.R.id.home) {
727            return revert();
728        }
729
730        final Activity activity = getActivity();
731        if (activity == null || activity.isFinishing() || activity.isDestroyed()) {
732            // If we no longer are attached to a running activity want to
733            // drain this event.
734            return true;
735        }
736
737        switch (item.getItemId()) {
738            case R.id.menu_save:
739                return save(SaveMode.CLOSE);
740            case R.id.menu_delete:
741                if (mListener != null) mListener.onDeleteRequested(mLookupUri);
742                return true;
743            case R.id.menu_split:
744                return doSplitContactAction();
745            case R.id.menu_join:
746                return doJoinContactAction();
747            case R.id.menu_help:
748                HelpUtils.launchHelpAndFeedbackForContactScreen(getActivity());
749                return true;
750        }
751
752        return false;
753    }
754
755    @Override
756    public boolean revert() {
757        if (mState.isEmpty() || !hasPendingChanges()) {
758            onCancelEditConfirmed();
759        } else {
760            CancelEditDialogFragment.show(this);
761        }
762        return true;
763    }
764
765    @Override
766    public void onCancelEditConfirmed() {
767        // When this Fragment is closed we don't want it to auto-save
768        mStatus = Status.CLOSING;
769        if (mListener != null) {
770            mListener.onReverted();
771        }
772    }
773
774    @Override
775    public void onSplitContactConfirmed(boolean hasPendingChanges) {
776        if (mState.isEmpty()) {
777            // This may happen when this Fragment is recreated by the system during users
778            // confirming the split action (and thus this method is called just before onCreate()),
779            // for example.
780            Log.e(TAG, "mState became null during the user's confirming split action. " +
781                    "Cannot perform the save action.");
782            return;
783        }
784
785        if (!hasPendingChanges && mHasNewContact) {
786            // If the user didn't add anything new, we don't want to split out the newly created
787            // raw contact into a name-only contact so remove them.
788            final Iterator<RawContactDelta> iterator = mState.iterator();
789            while (iterator.hasNext()) {
790                final RawContactDelta rawContactDelta = iterator.next();
791                if (rawContactDelta.getRawContactId() < 0) {
792                    iterator.remove();
793                }
794            }
795        }
796        mState.markRawContactsForSplitting();
797        save(SaveMode.SPLIT);
798    }
799
800    @Override
801    public void onSplitContactCanceled() {}
802
803    private boolean doSplitContactAction() {
804        if (!hasValidState()) return false;
805
806        SplitContactConfirmationDialogFragment.show(this, hasPendingChanges());
807        return true;
808    }
809
810    private boolean doJoinContactAction() {
811        if (!hasValidState() || mLookupUri == null) {
812            return false;
813        }
814
815        // If we just started creating a new contact and haven't added any data, it's too
816        // early to do a join
817        if (mState.size() == 1 && mState.get(0).isContactInsert()
818                && !hasPendingChanges()) {
819            Toast.makeText(mContext, R.string.toast_join_with_empty_contact,
820                    Toast.LENGTH_LONG).show();
821            return true;
822        }
823
824        showJoinAggregateActivity(mLookupUri);
825        return true;
826    }
827
828    @Override
829    public void onJoinContactConfirmed(long joinContactId) {
830        doSaveAction(SaveMode.JOIN, joinContactId);
831    }
832
833    @Override
834    public boolean save(int saveMode) {
835        if (!hasValidState() || mStatus != Status.EDITING) {
836            return false;
837        }
838
839        // If we are about to close the editor - there is no need to refresh the data
840        if (saveMode == SaveMode.CLOSE || saveMode == SaveMode.EDITOR
841                || saveMode == SaveMode.SPLIT) {
842            getLoaderManager().destroyLoader(LOADER_CONTACT);
843        }
844
845        mStatus = Status.SAVING;
846
847        if (!hasPendingChanges()) {
848            if (mLookupUri == null && saveMode == SaveMode.RELOAD) {
849                // We don't have anything to save and there isn't even an existing contact yet.
850                // Nothing to do, simply go back to editing mode
851                mStatus = Status.EDITING;
852                return true;
853            }
854            onSaveCompleted(/* hadChanges =*/ false, saveMode,
855                    /* saveSucceeded =*/ mLookupUri != null, mLookupUri, /* joinContactId =*/ null);
856            return true;
857        }
858
859        setEnabled(false);
860
861        return doSaveAction(saveMode, /* joinContactId */ null);
862    }
863
864    //
865    // State accessor methods
866    //
867
868    /**
869     * Check if our internal {@link #mState} is valid, usually checked before
870     * performing user actions.
871     */
872    private boolean hasValidState() {
873        return mState.size() > 0;
874    }
875
876    private boolean isEditingUserProfile() {
877        return mNewLocalProfile || mIsUserProfile;
878    }
879
880    /**
881     * Whether the contact being edited is composed of read-only raw contacts
882     * aggregated with a newly created writable raw contact.
883     */
884    private boolean isEditingReadOnlyRawContactWithNewContact() {
885        return mHasNewContact && mState.size() > 1;
886    }
887
888    /**
889     * @return true if the single raw contact we're looking at is read-only.
890     */
891    private boolean isEditingReadOnlyRawContact() {
892        return hasValidState() && mRawContactIdToDisplayAlone > 0
893                && !mState.getByRawContactId(mRawContactIdToDisplayAlone)
894                        .getAccountType(AccountTypeManager.getInstance(mContext))
895                                .areContactsWritable();
896    }
897
898    /**
899     * Return true if there are any edits to the current contact which need to
900     * be saved.
901     */
902    private boolean hasPendingRawContactChanges(Set<String> excludedMimeTypes) {
903        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
904        return RawContactModifier.hasChanges(mState, accountTypes, excludedMimeTypes);
905    }
906
907    /**
908     * Determines if changes were made in the editor that need to be saved, while taking into
909     * account that name changes are not real for read-only contacts.
910     * See go/editing-read-only-contacts
911     */
912    private boolean hasPendingChanges() {
913        if (isEditingReadOnlyRawContactWithNewContact()) {
914            // We created a new raw contact delta with a default display name.
915            // We must test for pending changes while ignoring the default display name.
916            final ValuesDelta beforeDelta = mState.getByRawContactId(mReadOnlyDisplayNameId)
917                    .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
918            final ValuesDelta pendingDelta = mState
919                    .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
920            if (structuredNamesAreEqual(beforeDelta, pendingDelta)) {
921                final Set<String> excludedMimeTypes = new HashSet<>();
922                excludedMimeTypes.add(StructuredName.CONTENT_ITEM_TYPE);
923                return hasPendingRawContactChanges(excludedMimeTypes);
924            }
925            return true;
926        }
927        return hasPendingRawContactChanges(/* excludedMimeTypes =*/ null);
928    }
929
930    /**
931     * Compares the two {@link ValuesDelta} to see if the structured name is changed. We made a copy
932     * of a read only delta and now we want to check if the copied delta has changes.
933     *
934     * @param before original {@link ValuesDelta}
935     * @param after copied {@link ValuesDelta}
936     * @return true if the copied {@link ValuesDelta} has all the same values in the structured
937     * name fields as the original.
938     */
939    private boolean structuredNamesAreEqual(ValuesDelta before, ValuesDelta after) {
940        if (before == after) return true;
941        if (before == null || after == null) return false;
942        final ContentValues original = before.getBefore();
943        final ContentValues pending = after.getAfter();
944        if (original != null && pending != null) {
945            final String beforeDisplayName = original.getAsString(StructuredName.DISPLAY_NAME);
946            final String afterDisplayName = pending.getAsString(StructuredName.DISPLAY_NAME);
947            if (!TextUtils.equals(beforeDisplayName, afterDisplayName)) return false;
948
949            final String beforePrefix = original.getAsString(StructuredName.PREFIX);
950            final String afterPrefix = pending.getAsString(StructuredName.PREFIX);
951            if (!TextUtils.equals(beforePrefix, afterPrefix)) return false;
952
953            final String beforeFirstName = original.getAsString(StructuredName.GIVEN_NAME);
954            final String afterFirstName = pending.getAsString(StructuredName.GIVEN_NAME);
955            if (!TextUtils.equals(beforeFirstName, afterFirstName)) return false;
956
957            final String beforeMiddleName = original.getAsString(StructuredName.MIDDLE_NAME);
958            final String afterMiddleName = pending.getAsString(StructuredName.MIDDLE_NAME);
959            if (!TextUtils.equals(beforeMiddleName, afterMiddleName)) return false;
960
961            final String beforeLastName = original.getAsString(StructuredName.FAMILY_NAME);
962            final String afterLastName = pending.getAsString(StructuredName.FAMILY_NAME);
963            if (!TextUtils.equals(beforeLastName, afterLastName)) return false;
964
965            final String beforeSuffix = original.getAsString(StructuredName.SUFFIX);
966            final String afterSuffix = pending.getAsString(StructuredName.SUFFIX);
967            return TextUtils.equals(beforeSuffix, afterSuffix);
968        }
969        return false;
970    }
971
972    //
973    // Account creation
974    //
975
976    private void selectAccountAndCreateContact() {
977        // If this is a local profile, then skip the logic about showing the accounts changed
978        // activity and create a phone-local contact.
979        if (mNewLocalProfile) {
980            createContact(null);
981            return;
982        }
983
984        // If there is no default account or the accounts have changed such that we need to
985        // prompt the user again, then launch the account prompt.
986        if (mEditorUtils.shouldShowAccountChangedNotification()) {
987            Intent intent = new Intent(mContext, ContactEditorAccountsChangedActivity.class);
988            // Prevent a second instance from being started on rotates
989            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
990            mStatus = Status.SUB_ACTIVITY;
991            startActivityForResult(intent, REQUEST_CODE_ACCOUNTS_CHANGED);
992        } else {
993            // Make sure the default account is automatically set if there is only one non-device
994            // account.
995            mEditorUtils.maybeUpdateDefaultAccount();
996            // Otherwise, there should be a default account. Then either create a local contact
997            // (if default account is null) or create a contact with the specified account.
998            AccountWithDataSet defaultAccount = mEditorUtils.getOnlyOrDefaultAccount();
999            createContact(defaultAccount);
1000        }
1001    }
1002
1003    /**
1004     * Create a contact by automatically selecting the first account. If there's no available
1005     * account, a device-local contact should be created.
1006     */
1007    private void createContact() {
1008        final List<AccountWithDataSet> accounts =
1009                AccountTypeManager.getInstance(mContext).getAccounts(true);
1010        // No Accounts available. Create a phone-local contact.
1011        if (accounts.isEmpty()) {
1012            createContact(null);
1013            return;
1014        }
1015
1016        // We have an account switcher in "create-account" screen, so don't need to ask a user to
1017        // select an account here.
1018        createContact(accounts.get(0));
1019    }
1020
1021    /**
1022     * Shows account creation screen associated with a given account.
1023     *
1024     * @param account may be null to signal a device-local contact should be created.
1025     */
1026    private void createContact(AccountWithDataSet account) {
1027        final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
1028        final AccountType accountType = accountTypes.getAccountTypeForAccount(account);
1029
1030        setStateForNewContact(account, accountType, isEditingUserProfile());
1031    }
1032
1033    //
1034    // Data binding
1035    //
1036
1037    private void setState(Contact contact) {
1038        // If we have already loaded data, we do not want to change it here to not confuse the user
1039        if (!mState.isEmpty()) {
1040            Log.v(TAG, "Ignoring background change. This will have to be rebased later");
1041            return;
1042        }
1043        mContact = contact;
1044        mRawContacts = contact.getRawContacts();
1045
1046        // Check for writable raw contacts.  If there are none, then we need to create one so user
1047        // can edit.  For the user profile case, there is already an editable contact.
1048        if (!contact.isUserProfile() && !contact.isWritableContact(mContext)) {
1049            mHasNewContact = true;
1050            mReadOnlyDisplayNameId = contact.getNameRawContactId();
1051            mCopyReadOnlyName = true;
1052            // This is potentially an asynchronous call and will add deltas to list.
1053            selectAccountAndCreateContact();
1054        } else {
1055            mHasNewContact = false;
1056        }
1057
1058        setStateForExistingContact(contact.isUserProfile(), mRawContacts);
1059        if (mAutoAddToDefaultGroup
1060                && InvisibleContactUtil.isInvisibleAndAddable(contact, getContext())) {
1061            InvisibleContactUtil.markAddToDefaultGroup(contact, mState, getContext());
1062        }
1063    }
1064
1065    /**
1066     * Prepare {@link #mState} for a newly created phone-local contact.
1067     */
1068    private void setStateForNewContact(AccountWithDataSet account, AccountType accountType,
1069            boolean isUserProfile) {
1070        setStateForNewContact(account, accountType, /* oldState =*/ null,
1071                /* oldAccountType =*/ null, isUserProfile);
1072    }
1073
1074    /**
1075     * Prepare {@link #mState} for a newly created phone-local contact, migrating the state
1076     * specified by oldState and oldAccountType.
1077     */
1078    private void setStateForNewContact(AccountWithDataSet account, AccountType accountType,
1079            RawContactDelta oldState, AccountType oldAccountType, boolean isUserProfile) {
1080        mStatus = Status.EDITING;
1081        mState.add(createNewRawContactDelta(account, accountType, oldState, oldAccountType));
1082        mIsUserProfile = isUserProfile;
1083        mNewContactDataReady = true;
1084        bindEditors();
1085    }
1086
1087    /**
1088     * Returns a {@link RawContactDelta} for a new contact suitable for addition into
1089     * {@link #mState}.
1090     *
1091     * If oldState and oldAccountType are specified, the state specified by those parameters
1092     * is migrated to the result {@link RawContactDelta}.
1093     */
1094    private RawContactDelta createNewRawContactDelta(AccountWithDataSet account,
1095            AccountType accountType, RawContactDelta oldState, AccountType oldAccountType) {
1096        final RawContact rawContact = new RawContact();
1097        if (account != null) {
1098            rawContact.setAccount(account);
1099        } else {
1100            rawContact.setAccountToLocal();
1101        }
1102
1103        final RawContactDelta result = new RawContactDelta(
1104                ValuesDelta.fromAfter(rawContact.getValues()));
1105        if (oldState == null) {
1106            // Parse any values from incoming intent
1107            RawContactModifier.parseExtras(mContext, accountType, result, mIntentExtras);
1108        } else {
1109            RawContactModifier.migrateStateForNewContact(
1110                    mContext, oldState, result, oldAccountType, accountType);
1111        }
1112
1113        // Ensure we have some default fields (if the account type does not support a field,
1114        // ensureKind will not add it, so it is safe to add e.g. Event)
1115        RawContactModifier.ensureKindExists(result, accountType, StructuredName.CONTENT_ITEM_TYPE);
1116        RawContactModifier.ensureKindExists(result, accountType, Phone.CONTENT_ITEM_TYPE);
1117        RawContactModifier.ensureKindExists(result, accountType, Email.CONTENT_ITEM_TYPE);
1118        RawContactModifier.ensureKindExists(result, accountType, Organization.CONTENT_ITEM_TYPE);
1119        RawContactModifier.ensureKindExists(result, accountType, Event.CONTENT_ITEM_TYPE);
1120        RawContactModifier.ensureKindExists(result, accountType,
1121                StructuredPostal.CONTENT_ITEM_TYPE);
1122
1123        // Set the correct URI for saving the contact as a profile
1124        if (mNewLocalProfile) {
1125            result.setProfileQueryUri();
1126        }
1127
1128        return result;
1129    }
1130
1131    /**
1132     * Prepare {@link #mState} for an existing contact.
1133     */
1134    private void setStateForExistingContact(boolean isUserProfile,
1135            ImmutableList<RawContact> rawContacts) {
1136        setEnabled(true);
1137
1138        mState.addAll(rawContacts.iterator());
1139        setIntentExtras(mIntentExtras);
1140        mIntentExtras = null;
1141
1142        // For user profile, change the contacts query URI
1143        mIsUserProfile = isUserProfile;
1144        boolean localProfileExists = false;
1145
1146        if (mIsUserProfile) {
1147            for (RawContactDelta rawContactDelta : mState) {
1148                // For profile contacts, we need a different query URI
1149                rawContactDelta.setProfileQueryUri();
1150                // Try to find a local profile contact
1151                if (rawContactDelta.getValues().getAsString(RawContacts.ACCOUNT_TYPE) == null) {
1152                    localProfileExists = true;
1153                }
1154            }
1155            // Editor should always present a local profile for editing
1156            // TODO(wjang): Need to figure out when this case comes up.  We can't do this if we're
1157            // going to prune all but the one raw contact that we're trying to display by itself.
1158            if (!localProfileExists && mRawContactIdToDisplayAlone <= 0) {
1159                mState.add(createLocalRawContactDelta());
1160            }
1161        }
1162        mExistingContactDataReady = true;
1163        bindEditors();
1164    }
1165
1166    /**
1167     * Set the enabled state of editors.
1168     */
1169    private void setEnabled(boolean enabled) {
1170        if (mEnabled != enabled) {
1171            mEnabled = enabled;
1172
1173            // Enable/disable editors
1174            if (mContent != null) {
1175                int count = mContent.getChildCount();
1176                for (int i = 0; i < count; i++) {
1177                    mContent.getChildAt(i).setEnabled(enabled);
1178                }
1179            }
1180
1181            // Enable/disable aggregation suggestion vies
1182            if (mAggregationSuggestionView != null) {
1183                LinearLayout itemList = (LinearLayout) mAggregationSuggestionView.findViewById(
1184                        R.id.aggregation_suggestions);
1185                int count = itemList.getChildCount();
1186                for (int i = 0; i < count; i++) {
1187                    itemList.getChildAt(i).setEnabled(enabled);
1188                }
1189            }
1190
1191            // Maybe invalidate the options menu
1192            final Activity activity = getActivity();
1193            if (activity != null) activity.invalidateOptionsMenu();
1194        }
1195    }
1196
1197    /**
1198     * Returns a {@link RawContactDelta} for a local contact suitable for addition into
1199     * {@link #mState}.
1200     */
1201    private static RawContactDelta createLocalRawContactDelta() {
1202        final RawContact rawContact = new RawContact();
1203        rawContact.setAccountToLocal();
1204
1205        final RawContactDelta result = new RawContactDelta(
1206                ValuesDelta.fromAfter(rawContact.getValues()));
1207        result.setProfileQueryUri();
1208
1209        return result;
1210    }
1211
1212    private void copyReadOnlyName() {
1213        // We should only ever be doing this if we're creating a new writable contact to attach to
1214        // a read only contact.
1215        if (!isEditingReadOnlyRawContactWithNewContact()) {
1216            return;
1217        }
1218        final int writableIndex = mState.indexOfFirstWritableRawContact(getContext());
1219        final RawContactDelta writable = mState.get(writableIndex);
1220        final RawContactDelta readOnly = mState.getByRawContactId(mContact.getNameRawContactId());
1221        final ValuesDelta writeNameDelta = writable
1222                .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
1223        final ValuesDelta readNameDelta = readOnly
1224                .getSuperPrimaryEntry(StructuredName.CONTENT_ITEM_TYPE);
1225        mCopyReadOnlyName = false;
1226        if (writeNameDelta == null || readNameDelta == null) {
1227            return;
1228        }
1229        writeNameDelta.copyStructuredNameFieldsFrom(readNameDelta);
1230    }
1231
1232    /**
1233     * Bind editors using {@link #mState} and other members initialized from the loaded (or new)
1234     * Contact.
1235     */
1236    protected void bindEditors() {
1237        if (!isReadyToBindEditors()) {
1238            return;
1239        }
1240
1241        // Add input fields for the loaded Contact
1242        final RawContactEditorView editorView = getContent();
1243        editorView.setListener(this);
1244        if (mCopyReadOnlyName) {
1245            copyReadOnlyName();
1246        }
1247        editorView.setState(mState, mMaterialPalette, mViewIdGenerator,
1248                mHasNewContact, mIsUserProfile, mAccountWithDataSet,
1249                mRawContactIdToDisplayAlone);
1250        if (isEditingReadOnlyRawContact()) {
1251            final Toolbar toolbar = getEditorActivity().getToolbar();
1252            if (toolbar != null) {
1253                toolbar.setTitle(R.string.contact_editor_title_read_only_contact);
1254                // Set activity title for Talkback
1255                getEditorActivity().setTitle(R.string.contact_editor_title_read_only_contact);
1256                toolbar.setNavigationIcon(R.drawable.ic_back_arrow);
1257                toolbar.setNavigationContentDescription(R.string.back_arrow_content_description);
1258            }
1259        }
1260
1261        // Set up the photo widget
1262        editorView.setPhotoListener(this);
1263        mPhotoRawContactId = editorView.getPhotoRawContactId();
1264        // If there is an updated full resolution photo apply it now, this will be the case if
1265        // the user selects or takes a new photo, then rotates the device.
1266        final Uri uri = (Uri) mUpdatedPhotos.get(String.valueOf(mPhotoRawContactId));
1267        if (uri != null) {
1268            editorView.setFullSizePhoto(uri);
1269        }
1270
1271        // The editor is ready now so make it visible
1272        editorView.setEnabled(mEnabled);
1273        editorView.setVisibility(View.VISIBLE);
1274
1275        // Refresh the ActionBar as the visibility of the join command
1276        // Activity can be null if we have been detached from the Activity.
1277        invalidateOptionsMenu();
1278    }
1279
1280    /**
1281     * Invalidates the options menu if we are still associated with an Activity.
1282     */
1283    private void invalidateOptionsMenu() {
1284        final Activity activity = getActivity();
1285        if (activity != null) {
1286            activity.invalidateOptionsMenu();
1287        }
1288    }
1289
1290    private boolean isReadyToBindEditors() {
1291        if (mState.isEmpty()) {
1292            if (Log.isLoggable(TAG, Log.VERBOSE)) {
1293                Log.v(TAG, "No data to bind editors");
1294            }
1295            return false;
1296        }
1297        if (mIsEdit && !mExistingContactDataReady) {
1298            if (Log.isLoggable(TAG, Log.VERBOSE)) {
1299                Log.v(TAG, "Existing contact data is not ready to bind editors.");
1300            }
1301            return false;
1302        }
1303        if (mHasNewContact && !mNewContactDataReady) {
1304            if (Log.isLoggable(TAG, Log.VERBOSE)) {
1305                Log.v(TAG, "New contact data is not ready to bind editors.");
1306            }
1307            return false;
1308        }
1309        return true;
1310    }
1311
1312    /**
1313     * Removes a current editor ({@link #mState}) and rebinds new editor for a new account.
1314     * Some of old data are reused with new restriction enforced by the new account.
1315     *
1316     * @param oldState Old data being edited.
1317     * @param oldAccount Old account associated with oldState.
1318     * @param newAccount New account to be used.
1319     */
1320    private void rebindEditorsForNewContact(
1321            RawContactDelta oldState, AccountWithDataSet oldAccount,
1322            AccountWithDataSet newAccount) {
1323        AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
1324        AccountType oldAccountType = accountTypes.getAccountTypeForAccount(oldAccount);
1325        AccountType newAccountType = accountTypes.getAccountTypeForAccount(newAccount);
1326
1327        mExistingContactDataReady = false;
1328        mNewContactDataReady = false;
1329        mState = new RawContactDeltaList();
1330        setStateForNewContact(newAccount, newAccountType, oldState, oldAccountType,
1331                isEditingUserProfile());
1332        if (mIsEdit) {
1333            setStateForExistingContact(isEditingUserProfile(), mRawContacts);
1334        }
1335    }
1336
1337    //
1338    // ContactEditor
1339    //
1340
1341    @Override
1342    public void setListener(Listener listener) {
1343        mListener = listener;
1344    }
1345
1346    @Override
1347    public void load(String action, Uri lookupUri, Bundle intentExtras) {
1348        mAction = action;
1349        mLookupUri = lookupUri;
1350        mIntentExtras = intentExtras;
1351
1352        if (mIntentExtras != null) {
1353            mAutoAddToDefaultGroup =
1354                    mIntentExtras.containsKey(INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY);
1355            mNewLocalProfile =
1356                    mIntentExtras.getBoolean(INTENT_EXTRA_NEW_LOCAL_PROFILE);
1357            mDisableDeleteMenuOption =
1358                    mIntentExtras.getBoolean(INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION);
1359            if (mIntentExtras.containsKey(INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR)
1360                    && mIntentExtras.containsKey(INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR)) {
1361                mMaterialPalette = new MaterialColorMapUtils.MaterialPalette(
1362                        mIntentExtras.getInt(INTENT_EXTRA_MATERIAL_PALETTE_PRIMARY_COLOR),
1363                        mIntentExtras.getInt(INTENT_EXTRA_MATERIAL_PALETTE_SECONDARY_COLOR));
1364            }
1365            mRawContactIdToDisplayAlone = mIntentExtras
1366                    .getLong(INTENT_EXTRA_RAW_CONTACT_ID_TO_DISPLAY_ALONE);
1367        }
1368    }
1369
1370    @Override
1371    public void setIntentExtras(Bundle extras) {
1372        getContent().setIntentExtras(extras);
1373    }
1374
1375    @Override
1376    public void onJoinCompleted(Uri uri) {
1377        onSaveCompleted(false, SaveMode.RELOAD, uri != null, uri, /* joinContactId */ null);
1378    }
1379
1380
1381    private String getNameToDisplay(Uri contactUri) {
1382        // The contact has been deleted or the uri is otherwise no longer right.
1383        if (contactUri == null) {
1384            return null;
1385        }
1386        final ContentResolver resolver = mContext.getContentResolver();
1387        final Cursor cursor = resolver.query(contactUri, new String[]{
1388                ContactsContract.Contacts.DISPLAY_NAME,
1389                ContactsContract.Contacts.DISPLAY_NAME_ALTERNATIVE}, null, null, null);
1390
1391        if (cursor != null) {
1392            try {
1393                if (cursor.moveToFirst()) {
1394                    final String displayName = cursor.getString(0);
1395                    final String displayNameAlt = cursor.getString(1);
1396                    cursor.close();
1397                    return ContactDisplayUtils.getPreferredDisplayName(displayName, displayNameAlt,
1398                            new ContactsPreferences(mContext));
1399                }
1400            } finally {
1401                cursor.close();
1402            }
1403        }
1404        return null;
1405    }
1406
1407
1408    @Override
1409    public void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded,
1410            Uri contactLookupUri, Long joinContactId) {
1411        if (hadChanges) {
1412            if (saveSucceeded) {
1413                switch (saveMode) {
1414                    case SaveMode.JOIN:
1415                        break;
1416                    case SaveMode.SPLIT:
1417                        Toast.makeText(mContext, R.string.contactUnlinkedToast, Toast.LENGTH_SHORT)
1418                                .show();
1419                        break;
1420                    default:
1421                        final String displayName = getNameToDisplay(contactLookupUri);
1422                        final String toastMessage;
1423                        if (!TextUtils.isEmpty(displayName)) {
1424                            toastMessage = getResources().getString(
1425                                    R.string.contactSavedNamedToast, displayName);
1426                        } else {
1427                            toastMessage = getResources().getString(R.string.contactSavedToast);
1428                        }
1429                        Toast.makeText(mContext, toastMessage, Toast.LENGTH_SHORT).show();
1430                }
1431
1432            } else {
1433                Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
1434            }
1435        }
1436        switch (saveMode) {
1437            case SaveMode.CLOSE: {
1438                Intent resultIntent = null;
1439                if (saveSucceeded && contactLookupUri != null) {
1440                    final Uri lookupUri = ContactEditorUtils.maybeConvertToLegacyLookupUri(
1441                            mContext, contactLookupUri, mLookupUri);
1442                    if (Flags.getInstance().getBoolean(Experiments.CONTACT_SHEET)) {
1443                        resultIntent = ObjectFactory.getContactSheetIntent(mContext, lookupUri);
1444                    }
1445                    if (resultIntent == null) {
1446                        resultIntent = ImplicitIntentsUtil.composeQuickContactIntent(
1447                                mContext, lookupUri, ScreenType.EDITOR);
1448                        resultIntent.putExtra(QuickContactActivity.EXTRA_CONTACT_EDITED, true);
1449                    }
1450                } else {
1451                    resultIntent = null;
1452                }
1453                // It is already saved, so prevent it from being saved again
1454                mStatus = Status.CLOSING;
1455                if (mListener != null) mListener.onSaveFinished(resultIntent);
1456                break;
1457            }
1458            case SaveMode.EDITOR: {
1459                // It is already saved, so prevent it from being saved again
1460                mStatus = Status.CLOSING;
1461                if (mListener != null) mListener.onSaveFinished(/* resultIntent= */ null);
1462                break;
1463            }
1464            case SaveMode.JOIN:
1465                if (saveSucceeded && contactLookupUri != null && joinContactId != null) {
1466                    joinAggregate(joinContactId);
1467                }
1468                break;
1469            case SaveMode.RELOAD:
1470                if (saveSucceeded && contactLookupUri != null) {
1471                    // If this was in INSERT, we are changing into an EDIT now.
1472                    // If it already was an EDIT, we are changing to the new Uri now
1473                    mState = new RawContactDeltaList();
1474                    load(Intent.ACTION_EDIT, contactLookupUri, null);
1475                    mStatus = Status.LOADING;
1476                    getLoaderManager().restartLoader(LOADER_CONTACT, null, mContactLoaderListener);
1477                }
1478                break;
1479
1480            case SaveMode.SPLIT:
1481                mStatus = Status.CLOSING;
1482                if (mListener != null) {
1483                    mListener.onContactSplit(contactLookupUri);
1484                } else {
1485                    Log.d(TAG, "No listener registered, can not call onSplitFinished");
1486                }
1487                break;
1488        }
1489    }
1490
1491    /**
1492     * Shows a list of aggregates that can be joined into the currently viewed aggregate.
1493     *
1494     * @param contactLookupUri the fresh URI for the currently edited contact (after saving it)
1495     */
1496    private void showJoinAggregateActivity(Uri contactLookupUri) {
1497        if (contactLookupUri == null || !isAdded()) {
1498            return;
1499        }
1500
1501        mContactIdForJoin = ContentUris.parseId(contactLookupUri);
1502        final Intent intent = new Intent(mContext, ContactSelectionActivity.class);
1503        intent.setAction(UiIntentActions.PICK_JOIN_CONTACT_ACTION);
1504        intent.putExtra(UiIntentActions.TARGET_CONTACT_ID_EXTRA_KEY, mContactIdForJoin);
1505        startActivityForResult(intent, REQUEST_CODE_JOIN);
1506    }
1507
1508    //
1509    // Aggregation PopupWindow
1510    //
1511
1512    /**
1513     * Triggers an asynchronous search for aggregation suggestions.
1514     */
1515    protected void acquireAggregationSuggestions(Context context,
1516            long rawContactId, ValuesDelta valuesDelta) {
1517        if (mAggregationSuggestionsRawContactId != rawContactId
1518                && mAggregationSuggestionView != null) {
1519            mAggregationSuggestionView.setVisibility(View.GONE);
1520            mAggregationSuggestionView = null;
1521            mAggregationSuggestionEngine.reset();
1522        }
1523
1524        mAggregationSuggestionsRawContactId = rawContactId;
1525
1526        if (mAggregationSuggestionEngine == null) {
1527            mAggregationSuggestionEngine = new AggregationSuggestionEngine(context);
1528            mAggregationSuggestionEngine.setListener(this);
1529            mAggregationSuggestionEngine.start();
1530        }
1531
1532        mAggregationSuggestionEngine.setContactId(getContactId());
1533        mAggregationSuggestionEngine.setAccountFilter(
1534                getContent().getCurrentRawContactDelta().getAccountWithDataSet());
1535
1536        mAggregationSuggestionEngine.onNameChange(valuesDelta);
1537    }
1538
1539    /**
1540     * Returns the contact ID for the currently edited contact or 0 if the contact is new.
1541     */
1542    private long getContactId() {
1543        for (RawContactDelta rawContact : mState) {
1544            Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID);
1545            if (contactId != null) {
1546                return contactId;
1547            }
1548        }
1549        return 0;
1550    }
1551
1552    @Override
1553    public void onAggregationSuggestionChange() {
1554        final Activity activity = getActivity();
1555        if ((activity != null && activity.isFinishing())
1556                || !isVisible() ||  mState.isEmpty() || mStatus != Status.EDITING) {
1557            return;
1558        }
1559
1560        UiClosables.closeQuietly(mAggregationSuggestionPopup);
1561
1562        if (mAggregationSuggestionEngine.getSuggestedContactCount() == 0) {
1563            return;
1564        }
1565
1566        final View anchorView = getAggregationAnchorView();
1567        if (anchorView == null) {
1568            return; // Raw contact deleted?
1569        }
1570        mAggregationSuggestionPopup = new ListPopupWindow(mContext, null);
1571        mAggregationSuggestionPopup.setAnchorView(anchorView);
1572        mAggregationSuggestionPopup.setWidth(anchorView.getWidth());
1573        mAggregationSuggestionPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
1574        mAggregationSuggestionPopup.setAdapter(
1575                new AggregationSuggestionAdapter(
1576                        getActivity(),
1577                        /* listener =*/ this,
1578                        mAggregationSuggestionEngine.getSuggestions()));
1579        mAggregationSuggestionPopup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
1580            @Override
1581            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1582                final AggregationSuggestionView suggestionView = (AggregationSuggestionView) view;
1583                suggestionView.handleItemClickEvent();
1584                UiClosables.closeQuietly(mAggregationSuggestionPopup);
1585                mAggregationSuggestionPopup = null;
1586            }
1587        });
1588        mAggregationSuggestionPopup.show();
1589    }
1590
1591    /**
1592     * Returns the editor view that should be used as the anchor for aggregation suggestions.
1593     */
1594    protected View getAggregationAnchorView() {
1595        return getContent().getAggregationAnchorView();
1596    }
1597
1598    /**
1599     * Joins the suggested contact (specified by the id's of constituent raw
1600     * contacts), save all changes, and stay in the editor.
1601     */
1602    public void doJoinSuggestedContact(long[] rawContactIds) {
1603        if (!hasValidState() || mStatus != Status.EDITING) {
1604            return;
1605        }
1606
1607        mState.setJoinWithRawContacts(rawContactIds);
1608        save(SaveMode.RELOAD);
1609    }
1610
1611    @Override
1612    public void onEditAction(Uri contactLookupUri, long rawContactId) {
1613        SuggestionEditConfirmationDialogFragment.show(this, contactLookupUri, rawContactId);
1614    }
1615
1616    /**
1617     * Abandons the currently edited contact and switches to editing the selected raw contact,
1618     * transferring all the data there
1619     */
1620    public void doEditSuggestedContact(Uri contactUri, long rawContactId) {
1621        if (mListener != null) {
1622            // make sure we don't save this contact when closing down
1623            mStatus = Status.CLOSING;
1624            mListener.onEditOtherRawContactRequested(contactUri, rawContactId,
1625                    getContent().getCurrentRawContactDelta().getContentValues());
1626        }
1627    }
1628
1629    /**
1630     * Sets group metadata on all bound editors.
1631     */
1632    protected void setGroupMetaData() {
1633        if (mGroupMetaData != null) {
1634            getContent().setGroupMetaData(mGroupMetaData);
1635        }
1636    }
1637
1638    /**
1639     * Persist the accumulated editor deltas.
1640     *
1641     * @param joinContactId the raw contact ID to join the contact being saved to after the save,
1642     *         may be null.
1643     */
1644    protected boolean doSaveAction(int saveMode, Long joinContactId) {
1645        final Intent intent = ContactSaveService.createSaveContactIntent(mContext, mState,
1646                SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(),
1647                ((Activity) mContext).getClass(),
1648                ContactEditorActivity.ACTION_SAVE_COMPLETED, mUpdatedPhotos,
1649                JOIN_CONTACT_ID_EXTRA_KEY, joinContactId);
1650        return startSaveService(mContext, intent, saveMode);
1651    }
1652
1653    private boolean startSaveService(Context context, Intent intent, int saveMode) {
1654        final boolean result = ContactSaveService.startService(
1655                context, intent, saveMode);
1656        if (!result) {
1657            onCancelEditConfirmed();
1658        }
1659        return result;
1660    }
1661
1662    //
1663    // Join Activity
1664    //
1665
1666    /**
1667     * Performs aggregation with the contact selected by the user from suggestions or A-Z list.
1668     */
1669    protected void joinAggregate(final long contactId) {
1670        final Intent intent = ContactSaveService.createJoinContactsIntent(
1671                mContext, mContactIdForJoin, contactId, ContactEditorActivity.class,
1672                ContactEditorActivity.ACTION_JOIN_COMPLETED);
1673        mContext.startService(intent);
1674    }
1675
1676    public void removePhoto() {
1677        getContent().removePhoto();
1678        mUpdatedPhotos.remove(String.valueOf(mPhotoRawContactId));
1679    }
1680
1681    public void updatePhoto(Uri uri) throws FileNotFoundException {
1682        final Bitmap bitmap = ContactPhotoUtils.getBitmapFromUri(getActivity(), uri);
1683        if (bitmap == null || bitmap.getHeight() <= 0 || bitmap.getWidth() <= 0) {
1684            Toast.makeText(mContext, R.string.contactPhotoSavedErrorToast,
1685                    Toast.LENGTH_SHORT).show();
1686            return;
1687        }
1688        mUpdatedPhotos.putParcelable(String.valueOf(mPhotoRawContactId), uri);
1689        getContent().updatePhoto(uri);
1690    }
1691
1692    public void setPrimaryPhoto() {
1693        getContent().setPrimaryPhoto();
1694    }
1695
1696    @Override
1697    public void onNameFieldChanged(long rawContactId, ValuesDelta valuesDelta) {
1698        final Activity activity = getActivity();
1699        if (activity == null || activity.isFinishing()) {
1700            return;
1701        }
1702        acquireAggregationSuggestions(activity, rawContactId, valuesDelta);
1703    }
1704
1705    @Override
1706    public void onRebindEditorsForNewContact(RawContactDelta oldState,
1707            AccountWithDataSet oldAccount, AccountWithDataSet newAccount) {
1708        mNewContactAccountChanged = true;
1709        mAccountWithDataSet = newAccount;
1710        rebindEditorsForNewContact(oldState, oldAccount, newAccount);
1711    }
1712
1713    @Override
1714    public void onBindEditorsFailed() {
1715        final Activity activity = getActivity();
1716        if (activity != null && !activity.isFinishing()) {
1717            Toast.makeText(activity, R.string.editor_failed_to_load,
1718                    Toast.LENGTH_SHORT).show();
1719            activity.setResult(Activity.RESULT_CANCELED);
1720            activity.finish();
1721        }
1722    }
1723
1724    @Override
1725    public void onEditorsBound() {
1726        final Activity activity = getActivity();
1727        if (activity == null || activity.isFinishing()) {
1728            return;
1729        }
1730        getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupsLoaderListener);
1731    }
1732
1733    @Override
1734    public void onPhotoEditorViewClicked() {
1735        // For contacts composed of a single writable raw contact, or raw contacts have no more
1736        // than 1 photo, clicking the photo view simply opens the source photo dialog
1737        getEditorActivity().changePhoto(getPhotoMode());
1738    }
1739
1740    private int getPhotoMode() {
1741        return getContent().isWritablePhotoSet() ? PhotoActionPopup.Modes.WRITE_ABLE_PHOTO
1742                : PhotoActionPopup.Modes.NO_PHOTO;
1743    }
1744
1745    private ContactEditorActivity getEditorActivity() {
1746        return (ContactEditorActivity) getActivity();
1747    }
1748
1749    private RawContactEditorView getContent() {
1750        return (RawContactEditorView) mContent;
1751    }
1752}
1753