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