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