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