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