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