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