• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 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.AlertDialog;
22 import android.app.Dialog;
23 import android.app.DialogFragment;
24 import android.app.Fragment;
25 import android.app.LoaderManager;
26 import android.app.LoaderManager.LoaderCallbacks;
27 import android.content.ActivityNotFoundException;
28 import android.content.ContentUris;
29 import android.content.ContentValues;
30 import android.content.Context;
31 import android.content.CursorLoader;
32 import android.content.DialogInterface;
33 import android.content.Intent;
34 import android.content.Loader;
35 import android.database.Cursor;
36 import android.graphics.Bitmap;
37 import android.graphics.BitmapFactory;
38 import android.graphics.Rect;
39 import android.media.RingtoneManager;
40 import android.net.Uri;
41 import android.os.Bundle;
42 import android.os.SystemClock;
43 import android.provider.ContactsContract.CommonDataKinds.Email;
44 import android.provider.ContactsContract.CommonDataKinds.Event;
45 import android.provider.ContactsContract.CommonDataKinds.Organization;
46 import android.provider.ContactsContract.CommonDataKinds.Phone;
47 import android.provider.ContactsContract.CommonDataKinds.Photo;
48 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
49 import android.provider.ContactsContract.Contacts;
50 import android.provider.ContactsContract.Groups;
51 import android.provider.ContactsContract.Intents;
52 import android.provider.ContactsContract.Intents.UI;
53 import android.provider.ContactsContract.QuickContact;
54 import android.provider.ContactsContract.RawContacts;
55 import android.text.TextUtils;
56 import android.util.Log;
57 import android.view.LayoutInflater;
58 import android.view.Menu;
59 import android.view.MenuInflater;
60 import android.view.MenuItem;
61 import android.view.View;
62 import android.view.ViewGroup;
63 import android.widget.AdapterView;
64 import android.widget.AdapterView.OnItemClickListener;
65 import android.widget.BaseAdapter;
66 import android.widget.LinearLayout;
67 import android.widget.ListPopupWindow;
68 import android.widget.Toast;
69 
70 import com.android.contacts.ContactSaveService;
71 import com.android.contacts.GroupMetaDataLoader;
72 import com.android.contacts.R;
73 import com.android.contacts.activities.ContactEditorAccountsChangedActivity;
74 import com.android.contacts.activities.ContactEditorActivity;
75 import com.android.contacts.common.model.AccountTypeManager;
76 import com.android.contacts.common.model.ValuesDelta;
77 import com.android.contacts.common.model.account.AccountType;
78 import com.android.contacts.common.model.account.AccountWithDataSet;
79 import com.android.contacts.common.model.account.GoogleAccountType;
80 import com.android.contacts.common.util.AccountsListAdapter;
81 import com.android.contacts.common.util.AccountsListAdapter.AccountListFilter;
82 import com.android.contacts.detail.PhotoSelectionHandler;
83 import com.android.contacts.editor.AggregationSuggestionEngine.Suggestion;
84 import com.android.contacts.editor.Editor.EditorListener;
85 import com.android.contacts.common.model.Contact;
86 import com.android.contacts.common.model.ContactLoader;
87 import com.android.contacts.common.model.RawContact;
88 import com.android.contacts.common.model.RawContactDelta;
89 import com.android.contacts.common.model.RawContactDeltaList;
90 import com.android.contacts.common.model.RawContactModifier;
91 import com.android.contacts.quickcontact.QuickContactActivity;
92 import com.android.contacts.util.ContactPhotoUtils;
93 import com.android.contacts.util.HelpUtils;
94 import com.android.contacts.util.PhoneCapabilityTester;
95 import com.android.contacts.util.UiClosables;
96 import com.google.common.collect.ImmutableList;
97 import com.google.common.collect.Lists;
98 
99 import java.io.FileNotFoundException;
100 import java.util.ArrayList;
101 import java.util.Collections;
102 import java.util.Comparator;
103 import java.util.List;
104 
105 public class ContactEditorFragment extends Fragment implements
106         SplitContactConfirmationDialogFragment.Listener,
107         AggregationSuggestionEngine.Listener, AggregationSuggestionView.Listener,
108         RawContactReadOnlyEditorView.Listener {
109 
110     private static final String TAG = ContactEditorFragment.class.getSimpleName();
111 
112     private static final int LOADER_DATA = 1;
113     private static final int LOADER_GROUPS = 2;
114 
115     private static final String KEY_URI = "uri";
116     private static final String KEY_ACTION = "action";
117     private static final String KEY_EDIT_STATE = "state";
118     private static final String KEY_RAW_CONTACT_ID_REQUESTING_PHOTO = "photorequester";
119     private static final String KEY_VIEW_ID_GENERATOR = "viewidgenerator";
120     private static final String KEY_CURRENT_PHOTO_URI = "currentphotouri";
121     private static final String KEY_CONTACT_ID_FOR_JOIN = "contactidforjoin";
122     private static final String KEY_CONTACT_WRITABLE_FOR_JOIN = "contactwritableforjoin";
123     private static final String KEY_SHOW_JOIN_SUGGESTIONS = "showJoinSuggestions";
124     private static final String KEY_ENABLED = "enabled";
125     private static final String KEY_STATUS = "status";
126     private static final String KEY_NEW_LOCAL_PROFILE = "newLocalProfile";
127     private static final String KEY_IS_USER_PROFILE = "isUserProfile";
128     private static final String KEY_DISABLE_DELETE_MENU_OPTION = "disableDeleteMenuOption";
129     private static final String KEY_UPDATED_PHOTOS = "updatedPhotos";
130     private static final String KEY_IS_EDIT = "isEdit";
131     private static final String KEY_HAS_NEW_CONTACT = "hasNewContact";
132     private static final String KEY_NEW_CONTACT_READY = "newContactDataReady";
133     private static final String KEY_EXISTING_CONTACT_READY = "existingContactDataReady";
134     private static final String KEY_RAW_CONTACTS = "rawContacts";
135     private static final String KEY_SEND_TO_VOICE_MAIL_STATE = "sendToVoicemailState";
136     private static final String KEY_CUSTOM_RINGTONE = "customRingtone";
137     private static final String KEY_ARE_PHONE_OPTIONS_CHANGEABLE = "arePhoneOptionsChangable";
138 
139     public static final String SAVE_MODE_EXTRA_KEY = "saveMode";
140 
141 
142     /**
143      * An intent extra that forces the editor to add the edited contact
144      * to the default group (e.g. "My Contacts").
145      */
146     public static final String INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY = "addToDefaultDirectory";
147 
148     public static final String INTENT_EXTRA_NEW_LOCAL_PROFILE = "newLocalProfile";
149 
150     public static final String INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION =
151             "disableDeleteMenuOption";
152 
153     /**
154      * Modes that specify what the AsyncTask has to perform after saving
155      */
156     public interface SaveMode {
157         /**
158          * Close the editor after saving
159          */
160         public static final int CLOSE = 0;
161 
162         /**
163          * Reload the data so that the user can continue editing
164          */
165         public static final int RELOAD = 1;
166 
167         /**
168          * Split the contact after saving
169          */
170         public static final int SPLIT = 2;
171 
172         /**
173          * Join another contact after saving
174          */
175         public static final int JOIN = 3;
176 
177         /**
178          * Navigate to Contacts Home activity after saving.
179          */
180         public static final int HOME = 4;
181     }
182 
183     private interface Status {
184         /**
185          * The loader is fetching data
186          */
187         public static final int LOADING = 0;
188 
189         /**
190          * Not currently busy. We are waiting for the user to enter data
191          */
192         public static final int EDITING = 1;
193 
194         /**
195          * The data is currently being saved. This is used to prevent more
196          * auto-saves (they shouldn't overlap)
197          */
198         public static final int SAVING = 2;
199 
200         /**
201          * Prevents any more saves. This is used if in the following cases:
202          * - After Save/Close
203          * - After Revert
204          * - After the user has accepted an edit suggestion
205          */
206         public static final int CLOSING = 3;
207 
208         /**
209          * Prevents saving while running a child activity.
210          */
211         public static final int SUB_ACTIVITY = 4;
212     }
213 
214     private static final int REQUEST_CODE_JOIN = 0;
215     private static final int REQUEST_CODE_ACCOUNTS_CHANGED = 1;
216     private static final int REQUEST_CODE_PICK_RINGTONE = 2;
217 
218     /**
219      * The raw contact for which we started "take photo" or "choose photo from gallery" most
220      * recently.  Used to restore {@link #mCurrentPhotoHandler} after orientation change.
221      */
222     private long mRawContactIdRequestingPhoto;
223     /**
224      * The {@link PhotoHandler} for the photo editor for the {@link #mRawContactIdRequestingPhoto}
225      * raw contact.
226      *
227      * A {@link PhotoHandler} is created for each photo editor in {@link #bindPhotoHandler}, but
228      * the only "active" one should get the activity result.  This member represents the active
229      * one.
230      */
231     private PhotoHandler mCurrentPhotoHandler;
232 
233     private final EntityDeltaComparator mComparator = new EntityDeltaComparator();
234 
235     private Cursor mGroupMetaData;
236 
237     private Uri mCurrentPhotoUri;
238     private Bundle mUpdatedPhotos = new Bundle();
239 
240     private Context mContext;
241     private String mAction;
242     private Uri mLookupUri;
243     private Bundle mIntentExtras;
244     private Listener mListener;
245 
246     private long mContactIdForJoin;
247     private boolean mContactWritableForJoin;
248 
249     private ContactEditorUtils mEditorUtils;
250 
251     private LinearLayout mContent;
252     private RawContactDeltaList mState;
253 
254     private ViewIdGenerator mViewIdGenerator;
255 
256     private long mLoaderStartTime;
257 
258     private int mStatus;
259 
260     // Whether to show the new contact blank form and if it's corresponding delta is ready.
261     private boolean mHasNewContact = false;
262     private boolean mNewContactDataReady = false;
263 
264     // Whether it's an edit of existing contact and if it's corresponding delta is ready.
265     private boolean mIsEdit = false;
266     private boolean mExistingContactDataReady = false;
267 
268     // Variables related to phone specific option menus
269     private boolean mSendToVoicemailState;
270     private boolean mArePhoneOptionsChangable;
271     private String mCustomRingtone;
272 
273     // This is used to pre-populate the editor with a display name when a user edits a read-only
274     // contact.
275     private String mDefaultDisplayName;
276 
277     // Used to temporarily store existing contact data during a rebind call (i.e. account switch)
278     private ImmutableList<RawContact> mRawContacts;
279 
280     private AggregationSuggestionEngine mAggregationSuggestionEngine;
281     private long mAggregationSuggestionsRawContactId;
282     private View mAggregationSuggestionView;
283 
284     private ListPopupWindow mAggregationSuggestionPopup;
285 
286     private static final class AggregationSuggestionAdapter extends BaseAdapter {
287         private final Activity mActivity;
288         private final boolean mSetNewContact;
289         private final AggregationSuggestionView.Listener mListener;
290         private final List<Suggestion> mSuggestions;
291 
AggregationSuggestionAdapter(Activity activity, boolean setNewContact, AggregationSuggestionView.Listener listener, List<Suggestion> suggestions)292         public AggregationSuggestionAdapter(Activity activity, boolean setNewContact,
293                 AggregationSuggestionView.Listener listener, List<Suggestion> suggestions) {
294             mActivity = activity;
295             mSetNewContact = setNewContact;
296             mListener = listener;
297             mSuggestions = suggestions;
298         }
299 
300         @Override
getView(int position, View convertView, ViewGroup parent)301         public View getView(int position, View convertView, ViewGroup parent) {
302             Suggestion suggestion = (Suggestion) getItem(position);
303             LayoutInflater inflater = mActivity.getLayoutInflater();
304             AggregationSuggestionView suggestionView =
305                     (AggregationSuggestionView) inflater.inflate(
306                             R.layout.aggregation_suggestions_item, null);
307             suggestionView.setNewContact(mSetNewContact);
308             suggestionView.setListener(mListener);
309             suggestionView.bindSuggestion(suggestion);
310             return suggestionView;
311         }
312 
313         @Override
getItemId(int position)314         public long getItemId(int position) {
315             return position;
316         }
317 
318         @Override
getItem(int position)319         public Object getItem(int position) {
320             return mSuggestions.get(position);
321         }
322 
323         @Override
getCount()324         public int getCount() {
325             return mSuggestions.size();
326         }
327     }
328 
329     private OnItemClickListener mAggregationSuggestionItemClickListener =
330             new OnItemClickListener() {
331         @Override
332         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
333             final AggregationSuggestionView suggestionView = (AggregationSuggestionView) view;
334             suggestionView.handleItemClickEvent();
335             UiClosables.closeQuietly(mAggregationSuggestionPopup);
336             mAggregationSuggestionPopup = null;
337         }
338     };
339 
340     private boolean mAutoAddToDefaultGroup;
341 
342     private boolean mEnabled = true;
343     private boolean mRequestFocus;
344     private boolean mNewLocalProfile = false;
345     private boolean mIsUserProfile = false;
346     private boolean mDisableDeleteMenuOption = false;
347 
ContactEditorFragment()348     public ContactEditorFragment() {
349     }
350 
setEnabled(boolean enabled)351     public void setEnabled(boolean enabled) {
352         if (mEnabled != enabled) {
353             mEnabled = enabled;
354             if (mContent != null) {
355                 int count = mContent.getChildCount();
356                 for (int i = 0; i < count; i++) {
357                     mContent.getChildAt(i).setEnabled(enabled);
358                 }
359             }
360             setAggregationSuggestionViewEnabled(enabled);
361             final Activity activity = getActivity();
362             if (activity != null) activity.invalidateOptionsMenu();
363         }
364     }
365 
366     @Override
onAttach(Activity activity)367     public void onAttach(Activity activity) {
368         super.onAttach(activity);
369         mContext = activity;
370         mEditorUtils = ContactEditorUtils.getInstance(mContext);
371     }
372 
373     @Override
onStop()374     public void onStop() {
375         super.onStop();
376 
377         UiClosables.closeQuietly(mAggregationSuggestionPopup);
378 
379         // If anything was left unsaved, save it now but keep the editor open.
380         if (!getActivity().isChangingConfigurations() && mStatus == Status.EDITING) {
381             save(SaveMode.RELOAD);
382         }
383     }
384 
385     @Override
onDestroy()386     public void onDestroy() {
387         super.onDestroy();
388         if (mAggregationSuggestionEngine != null) {
389             mAggregationSuggestionEngine.quit();
390         }
391     }
392 
393     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState)394     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
395         final View view = inflater.inflate(R.layout.contact_editor_fragment, container, false);
396 
397         mContent = (LinearLayout) view.findViewById(R.id.editors);
398 
399         setHasOptionsMenu(true);
400 
401         return view;
402     }
403 
404     @Override
onActivityCreated(Bundle savedInstanceState)405     public void onActivityCreated(Bundle savedInstanceState) {
406         super.onActivityCreated(savedInstanceState);
407 
408         validateAction(mAction);
409 
410         if (mState.isEmpty()) {
411             // The delta list may not have finished loading before orientation change happens.
412             // In this case, there will be a saved state but deltas will be missing.  Reload from
413             // database.
414             if (Intent.ACTION_EDIT.equals(mAction)) {
415                 // Either...
416                 // 1) orientation change but load never finished.
417                 // or
418                 // 2) not an orientation change.  data needs to be loaded for first time.
419                 getLoaderManager().initLoader(LOADER_DATA, null, mDataLoaderListener);
420             }
421         } else {
422             // Orientation change, we already have mState, it was loaded by onCreate
423             bindEditors();
424         }
425 
426         // Handle initial actions only when existing state missing
427         if (savedInstanceState == null) {
428             if (Intent.ACTION_EDIT.equals(mAction)) {
429                 mIsEdit = true;
430             } else if (Intent.ACTION_INSERT.equals(mAction)) {
431                 mHasNewContact = true;
432                 final Account account = mIntentExtras == null ? null :
433                         (Account) mIntentExtras.getParcelable(Intents.Insert.ACCOUNT);
434                 final String dataSet = mIntentExtras == null ? null :
435                         mIntentExtras.getString(Intents.Insert.DATA_SET);
436 
437                 if (account != null) {
438                     // Account specified in Intent
439                     createContact(new AccountWithDataSet(account.name, account.type, dataSet));
440                 } else {
441                     // No Account specified. Let the user choose
442                     // Load Accounts async so that we can present them
443                     selectAccountAndCreateContact();
444                 }
445             }
446         }
447     }
448 
449     /**
450      * Checks if the requested action is valid.
451      *
452      * @param action The action to test.
453      * @throws IllegalArgumentException when the action is invalid.
454      */
validateAction(String action)455     private void validateAction(String action) {
456         if (Intent.ACTION_EDIT.equals(action) || Intent.ACTION_INSERT.equals(action) ||
457                 ContactEditorActivity.ACTION_SAVE_COMPLETED.equals(action)) {
458             return;
459         }
460         throw new IllegalArgumentException("Unknown Action String " + mAction +
461                 ". Only support " + Intent.ACTION_EDIT + " or " + Intent.ACTION_INSERT + " or " +
462                 ContactEditorActivity.ACTION_SAVE_COMPLETED);
463     }
464 
465     @Override
onStart()466     public void onStart() {
467         getLoaderManager().initLoader(LOADER_GROUPS, null, mGroupLoaderListener);
468         super.onStart();
469     }
470 
load(String action, Uri lookupUri, Bundle intentExtras)471     public void load(String action, Uri lookupUri, Bundle intentExtras) {
472         mAction = action;
473         mLookupUri = lookupUri;
474         mIntentExtras = intentExtras;
475         mAutoAddToDefaultGroup = mIntentExtras != null
476                 && mIntentExtras.containsKey(INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY);
477         mNewLocalProfile = mIntentExtras != null
478                 && mIntentExtras.getBoolean(INTENT_EXTRA_NEW_LOCAL_PROFILE);
479         mDisableDeleteMenuOption = mIntentExtras != null
480                 && mIntentExtras.getBoolean(INTENT_EXTRA_DISABLE_DELETE_MENU_OPTION);
481     }
482 
setListener(Listener value)483     public void setListener(Listener value) {
484         mListener = value;
485     }
486 
487     @Override
onCreate(Bundle savedState)488     public void onCreate(Bundle savedState) {
489         if (savedState != null) {
490             // Restore mUri before calling super.onCreate so that onInitializeLoaders
491             // would already have a uri and an action to work with
492             mLookupUri = savedState.getParcelable(KEY_URI);
493             mAction = savedState.getString(KEY_ACTION);
494         }
495 
496         super.onCreate(savedState);
497 
498         if (savedState == null) {
499             // If savedState is non-null, onRestoreInstanceState() will restore the generator.
500             mViewIdGenerator = new ViewIdGenerator();
501         } else {
502             // Read state from savedState. No loading involved here
503             mState = savedState.<RawContactDeltaList> getParcelable(KEY_EDIT_STATE);
504             mRawContactIdRequestingPhoto = savedState.getLong(
505                     KEY_RAW_CONTACT_ID_REQUESTING_PHOTO);
506             mViewIdGenerator = savedState.getParcelable(KEY_VIEW_ID_GENERATOR);
507             mCurrentPhotoUri = savedState.getParcelable(KEY_CURRENT_PHOTO_URI);
508             mContactIdForJoin = savedState.getLong(KEY_CONTACT_ID_FOR_JOIN);
509             mContactWritableForJoin = savedState.getBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN);
510             mAggregationSuggestionsRawContactId = savedState.getLong(KEY_SHOW_JOIN_SUGGESTIONS);
511             mEnabled = savedState.getBoolean(KEY_ENABLED);
512             mStatus = savedState.getInt(KEY_STATUS);
513             mNewLocalProfile = savedState.getBoolean(KEY_NEW_LOCAL_PROFILE);
514             mDisableDeleteMenuOption = savedState.getBoolean(KEY_DISABLE_DELETE_MENU_OPTION);
515             mIsUserProfile = savedState.getBoolean(KEY_IS_USER_PROFILE);
516             mUpdatedPhotos = savedState.getParcelable(KEY_UPDATED_PHOTOS);
517             mIsEdit = savedState.getBoolean(KEY_IS_EDIT);
518             mHasNewContact = savedState.getBoolean(KEY_HAS_NEW_CONTACT);
519             mNewContactDataReady = savedState.getBoolean(KEY_NEW_CONTACT_READY);
520             mExistingContactDataReady = savedState.getBoolean(KEY_EXISTING_CONTACT_READY);
521             mRawContacts = ImmutableList.copyOf(savedState.<RawContact>getParcelableArrayList(
522                     KEY_RAW_CONTACTS));
523             mSendToVoicemailState = savedState.getBoolean(KEY_SEND_TO_VOICE_MAIL_STATE);
524             mCustomRingtone =  savedState.getString(KEY_CUSTOM_RINGTONE);
525             mArePhoneOptionsChangable =  savedState.getBoolean(KEY_ARE_PHONE_OPTIONS_CHANGEABLE);
526         }
527 
528         // mState can still be null because it may not have have finished loading before
529         // onSaveInstanceState was called.
530         if (mState == null) {
531             mState = new RawContactDeltaList();
532         }
533     }
534 
setData(Contact contact)535     public void setData(Contact contact) {
536 
537         // If we have already loaded data, we do not want to change it here to not confuse the user
538         if (!mState.isEmpty()) {
539             Log.v(TAG, "Ignoring background change. This will have to be rebased later");
540             return;
541         }
542 
543         // See if this edit operation needs to be redirected to a custom editor
544         mRawContacts = contact.getRawContacts();
545         if (mRawContacts.size() == 1) {
546             RawContact rawContact = mRawContacts.get(0);
547             String type = rawContact.getAccountTypeString();
548             String dataSet = rawContact.getDataSet();
549             AccountType accountType = rawContact.getAccountType(mContext);
550             if (accountType.getEditContactActivityClassName() != null &&
551                     !accountType.areContactsWritable()) {
552                 if (mListener != null) {
553                     String name = rawContact.getAccountName();
554                     long rawContactId = rawContact.getId();
555                     mListener.onCustomEditContactActivityRequested(
556                             new AccountWithDataSet(name, type, dataSet),
557                             ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
558                             mIntentExtras, true);
559                 }
560                 return;
561             }
562         }
563 
564         String displayName = null;
565         // Check for writable raw contacts.  If there are none, then we need to create one so user
566         // can edit.  For the user profile case, there is already an editable contact.
567         if (!contact.isUserProfile() && !contact.isWritableContact(mContext)) {
568             mHasNewContact = true;
569 
570             // This is potentially an asynchronous call and will add deltas to list.
571             selectAccountAndCreateContact();
572             displayName = contact.getDisplayName();
573         }
574 
575         // This also adds deltas to list
576         // If displayName is null at this point it is simply ignored later on by the editor.
577         bindEditorsForExistingContact(displayName, contact.isUserProfile(),
578                 mRawContacts);
579 
580         bindMenuItemsForPhone(contact);
581     }
582 
583     @Override
onExternalEditorRequest(AccountWithDataSet account, Uri uri)584     public void onExternalEditorRequest(AccountWithDataSet account, Uri uri) {
585         mListener.onCustomEditContactActivityRequested(account, uri, null, false);
586     }
587 
bindEditorsForExistingContact(String displayName, boolean isUserProfile, ImmutableList<RawContact> rawContacts)588     private void bindEditorsForExistingContact(String displayName, boolean isUserProfile,
589             ImmutableList<RawContact> rawContacts) {
590         setEnabled(true);
591         mDefaultDisplayName = displayName;
592 
593         mState.addAll(rawContacts.iterator());
594         setIntentExtras(mIntentExtras);
595         mIntentExtras = null;
596 
597         // For user profile, change the contacts query URI
598         mIsUserProfile = isUserProfile;
599         boolean localProfileExists = false;
600 
601         if (mIsUserProfile) {
602             for (RawContactDelta state : mState) {
603                 // For profile contacts, we need a different query URI
604                 state.setProfileQueryUri();
605                 // Try to find a local profile contact
606                 if (state.getValues().getAsString(RawContacts.ACCOUNT_TYPE) == null) {
607                     localProfileExists = true;
608                 }
609             }
610             // Editor should always present a local profile for editing
611             if (!localProfileExists) {
612                 final RawContact rawContact = new RawContact();
613                 rawContact.setAccountToLocal();
614 
615                 RawContactDelta insert = new RawContactDelta(ValuesDelta.fromAfter(
616                         rawContact.getValues()));
617                 insert.setProfileQueryUri();
618                 mState.add(insert);
619             }
620         }
621         mRequestFocus = true;
622         mExistingContactDataReady = true;
623         bindEditors();
624     }
625 
bindMenuItemsForPhone(Contact contact)626     private void bindMenuItemsForPhone(Contact contact) {
627         mSendToVoicemailState = contact.isSendToVoicemail();
628         mCustomRingtone = contact.getCustomRingtone();
629         mArePhoneOptionsChangable = arePhoneOptionsChangable(contact);
630     }
631 
arePhoneOptionsChangable(Contact contact)632     private boolean arePhoneOptionsChangable(Contact contact) {
633         return contact != null && !contact.isDirectoryEntry()
634                 && PhoneCapabilityTester.isPhone(mContext);
635     }
636 
637     /**
638      * Merges extras from the intent.
639      */
setIntentExtras(Bundle extras)640     public void setIntentExtras(Bundle extras) {
641         if (extras == null || extras.size() == 0) {
642             return;
643         }
644 
645         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
646         for (RawContactDelta state : mState) {
647             final AccountType type = state.getAccountType(accountTypes);
648             if (type.areContactsWritable()) {
649                 // Apply extras to the first writable raw contact only
650                 RawContactModifier.parseExtras(mContext, type, state, extras);
651                 break;
652             }
653         }
654     }
655 
selectAccountAndCreateContact()656     private void selectAccountAndCreateContact() {
657         // If this is a local profile, then skip the logic about showing the accounts changed
658         // activity and create a phone-local contact.
659         if (mNewLocalProfile) {
660             createContact(null);
661             return;
662         }
663 
664         // If there is no default account or the accounts have changed such that we need to
665         // prompt the user again, then launch the account prompt.
666         if (mEditorUtils.shouldShowAccountChangedNotification()) {
667             Intent intent = new Intent(mContext, ContactEditorAccountsChangedActivity.class);
668             mStatus = Status.SUB_ACTIVITY;
669             startActivityForResult(intent, REQUEST_CODE_ACCOUNTS_CHANGED);
670         } else {
671             // Otherwise, there should be a default account. Then either create a local contact
672             // (if default account is null) or create a contact with the specified account.
673             AccountWithDataSet defaultAccount = mEditorUtils.getDefaultAccount();
674             if (defaultAccount == null) {
675                 createContact(null);
676             } else {
677                 createContact(defaultAccount);
678             }
679         }
680     }
681 
682     /**
683      * Create a contact by automatically selecting the first account. If there's no available
684      * account, a device-local contact should be created.
685      */
createContact()686     private void createContact() {
687         final List<AccountWithDataSet> accounts =
688                 AccountTypeManager.getInstance(mContext).getAccounts(true);
689         // No Accounts available. Create a phone-local contact.
690         if (accounts.isEmpty()) {
691             createContact(null);
692             return;
693         }
694 
695         // We have an account switcher in "create-account" screen, so don't need to ask a user to
696         // select an account here.
697         createContact(accounts.get(0));
698     }
699 
700     /**
701      * Shows account creation screen associated with a given account.
702      *
703      * @param account may be null to signal a device-local contact should be created.
704      */
createContact(AccountWithDataSet account)705     private void createContact(AccountWithDataSet account) {
706         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
707         final AccountType accountType =
708                 accountTypes.getAccountType(account != null ? account.type : null,
709                         account != null ? account.dataSet : null);
710 
711         if (accountType.getCreateContactActivityClassName() != null) {
712             if (mListener != null) {
713                 mListener.onCustomCreateContactActivityRequested(account, mIntentExtras);
714             }
715         } else {
716             bindEditorsForNewContact(account, accountType);
717         }
718     }
719 
720     /**
721      * Removes a current editor ({@link #mState}) and rebinds new editor for a new account.
722      * Some of old data are reused with new restriction enforced by the new account.
723      *
724      * @param oldState Old data being edited.
725      * @param oldAccount Old account associated with oldState.
726      * @param newAccount New account to be used.
727      */
rebindEditorsForNewContact( RawContactDelta oldState, AccountWithDataSet oldAccount, AccountWithDataSet newAccount)728     private void rebindEditorsForNewContact(
729             RawContactDelta oldState, AccountWithDataSet oldAccount,
730             AccountWithDataSet newAccount) {
731         AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
732         AccountType oldAccountType = accountTypes.getAccountType(
733                 oldAccount.type, oldAccount.dataSet);
734         AccountType newAccountType = accountTypes.getAccountType(
735                 newAccount.type, newAccount.dataSet);
736 
737         if (newAccountType.getCreateContactActivityClassName() != null) {
738             Log.w(TAG, "external activity called in rebind situation");
739             if (mListener != null) {
740                 mListener.onCustomCreateContactActivityRequested(newAccount, mIntentExtras);
741             }
742         } else {
743             mExistingContactDataReady = false;
744             mNewContactDataReady = false;
745             mState = new RawContactDeltaList();
746             bindEditorsForNewContact(newAccount, newAccountType, oldState, oldAccountType);
747             if (mIsEdit) {
748                 bindEditorsForExistingContact(mDefaultDisplayName, mIsUserProfile, mRawContacts);
749             }
750         }
751     }
752 
bindEditorsForNewContact(AccountWithDataSet account, final AccountType accountType)753     private void bindEditorsForNewContact(AccountWithDataSet account,
754             final AccountType accountType) {
755         bindEditorsForNewContact(account, accountType, null, null);
756     }
757 
bindEditorsForNewContact(AccountWithDataSet newAccount, final AccountType newAccountType, RawContactDelta oldState, AccountType oldAccountType)758     private void bindEditorsForNewContact(AccountWithDataSet newAccount,
759             final AccountType newAccountType, RawContactDelta oldState,
760             AccountType oldAccountType) {
761         mStatus = Status.EDITING;
762 
763         final RawContact rawContact = new RawContact();
764         if (newAccount != null) {
765             rawContact.setAccount(newAccount);
766         } else {
767             rawContact.setAccountToLocal();
768         }
769 
770         final ValuesDelta valuesDelta = ValuesDelta.fromAfter(rawContact.getValues());
771         final RawContactDelta insert = new RawContactDelta(valuesDelta);
772         if (oldState == null) {
773             // Parse any values from incoming intent
774             RawContactModifier.parseExtras(mContext, newAccountType, insert, mIntentExtras);
775         } else {
776             RawContactModifier.migrateStateForNewContact(mContext, oldState, insert,
777                     oldAccountType, newAccountType);
778         }
779 
780         // Ensure we have some default fields (if the account type does not support a field,
781         // ensureKind will not add it, so it is safe to add e.g. Event)
782         RawContactModifier.ensureKindExists(insert, newAccountType, Phone.CONTENT_ITEM_TYPE);
783         RawContactModifier.ensureKindExists(insert, newAccountType, Email.CONTENT_ITEM_TYPE);
784         RawContactModifier.ensureKindExists(insert, newAccountType, Organization.CONTENT_ITEM_TYPE);
785         RawContactModifier.ensureKindExists(insert, newAccountType, Event.CONTENT_ITEM_TYPE);
786         RawContactModifier.ensureKindExists(insert, newAccountType,
787                 StructuredPostal.CONTENT_ITEM_TYPE);
788 
789         // Set the correct URI for saving the contact as a profile
790         if (mNewLocalProfile) {
791             insert.setProfileQueryUri();
792         }
793 
794         mState.add(insert);
795 
796         mRequestFocus = true;
797 
798         mNewContactDataReady = true;
799         bindEditors();
800     }
801 
bindEditors()802     private void bindEditors() {
803         // bindEditors() can only bind views if there is data in mState, so immediately return
804         // if mState is null
805         if (mState.isEmpty()) {
806             return;
807         }
808 
809         // Check if delta list is ready.  Delta list is populated from existing data and when
810         // editing an read-only contact, it's also populated with newly created data for the
811         // blank form.  When the data is not ready, skip. This method will be called multiple times.
812         if ((mIsEdit && !mExistingContactDataReady) || (mHasNewContact && !mNewContactDataReady)) {
813             return;
814         }
815 
816         // Sort the editors
817         Collections.sort(mState, mComparator);
818 
819         // Remove any existing editors and rebuild any visible
820         mContent.removeAllViews();
821 
822         final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
823                 Context.LAYOUT_INFLATER_SERVICE);
824         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
825         int numRawContacts = mState.size();
826 
827         for (int i = 0; i < numRawContacts; i++) {
828             // TODO ensure proper ordering of entities in the list
829             final RawContactDelta rawContactDelta = mState.get(i);
830             if (!rawContactDelta.isVisible()) continue;
831 
832             final AccountType type = rawContactDelta.getAccountType(accountTypes);
833             final long rawContactId = rawContactDelta.getRawContactId();
834 
835             final BaseRawContactEditorView editor;
836             if (!type.areContactsWritable()) {
837                 editor = (BaseRawContactEditorView) inflater.inflate(
838                         R.layout.raw_contact_readonly_editor_view, mContent, false);
839                 ((RawContactReadOnlyEditorView) editor).setListener(this);
840             } else {
841                 editor = (RawContactEditorView) inflater.inflate(R.layout.raw_contact_editor_view,
842                         mContent, false);
843             }
844             if (mHasNewContact && !mNewLocalProfile) {
845                 final List<AccountWithDataSet> accounts =
846                         AccountTypeManager.getInstance(mContext).getAccounts(true);
847                 if (accounts.size() > 1) {
848                     addAccountSwitcher(mState.get(0), editor);
849                 } else {
850                     disableAccountSwitcher(editor);
851                 }
852             } else {
853                 disableAccountSwitcher(editor);
854             }
855 
856             editor.setEnabled(mEnabled);
857 
858             mContent.addView(editor);
859 
860             editor.setState(rawContactDelta, type, mViewIdGenerator, isEditingUserProfile());
861 
862             // Set up the photo handler.
863             bindPhotoHandler(editor, type, mState);
864 
865             // If a new photo was chosen but not yet saved, we need to
866             // update the thumbnail to reflect this.
867             Bitmap bitmap = updatedBitmapForRawContact(rawContactId);
868             if (bitmap != null) editor.setPhotoBitmap(bitmap);
869 
870             if (editor instanceof RawContactEditorView) {
871                 final Activity activity = getActivity();
872                 final RawContactEditorView rawContactEditor = (RawContactEditorView) editor;
873                 EditorListener listener = new EditorListener() {
874 
875                     @Override
876                     public void onRequest(int request) {
877                         if (activity.isFinishing()) { // Make sure activity is still running.
878                             return;
879                         }
880                         if (request == EditorListener.FIELD_CHANGED && !isEditingUserProfile()) {
881                             acquireAggregationSuggestions(activity, rawContactEditor);
882                         }
883                     }
884 
885                     @Override
886                     public void onDeleteRequested(Editor removedEditor) {
887                     }
888                 };
889 
890                 final StructuredNameEditorView nameEditor = rawContactEditor.getNameEditor();
891                 if (mRequestFocus) {
892                     nameEditor.requestFocus();
893                     mRequestFocus = false;
894                 }
895                 nameEditor.setEditorListener(listener);
896                 if (!TextUtils.isEmpty(mDefaultDisplayName)) {
897                     nameEditor.setDisplayName(mDefaultDisplayName);
898                 }
899 
900                 final TextFieldsEditorView phoneticNameEditor =
901                         rawContactEditor.getPhoneticNameEditor();
902                 phoneticNameEditor.setEditorListener(listener);
903                 rawContactEditor.setAutoAddToDefaultGroup(mAutoAddToDefaultGroup);
904 
905                 if (rawContactId == mAggregationSuggestionsRawContactId) {
906                     acquireAggregationSuggestions(activity, rawContactEditor);
907                 }
908             }
909         }
910 
911         mRequestFocus = false;
912 
913         bindGroupMetaData();
914 
915         // Show editor now that we've loaded state
916         mContent.setVisibility(View.VISIBLE);
917 
918         // Refresh Action Bar as the visibility of the join command
919         // Activity can be null if we have been detached from the Activity
920         final Activity activity = getActivity();
921         if (activity != null) activity.invalidateOptionsMenu();
922     }
923 
924     /**
925      * If we've stashed a temporary file containing a contact's new photo,
926      * decode it and return the bitmap.
927      * @param rawContactId identifies the raw-contact whose Bitmap we'll try to return.
928      * @return Bitmap of photo for specified raw-contact, or null
929     */
updatedBitmapForRawContact(long rawContactId)930     private Bitmap updatedBitmapForRawContact(long rawContactId) {
931         String path = mUpdatedPhotos.getString(String.valueOf(rawContactId));
932         return path == null ? null : BitmapFactory.decodeFile(path);
933     }
934 
bindPhotoHandler(BaseRawContactEditorView editor, AccountType type, RawContactDeltaList state)935     private void bindPhotoHandler(BaseRawContactEditorView editor, AccountType type,
936             RawContactDeltaList state) {
937         final int mode;
938         if (type.areContactsWritable()) {
939             if (editor.hasSetPhoto()) {
940                 if (hasMoreThanOnePhoto()) {
941                     mode = PhotoActionPopup.Modes.PHOTO_ALLOW_PRIMARY;
942                 } else {
943                     mode = PhotoActionPopup.Modes.PHOTO_DISALLOW_PRIMARY;
944                 }
945             } else {
946                 mode = PhotoActionPopup.Modes.NO_PHOTO;
947             }
948         } else {
949             if (editor.hasSetPhoto() && hasMoreThanOnePhoto()) {
950                 mode = PhotoActionPopup.Modes.READ_ONLY_ALLOW_PRIMARY;
951             } else {
952                 // Read-only and either no photo or the only photo ==> no options
953                 editor.getPhotoEditor().setEditorListener(null);
954                 return;
955             }
956         }
957         final PhotoHandler photoHandler = new PhotoHandler(mContext, editor, mode, state);
958         editor.getPhotoEditor().setEditorListener(
959                 (PhotoHandler.PhotoEditorListener) photoHandler.getListener());
960 
961         // Note a newly created raw contact gets some random negative ID, so any value is valid
962         // here. (i.e. don't check against -1 or anything.)
963         if (mRawContactIdRequestingPhoto == editor.getRawContactId()) {
964             mCurrentPhotoHandler = photoHandler;
965         }
966     }
967 
bindGroupMetaData()968     private void bindGroupMetaData() {
969         if (mGroupMetaData == null) {
970             return;
971         }
972 
973         int editorCount = mContent.getChildCount();
974         for (int i = 0; i < editorCount; i++) {
975             BaseRawContactEditorView editor = (BaseRawContactEditorView) mContent.getChildAt(i);
976             editor.setGroupMetaData(mGroupMetaData);
977         }
978     }
979 
saveDefaultAccountIfNecessary()980     private void saveDefaultAccountIfNecessary() {
981         // Verify that this is a newly created contact, that the contact is composed of only
982         // 1 raw contact, and that the contact is not a user profile.
983         if (!Intent.ACTION_INSERT.equals(mAction) && mState.size() == 1 &&
984                 !isEditingUserProfile()) {
985             return;
986         }
987 
988         // Find the associated account for this contact (retrieve it here because there are
989         // multiple paths to creating a contact and this ensures we always have the correct
990         // account).
991         final RawContactDelta rawContactDelta = mState.get(0);
992         String name = rawContactDelta.getAccountName();
993         String type = rawContactDelta.getAccountType();
994         String dataSet = rawContactDelta.getDataSet();
995 
996         AccountWithDataSet account = (name == null || type == null) ? null :
997                 new AccountWithDataSet(name, type, dataSet);
998         mEditorUtils.saveDefaultAndAllAccounts(account);
999     }
1000 
addAccountSwitcher( final RawContactDelta currentState, BaseRawContactEditorView editor)1001     private void addAccountSwitcher(
1002             final RawContactDelta currentState, BaseRawContactEditorView editor) {
1003         final AccountWithDataSet currentAccount = new AccountWithDataSet(
1004                 currentState.getAccountName(),
1005                 currentState.getAccountType(),
1006                 currentState.getDataSet());
1007         final View accountView = editor.findViewById(R.id.account);
1008         final View anchorView = editor.findViewById(R.id.account_container);
1009         accountView.setOnClickListener(new View.OnClickListener() {
1010             @Override
1011             public void onClick(View v) {
1012                 final ListPopupWindow popup = new ListPopupWindow(mContext, null);
1013                 final AccountsListAdapter adapter =
1014                         new AccountsListAdapter(mContext,
1015                         AccountListFilter.ACCOUNTS_CONTACT_WRITABLE, currentAccount);
1016                 popup.setWidth(anchorView.getWidth());
1017                 popup.setAnchorView(anchorView);
1018                 popup.setAdapter(adapter);
1019                 popup.setModal(true);
1020                 popup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
1021                 popup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
1022                     @Override
1023                     public void onItemClick(AdapterView<?> parent, View view, int position,
1024                             long id) {
1025                         UiClosables.closeQuietly(popup);
1026                         AccountWithDataSet newAccount = adapter.getItem(position);
1027                         if (!newAccount.equals(currentAccount)) {
1028                             rebindEditorsForNewContact(currentState, currentAccount, newAccount);
1029                         }
1030                     }
1031                 });
1032                 popup.show();
1033             }
1034         });
1035     }
1036 
disableAccountSwitcher(BaseRawContactEditorView editor)1037     private void disableAccountSwitcher(BaseRawContactEditorView editor) {
1038         // Remove the pressed state from the account header because the user cannot switch accounts
1039         // on an existing contact
1040         final View accountView = editor.findViewById(R.id.account);
1041         accountView.setBackground(null);
1042         accountView.setEnabled(false);
1043     }
1044 
1045     @Override
onCreateOptionsMenu(Menu menu, final MenuInflater inflater)1046     public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
1047         inflater.inflate(R.menu.edit_contact, menu);
1048     }
1049 
1050     @Override
onPrepareOptionsMenu(Menu menu)1051     public void onPrepareOptionsMenu(Menu menu) {
1052         // This supports the keyboard shortcut to save changes to a contact but shouldn't be visible
1053         // because the custom action bar contains the "save" button now (not the overflow menu).
1054         // TODO: Find a better way to handle shortcuts, i.e. onKeyDown()?
1055         final MenuItem doneMenu = menu.findItem(R.id.menu_done);
1056         final MenuItem splitMenu = menu.findItem(R.id.menu_split);
1057         final MenuItem joinMenu = menu.findItem(R.id.menu_join);
1058         final MenuItem helpMenu = menu.findItem(R.id.menu_help);
1059         final MenuItem discardMenu = menu.findItem(R.id.menu_discard);
1060         final MenuItem sendToVoiceMailMenu = menu.findItem(R.id.menu_send_to_voicemail);
1061         final MenuItem ringToneMenu = menu.findItem(R.id.menu_set_ringtone);
1062         final MenuItem deleteMenu = menu.findItem(R.id.menu_delete);
1063 
1064         // Set visibility of menus
1065         doneMenu.setVisible(false);
1066 
1067         // Discard menu is only available if at least one raw contact is editable
1068         discardMenu.setVisible(mState != null &&
1069                 mState.getFirstWritableRawContact(mContext) != null);
1070 
1071         // help menu depending on whether this is inserting or editing
1072         if (Intent.ACTION_INSERT.equals(mAction)) {
1073             HelpUtils.prepareHelpMenuItem(mContext, helpMenu, R.string.help_url_people_add);
1074             splitMenu.setVisible(false);
1075             joinMenu.setVisible(false);
1076             deleteMenu.setVisible(false);
1077         } else if (Intent.ACTION_EDIT.equals(mAction)) {
1078             HelpUtils.prepareHelpMenuItem(mContext, helpMenu, R.string.help_url_people_edit);
1079             // Split only if more than one raw profile and not a user profile
1080             splitMenu.setVisible(mState.size() > 1 && !isEditingUserProfile());
1081             // Cannot join a user profile
1082             joinMenu.setVisible(!isEditingUserProfile());
1083             deleteMenu.setVisible(!mDisableDeleteMenuOption);
1084         } else {
1085             // something else, so don't show the help menu
1086             helpMenu.setVisible(false);
1087         }
1088 
1089         // Hide telephony-related settings (ringtone, send to voicemail)
1090         // if we don't have a telephone or are editing a new contact.
1091         sendToVoiceMailMenu.setChecked(mSendToVoicemailState);
1092         sendToVoiceMailMenu.setVisible(mArePhoneOptionsChangable);
1093         ringToneMenu.setVisible(mArePhoneOptionsChangable);
1094 
1095         int size = menu.size();
1096         for (int i = 0; i < size; i++) {
1097             menu.getItem(i).setEnabled(mEnabled);
1098         }
1099     }
1100 
1101     @Override
onOptionsItemSelected(MenuItem item)1102     public boolean onOptionsItemSelected(MenuItem item) {
1103         switch (item.getItemId()) {
1104             case R.id.menu_done:
1105                 return save(SaveMode.CLOSE);
1106             case R.id.menu_discard:
1107                 return revert();
1108             case R.id.menu_delete:
1109                 if (mListener != null) mListener.onDeleteRequested(mLookupUri);
1110                 return true;
1111             case R.id.menu_split:
1112                 return doSplitContactAction();
1113             case R.id.menu_join:
1114                 return doJoinContactAction();
1115             case R.id.menu_set_ringtone:
1116                 doPickRingtone();
1117                 return true;
1118             case R.id.menu_send_to_voicemail:
1119                 // Update state and save
1120                 mSendToVoicemailState = !mSendToVoicemailState;
1121                 item.setChecked(mSendToVoicemailState);
1122                 final Intent intent = ContactSaveService.createSetSendToVoicemail(
1123                         mContext, mLookupUri, mSendToVoicemailState);
1124                 mContext.startService(intent);
1125                 return true;
1126         }
1127 
1128         return false;
1129     }
1130 
doSplitContactAction()1131     private boolean doSplitContactAction() {
1132         if (!hasValidState()) return false;
1133 
1134         final SplitContactConfirmationDialogFragment dialog =
1135                 new SplitContactConfirmationDialogFragment();
1136         dialog.setTargetFragment(this, 0);
1137         dialog.show(getFragmentManager(), SplitContactConfirmationDialogFragment.TAG);
1138         return true;
1139     }
1140 
doJoinContactAction()1141     private boolean doJoinContactAction() {
1142         if (!hasValidState()) {
1143             return false;
1144         }
1145 
1146         // If we just started creating a new contact and haven't added any data, it's too
1147         // early to do a join
1148         if (mState.size() == 1 && mState.get(0).isContactInsert() && !hasPendingChanges()) {
1149             Toast.makeText(mContext, R.string.toast_join_with_empty_contact,
1150                             Toast.LENGTH_LONG).show();
1151             return true;
1152         }
1153 
1154         return save(SaveMode.JOIN);
1155     }
1156 
1157     /**
1158      * Check if our internal {@link #mState} is valid, usually checked before
1159      * performing user actions.
1160      */
hasValidState()1161     private boolean hasValidState() {
1162         return mState.size() > 0;
1163     }
1164 
1165     /**
1166      * Return true if there are any edits to the current contact which need to
1167      * be saved.
1168      */
hasPendingChanges()1169     private boolean hasPendingChanges() {
1170         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
1171         return RawContactModifier.hasChanges(mState, accountTypes);
1172     }
1173 
1174     /**
1175      * Saves or creates the contact based on the mode, and if successful
1176      * finishes the activity.
1177      */
save(int saveMode)1178     public boolean save(int saveMode) {
1179         if (!hasValidState() || mStatus != Status.EDITING) {
1180             return false;
1181         }
1182 
1183         // If we are about to close the editor - there is no need to refresh the data
1184         if (saveMode == SaveMode.CLOSE || saveMode == SaveMode.SPLIT) {
1185             getLoaderManager().destroyLoader(LOADER_DATA);
1186         }
1187 
1188         mStatus = Status.SAVING;
1189 
1190         if (!hasPendingChanges()) {
1191             if (mLookupUri == null && saveMode == SaveMode.RELOAD) {
1192                 // We don't have anything to save and there isn't even an existing contact yet.
1193                 // Nothing to do, simply go back to editing mode
1194                 mStatus = Status.EDITING;
1195                 return true;
1196             }
1197             onSaveCompleted(false, saveMode, mLookupUri != null, mLookupUri);
1198             return true;
1199         }
1200 
1201         setEnabled(false);
1202 
1203         // Store account as default account, only if this is a new contact
1204         saveDefaultAccountIfNecessary();
1205 
1206         // Save contact
1207         Intent intent = ContactSaveService.createSaveContactIntent(mContext, mState,
1208                 SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(),
1209                 ((Activity)mContext).getClass(), ContactEditorActivity.ACTION_SAVE_COMPLETED,
1210                 mUpdatedPhotos);
1211         mContext.startService(intent);
1212 
1213         // Don't try to save the same photos twice.
1214         mUpdatedPhotos = new Bundle();
1215 
1216         return true;
1217     }
1218 
doPickRingtone()1219     private void doPickRingtone() {
1220 
1221         final Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
1222         // Allow user to pick 'Default'
1223         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
1224         // Show only ringtones
1225         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_RINGTONE);
1226         // Allow the user to pick a silent ringtone
1227         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
1228 
1229         final Uri ringtoneUri;
1230         if (mCustomRingtone != null) {
1231             ringtoneUri = Uri.parse(mCustomRingtone);
1232         } else {
1233             // Otherwise pick default ringtone Uri so that something is selected.
1234             ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
1235         }
1236 
1237         // Put checkmark next to the current ringtone for this contact
1238         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, ringtoneUri);
1239 
1240         // Launch!
1241         try {
1242             startActivityForResult(intent, REQUEST_CODE_PICK_RINGTONE);
1243         } catch (ActivityNotFoundException ex) {
1244             Toast.makeText(mContext, R.string.missing_app, Toast.LENGTH_SHORT).show();
1245         }
1246     }
1247 
handleRingtonePicked(Uri pickedUri)1248     private void handleRingtonePicked(Uri pickedUri) {
1249         if (pickedUri == null || RingtoneManager.isDefault(pickedUri)) {
1250             mCustomRingtone = null;
1251         } else {
1252             mCustomRingtone = pickedUri.toString();
1253         }
1254         Intent intent = ContactSaveService.createSetRingtone(
1255                 mContext, mLookupUri, mCustomRingtone);
1256         mContext.startService(intent);
1257     }
1258 
1259     public static class CancelEditDialogFragment extends DialogFragment {
1260 
show(ContactEditorFragment fragment)1261         public static void show(ContactEditorFragment fragment) {
1262             CancelEditDialogFragment dialog = new CancelEditDialogFragment();
1263             dialog.setTargetFragment(fragment, 0);
1264             dialog.show(fragment.getFragmentManager(), "cancelEditor");
1265         }
1266 
1267         @Override
onCreateDialog(Bundle savedInstanceState)1268         public Dialog onCreateDialog(Bundle savedInstanceState) {
1269             AlertDialog dialog = new AlertDialog.Builder(getActivity())
1270                     .setIconAttribute(android.R.attr.alertDialogIcon)
1271                     .setMessage(R.string.cancel_confirmation_dialog_message)
1272                     .setPositiveButton(android.R.string.ok,
1273                         new DialogInterface.OnClickListener() {
1274                             @Override
1275                             public void onClick(DialogInterface dialogInterface, int whichButton) {
1276                                 ((ContactEditorFragment)getTargetFragment()).doRevertAction();
1277                             }
1278                         }
1279                     )
1280                     .setNegativeButton(android.R.string.cancel, null)
1281                     .create();
1282             return dialog;
1283         }
1284     }
1285 
revert()1286     private boolean revert() {
1287         if (mState.isEmpty() || !hasPendingChanges()) {
1288             doRevertAction();
1289         } else {
1290             CancelEditDialogFragment.show(this);
1291         }
1292         return true;
1293     }
1294 
doRevertAction()1295     private void doRevertAction() {
1296         // When this Fragment is closed we don't want it to auto-save
1297         mStatus = Status.CLOSING;
1298         if (mListener != null) mListener.onReverted();
1299     }
1300 
doSaveAction()1301     public void doSaveAction() {
1302         save(SaveMode.CLOSE);
1303     }
1304 
onJoinCompleted(Uri uri)1305     public void onJoinCompleted(Uri uri) {
1306         onSaveCompleted(false, SaveMode.RELOAD, uri != null, uri);
1307     }
1308 
onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded, Uri contactLookupUri)1309     public void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded,
1310             Uri contactLookupUri) {
1311         if (hadChanges) {
1312             if (saveSucceeded) {
1313                 if (saveMode != SaveMode.JOIN) {
1314                     Toast.makeText(mContext, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
1315                 }
1316             } else {
1317                 Toast.makeText(mContext, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
1318             }
1319         }
1320         switch (saveMode) {
1321             case SaveMode.CLOSE:
1322             case SaveMode.HOME:
1323                 final Intent resultIntent;
1324                 if (saveSucceeded && contactLookupUri != null) {
1325                     final String requestAuthority =
1326                             mLookupUri == null ? null : mLookupUri.getAuthority();
1327 
1328                     final String legacyAuthority = "contacts";
1329                     final Uri lookupUri;
1330                     if (legacyAuthority.equals(requestAuthority)) {
1331                         // Build legacy Uri when requested by caller
1332                         final long contactId = ContentUris.parseId(Contacts.lookupContact(
1333                                 mContext.getContentResolver(), contactLookupUri));
1334                         final Uri legacyContentUri = Uri.parse("content://contacts/people");
1335                         final Uri legacyUri = ContentUris.withAppendedId(
1336                                 legacyContentUri, contactId);
1337                         lookupUri = legacyUri;
1338                     } else {
1339                         // Otherwise pass back a lookup-style Uri
1340                         lookupUri = contactLookupUri;
1341                     }
1342                     resultIntent = QuickContact.composeQuickContactsIntent(getActivity(),
1343                             (Rect) null, lookupUri, QuickContactActivity.MODE_FULLY_EXPANDED, null);
1344                     // Make sure not to show QuickContacts on top of another QuickContacts.
1345                     resultIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
1346                 } else {
1347                     resultIntent = null;
1348                 }
1349                 // It is already saved, so prevent that it is saved again
1350                 mStatus = Status.CLOSING;
1351                 if (mListener != null) mListener.onSaveFinished(resultIntent);
1352                 break;
1353 
1354             case SaveMode.RELOAD:
1355             case SaveMode.JOIN:
1356                 if (saveSucceeded && contactLookupUri != null) {
1357                     // If it was a JOIN, we are now ready to bring up the join activity.
1358                     if (saveMode == SaveMode.JOIN && hasValidState()) {
1359                         showJoinAggregateActivity(contactLookupUri);
1360                     }
1361 
1362                     // If this was in INSERT, we are changing into an EDIT now.
1363                     // If it already was an EDIT, we are changing to the new Uri now
1364                     mState = new RawContactDeltaList();
1365                     load(Intent.ACTION_EDIT, contactLookupUri, null);
1366                     mStatus = Status.LOADING;
1367                     getLoaderManager().restartLoader(LOADER_DATA, null, mDataLoaderListener);
1368                 }
1369                 break;
1370 
1371             case SaveMode.SPLIT:
1372                 mStatus = Status.CLOSING;
1373                 if (mListener != null) {
1374                     mListener.onContactSplit(contactLookupUri);
1375                 } else {
1376                     Log.d(TAG, "No listener registered, can not call onSplitFinished");
1377                 }
1378                 break;
1379         }
1380     }
1381 
1382     /**
1383      * Shows a list of aggregates that can be joined into the currently viewed aggregate.
1384      *
1385      * @param contactLookupUri the fresh URI for the currently edited contact (after saving it)
1386      */
showJoinAggregateActivity(Uri contactLookupUri)1387     private void showJoinAggregateActivity(Uri contactLookupUri) {
1388         if (contactLookupUri == null || !isAdded()) {
1389             return;
1390         }
1391 
1392         mContactIdForJoin = ContentUris.parseId(contactLookupUri);
1393         mContactWritableForJoin = isContactWritable();
1394         final Intent intent = new Intent(UI.PICK_JOIN_CONTACT_ACTION);
1395         intent.putExtra(UI.TARGET_CONTACT_ID_EXTRA_KEY, mContactIdForJoin);
1396         startActivityForResult(intent, REQUEST_CODE_JOIN);
1397     }
1398 
1399     /**
1400      * Performs aggregation with the contact selected by the user from suggestions or A-Z list.
1401      */
joinAggregate(final long contactId)1402     private void joinAggregate(final long contactId) {
1403         Intent intent = ContactSaveService.createJoinContactsIntent(mContext, mContactIdForJoin,
1404                 contactId, mContactWritableForJoin,
1405                 ContactEditorActivity.class, ContactEditorActivity.ACTION_JOIN_COMPLETED);
1406         mContext.startService(intent);
1407     }
1408 
1409     /**
1410      * Returns true if there is at least one writable raw contact in the current contact.
1411      */
isContactWritable()1412     private boolean isContactWritable() {
1413         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
1414         int size = mState.size();
1415         for (int i = 0; i < size; i++) {
1416             RawContactDelta entity = mState.get(i);
1417             final AccountType type = entity.getAccountType(accountTypes);
1418             if (type.areContactsWritable()) {
1419                 return true;
1420             }
1421         }
1422         return false;
1423     }
1424 
isEditingUserProfile()1425     private boolean isEditingUserProfile() {
1426         return mNewLocalProfile || mIsUserProfile;
1427     }
1428 
1429     public static interface Listener {
1430         /**
1431          * Contact was not found, so somehow close this fragment. This is raised after a contact
1432          * is removed via Menu/Delete (unless it was a new contact)
1433          */
onContactNotFound()1434         void onContactNotFound();
1435 
1436         /**
1437          * Contact was split, so we can close now.
1438          * @param newLookupUri The lookup uri of the new contact that should be shown to the user.
1439          * The editor tries best to chose the most natural contact here.
1440          */
onContactSplit(Uri newLookupUri)1441         void onContactSplit(Uri newLookupUri);
1442 
1443         /**
1444          * User has tapped Revert, close the fragment now.
1445          */
onReverted()1446         void onReverted();
1447 
1448         /**
1449          * Contact was saved and the Fragment can now be closed safely.
1450          */
onSaveFinished(Intent resultIntent)1451         void onSaveFinished(Intent resultIntent);
1452 
1453         /**
1454          * User switched to editing a different contact (a suggestion from the
1455          * aggregation engine).
1456          */
onEditOtherContactRequested( Uri contactLookupUri, ArrayList<ContentValues> contentValues)1457         void onEditOtherContactRequested(
1458                 Uri contactLookupUri, ArrayList<ContentValues> contentValues);
1459 
1460         /**
1461          * Contact is being created for an external account that provides its own
1462          * new contact activity.
1463          */
onCustomCreateContactActivityRequested(AccountWithDataSet account, Bundle intentExtras)1464         void onCustomCreateContactActivityRequested(AccountWithDataSet account,
1465                 Bundle intentExtras);
1466 
1467         /**
1468          * The edited raw contact belongs to an external account that provides
1469          * its own edit activity.
1470          *
1471          * @param redirect indicates that the current editor should be closed
1472          *            before the custom editor is shown.
1473          */
onCustomEditContactActivityRequested(AccountWithDataSet account, Uri rawContactUri, Bundle intentExtras, boolean redirect)1474         void onCustomEditContactActivityRequested(AccountWithDataSet account, Uri rawContactUri,
1475                 Bundle intentExtras, boolean redirect);
1476 
onDeleteRequested(Uri contactUri)1477         void onDeleteRequested(Uri contactUri);
1478     }
1479 
1480     private class EntityDeltaComparator implements Comparator<RawContactDelta> {
1481         /**
1482          * Compare EntityDeltas for sorting the stack of editors.
1483          */
1484         @Override
compare(RawContactDelta one, RawContactDelta two)1485         public int compare(RawContactDelta one, RawContactDelta two) {
1486             // Check direct equality
1487             if (one.equals(two)) {
1488                 return 0;
1489             }
1490 
1491             final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
1492             String accountType1 = one.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
1493             String dataSet1 = one.getValues().getAsString(RawContacts.DATA_SET);
1494             final AccountType type1 = accountTypes.getAccountType(accountType1, dataSet1);
1495             String accountType2 = two.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
1496             String dataSet2 = two.getValues().getAsString(RawContacts.DATA_SET);
1497             final AccountType type2 = accountTypes.getAccountType(accountType2, dataSet2);
1498 
1499             // Check read-only. Sort read/write before read-only.
1500             if (!type1.areContactsWritable() && type2.areContactsWritable()) {
1501                 return 1;
1502             } else if (type1.areContactsWritable() && !type2.areContactsWritable()) {
1503                 return -1;
1504             }
1505 
1506             // Check account type. Sort Google before non-Google.
1507             boolean skipAccountTypeCheck = false;
1508             boolean isGoogleAccount1 = type1 instanceof GoogleAccountType;
1509             boolean isGoogleAccount2 = type2 instanceof GoogleAccountType;
1510             if (isGoogleAccount1 && !isGoogleAccount2) {
1511                 return -1;
1512             } else if (!isGoogleAccount1 && isGoogleAccount2) {
1513                 return 1;
1514             } else if (isGoogleAccount1 && isGoogleAccount2){
1515                 skipAccountTypeCheck = true;
1516             }
1517 
1518             int value;
1519             if (!skipAccountTypeCheck) {
1520                 // Sort accounts with type before accounts without types.
1521                 if (type1.accountType != null && type2.accountType == null) {
1522                     return -1;
1523                 } else if (type1.accountType == null && type2.accountType != null) {
1524                     return 1;
1525                 }
1526 
1527                 if (type1.accountType != null && type2.accountType != null) {
1528                     value = type1.accountType.compareTo(type2.accountType);
1529                     if (value != 0) {
1530                         return value;
1531                     }
1532                 }
1533 
1534                 // Fall back to data set. Sort accounts with data sets before
1535                 // those without.
1536                 if (type1.dataSet != null && type2.dataSet == null) {
1537                     return -1;
1538                 } else if (type1.dataSet == null && type2.dataSet != null) {
1539                     return 1;
1540                 }
1541 
1542                 if (type1.dataSet != null && type2.dataSet != null) {
1543                     value = type1.dataSet.compareTo(type2.dataSet);
1544                     if (value != 0) {
1545                         return value;
1546                     }
1547                 }
1548             }
1549 
1550             // Check account name
1551             String oneAccount = one.getAccountName();
1552             if (oneAccount == null) oneAccount = "";
1553             String twoAccount = two.getAccountName();
1554             if (twoAccount == null) twoAccount = "";
1555             value = oneAccount.compareTo(twoAccount);
1556             if (value != 0) {
1557                 return value;
1558             }
1559 
1560             // Both are in the same account, fall back to contact ID
1561             Long oneId = one.getRawContactId();
1562             Long twoId = two.getRawContactId();
1563             if (oneId == null) {
1564                 return -1;
1565             } else if (twoId == null) {
1566                 return 1;
1567             }
1568 
1569             return (int)(oneId - twoId);
1570         }
1571     }
1572 
1573     /**
1574      * Returns the contact ID for the currently edited contact or 0 if the contact is new.
1575      */
getContactId()1576     protected long getContactId() {
1577         for (RawContactDelta rawContact : mState) {
1578             Long contactId = rawContact.getValues().getAsLong(RawContacts.CONTACT_ID);
1579             if (contactId != null) {
1580                 return contactId;
1581             }
1582         }
1583         return 0;
1584     }
1585 
1586     /**
1587      * Triggers an asynchronous search for aggregation suggestions.
1588      */
acquireAggregationSuggestions(Context context, RawContactEditorView rawContactEditor)1589     private void acquireAggregationSuggestions(Context context,
1590             RawContactEditorView rawContactEditor) {
1591         long rawContactId = rawContactEditor.getRawContactId();
1592         if (mAggregationSuggestionsRawContactId != rawContactId
1593                 && mAggregationSuggestionView != null) {
1594             mAggregationSuggestionView.setVisibility(View.GONE);
1595             mAggregationSuggestionView = null;
1596             mAggregationSuggestionEngine.reset();
1597         }
1598 
1599         mAggregationSuggestionsRawContactId = rawContactId;
1600 
1601         if (mAggregationSuggestionEngine == null) {
1602             mAggregationSuggestionEngine = new AggregationSuggestionEngine(context);
1603             mAggregationSuggestionEngine.setListener(this);
1604             mAggregationSuggestionEngine.start();
1605         }
1606 
1607         mAggregationSuggestionEngine.setContactId(getContactId());
1608 
1609         LabeledEditorView nameEditor = rawContactEditor.getNameEditor();
1610         mAggregationSuggestionEngine.onNameChange(nameEditor.getValues());
1611     }
1612 
1613     @Override
onAggregationSuggestionChange()1614     public void onAggregationSuggestionChange() {
1615         Activity activity = getActivity();
1616         if ((activity != null && activity.isFinishing())
1617                 || !isVisible() ||  mState.isEmpty() || mStatus != Status.EDITING) {
1618             return;
1619         }
1620 
1621         UiClosables.closeQuietly(mAggregationSuggestionPopup);
1622 
1623         if (mAggregationSuggestionEngine.getSuggestedContactCount() == 0) {
1624             return;
1625         }
1626 
1627         final RawContactEditorView rawContactView =
1628                 (RawContactEditorView)getRawContactEditorView(mAggregationSuggestionsRawContactId);
1629         if (rawContactView == null) {
1630             return; // Raw contact deleted?
1631         }
1632         final View anchorView = rawContactView.findViewById(R.id.anchor_view);
1633         mAggregationSuggestionPopup = new ListPopupWindow(mContext, null);
1634         mAggregationSuggestionPopup.setAnchorView(anchorView);
1635         mAggregationSuggestionPopup.setWidth(anchorView.getWidth());
1636         mAggregationSuggestionPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
1637         mAggregationSuggestionPopup.setAdapter(
1638                 new AggregationSuggestionAdapter(getActivity(),
1639                         mState.size() == 1 && mState.get(0).isContactInsert(),
1640                         this, mAggregationSuggestionEngine.getSuggestions()));
1641         mAggregationSuggestionPopup.setOnItemClickListener(mAggregationSuggestionItemClickListener);
1642         mAggregationSuggestionPopup.show();
1643     }
1644 
1645     @Override
onJoinAction(long contactId, List<Long> rawContactIdList)1646     public void onJoinAction(long contactId, List<Long> rawContactIdList) {
1647         long rawContactIds[] = new long[rawContactIdList.size()];
1648         for (int i = 0; i < rawContactIds.length; i++) {
1649             rawContactIds[i] = rawContactIdList.get(i);
1650         }
1651         JoinSuggestedContactDialogFragment dialog =
1652                 new JoinSuggestedContactDialogFragment();
1653         Bundle args = new Bundle();
1654         args.putLongArray("rawContactIds", rawContactIds);
1655         dialog.setArguments(args);
1656         dialog.setTargetFragment(this, 0);
1657         try {
1658             dialog.show(getFragmentManager(), "join");
1659         } catch (Exception ex) {
1660             // No problem - the activity is no longer available to display the dialog
1661         }
1662     }
1663 
1664     public static class JoinSuggestedContactDialogFragment extends DialogFragment {
1665 
1666         @Override
onCreateDialog(Bundle savedInstanceState)1667         public Dialog onCreateDialog(Bundle savedInstanceState) {
1668             return new AlertDialog.Builder(getActivity())
1669                     .setIconAttribute(android.R.attr.alertDialogIcon)
1670                     .setMessage(R.string.aggregation_suggestion_join_dialog_message)
1671                     .setPositiveButton(android.R.string.yes,
1672                         new DialogInterface.OnClickListener() {
1673                             @Override
1674                             public void onClick(DialogInterface dialog, int whichButton) {
1675                                 ContactEditorFragment targetFragment =
1676                                         (ContactEditorFragment) getTargetFragment();
1677                                 long rawContactIds[] =
1678                                         getArguments().getLongArray("rawContactIds");
1679                                 targetFragment.doJoinSuggestedContact(rawContactIds);
1680                             }
1681                         }
1682                     )
1683                     .setNegativeButton(android.R.string.no, null)
1684                     .create();
1685         }
1686     }
1687 
1688     /**
1689      * Joins the suggested contact (specified by the id's of constituent raw
1690      * contacts), save all changes, and stay in the editor.
1691      */
1692     protected void doJoinSuggestedContact(long[] rawContactIds) {
1693         if (!hasValidState() || mStatus != Status.EDITING) {
1694             return;
1695         }
1696 
1697         mState.setJoinWithRawContacts(rawContactIds);
1698         save(SaveMode.RELOAD);
1699     }
1700 
1701     @Override
1702     public void onEditAction(Uri contactLookupUri) {
1703         SuggestionEditConfirmationDialogFragment dialog =
1704                 new SuggestionEditConfirmationDialogFragment();
1705         Bundle args = new Bundle();
1706         args.putParcelable("contactUri", contactLookupUri);
1707         dialog.setArguments(args);
1708         dialog.setTargetFragment(this, 0);
1709         dialog.show(getFragmentManager(), "edit");
1710     }
1711 
1712     public static class SuggestionEditConfirmationDialogFragment extends DialogFragment {
1713 
1714         @Override
1715         public Dialog onCreateDialog(Bundle savedInstanceState) {
1716             return new AlertDialog.Builder(getActivity())
1717                     .setIconAttribute(android.R.attr.alertDialogIcon)
1718                     .setMessage(R.string.aggregation_suggestion_edit_dialog_message)
1719                     .setPositiveButton(android.R.string.yes,
1720                         new DialogInterface.OnClickListener() {
1721                             @Override
1722                             public void onClick(DialogInterface dialog, int whichButton) {
1723                                 ContactEditorFragment targetFragment =
1724                                         (ContactEditorFragment) getTargetFragment();
1725                                 Uri contactUri =
1726                                         getArguments().getParcelable("contactUri");
1727                                 targetFragment.doEditSuggestedContact(contactUri);
1728                             }
1729                         }
1730                     )
1731                     .setNegativeButton(android.R.string.no, null)
1732                     .create();
1733         }
1734     }
1735 
1736     /**
1737      * Abandons the currently edited contact and switches to editing the suggested
1738      * one, transferring all the data there
1739      */
1740     protected void doEditSuggestedContact(Uri contactUri) {
1741         if (mListener != null) {
1742             // make sure we don't save this contact when closing down
1743             mStatus = Status.CLOSING;
1744             mListener.onEditOtherContactRequested(
1745                     contactUri, mState.get(0).getContentValues());
1746         }
1747     }
1748 
1749     public void setAggregationSuggestionViewEnabled(boolean enabled) {
1750         if (mAggregationSuggestionView == null) {
1751             return;
1752         }
1753 
1754         LinearLayout itemList = (LinearLayout) mAggregationSuggestionView.findViewById(
1755                 R.id.aggregation_suggestions);
1756         int count = itemList.getChildCount();
1757         for (int i = 0; i < count; i++) {
1758             itemList.getChildAt(i).setEnabled(enabled);
1759         }
1760     }
1761 
1762     @Override
1763     public void onSaveInstanceState(Bundle outState) {
1764         outState.putParcelable(KEY_URI, mLookupUri);
1765         outState.putString(KEY_ACTION, mAction);
1766 
1767         if (hasValidState()) {
1768             // Store entities with modifications
1769             outState.putParcelable(KEY_EDIT_STATE, mState);
1770         }
1771         outState.putLong(KEY_RAW_CONTACT_ID_REQUESTING_PHOTO, mRawContactIdRequestingPhoto);
1772         outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator);
1773         outState.putParcelable(KEY_CURRENT_PHOTO_URI, mCurrentPhotoUri);
1774         outState.putLong(KEY_CONTACT_ID_FOR_JOIN, mContactIdForJoin);
1775         outState.putBoolean(KEY_CONTACT_WRITABLE_FOR_JOIN, mContactWritableForJoin);
1776         outState.putLong(KEY_SHOW_JOIN_SUGGESTIONS, mAggregationSuggestionsRawContactId);
1777         outState.putBoolean(KEY_ENABLED, mEnabled);
1778         outState.putBoolean(KEY_NEW_LOCAL_PROFILE, mNewLocalProfile);
1779         outState.putBoolean(KEY_DISABLE_DELETE_MENU_OPTION, mDisableDeleteMenuOption);
1780         outState.putBoolean(KEY_IS_USER_PROFILE, mIsUserProfile);
1781         outState.putInt(KEY_STATUS, mStatus);
1782         outState.putParcelable(KEY_UPDATED_PHOTOS, mUpdatedPhotos);
1783         outState.putBoolean(KEY_HAS_NEW_CONTACT, mHasNewContact);
1784         outState.putBoolean(KEY_IS_EDIT, mIsEdit);
1785         outState.putBoolean(KEY_NEW_CONTACT_READY, mNewContactDataReady);
1786         outState.putBoolean(KEY_EXISTING_CONTACT_READY, mExistingContactDataReady);
1787         outState.putParcelableArrayList(KEY_RAW_CONTACTS,
1788                 mRawContacts == null ?
1789                 Lists.<RawContact> newArrayList() :  Lists.newArrayList(mRawContacts));
1790         outState.putBoolean(KEY_SEND_TO_VOICE_MAIL_STATE, mSendToVoicemailState);
1791         outState.putString(KEY_CUSTOM_RINGTONE, mCustomRingtone);
1792         outState.putBoolean(KEY_ARE_PHONE_OPTIONS_CHANGEABLE, mArePhoneOptionsChangable);
1793 
1794         super.onSaveInstanceState(outState);
1795     }
1796 
1797     @Override
1798     public void onActivityResult(int requestCode, int resultCode, Intent data) {
1799         if (mStatus == Status.SUB_ACTIVITY) {
1800             mStatus = Status.EDITING;
1801         }
1802 
1803         // See if the photo selection handler handles this result.
1804         if (mCurrentPhotoHandler != null && mCurrentPhotoHandler.handlePhotoActivityResult(
1805                 requestCode, resultCode, data)) {
1806             return;
1807         }
1808 
1809         switch (requestCode) {
1810             case REQUEST_CODE_JOIN: {
1811                 // Ignore failed requests
1812                 if (resultCode != Activity.RESULT_OK) return;
1813                 if (data != null) {
1814                     final long contactId = ContentUris.parseId(data.getData());
1815                     joinAggregate(contactId);
1816                 }
1817                 break;
1818             }
1819             case REQUEST_CODE_ACCOUNTS_CHANGED: {
1820                 // Bail if the account selector was not successful.
1821                 if (resultCode != Activity.RESULT_OK) {
1822                     mListener.onReverted();
1823                     return;
1824                 }
1825                 // If there's an account specified, use it.
1826                 if (data != null) {
1827                     AccountWithDataSet account = data.getParcelableExtra(Intents.Insert.ACCOUNT);
1828                     if (account != null) {
1829                         createContact(account);
1830                         return;
1831                     }
1832                 }
1833                 // If there isn't an account specified, then this is likely a phone-local
1834                 // contact, so we should continue setting up the editor by automatically selecting
1835                 // the most appropriate account.
1836                 createContact();
1837                 break;
1838             }
1839             case REQUEST_CODE_PICK_RINGTONE: {
1840                 if (data != null) {
1841                     final Uri pickedUri = data.getParcelableExtra(
1842                             RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
1843                     handleRingtonePicked(pickedUri);
1844                 }
1845                 break;
1846             }
1847         }
1848     }
1849 
1850     /**
1851      * Sets the photo stored in mPhoto and writes it to the RawContact with the given id
1852      */
1853     private void setPhoto(long rawContact, Bitmap photo, Uri photoUri) {
1854         BaseRawContactEditorView requestingEditor = getRawContactEditorView(rawContact);
1855 
1856         if (photo == null || photo.getHeight() < 0 || photo.getWidth() < 0) {
1857             // This is unexpected.
1858             Log.w(TAG, "Invalid bitmap passed to setPhoto()");
1859         }
1860 
1861         if (requestingEditor != null) {
1862             requestingEditor.setPhotoBitmap(photo);
1863         } else {
1864             Log.w(TAG, "The contact that requested the photo is no longer present.");
1865         }
1866 
1867         mUpdatedPhotos.putParcelable(String.valueOf(rawContact), photoUri);
1868     }
1869 
1870     /**
1871      * Finds raw contact editor view for the given rawContactId.
1872      */
1873     public BaseRawContactEditorView getRawContactEditorView(long rawContactId) {
1874         for (int i = 0; i < mContent.getChildCount(); i++) {
1875             final View childView = mContent.getChildAt(i);
1876             if (childView instanceof BaseRawContactEditorView) {
1877                 final BaseRawContactEditorView editor = (BaseRawContactEditorView) childView;
1878                 if (editor.getRawContactId() == rawContactId) {
1879                     return editor;
1880                 }
1881             }
1882         }
1883         return null;
1884     }
1885 
1886     /**
1887      * Returns true if there is currently more than one photo on screen.
1888      */
1889     private boolean hasMoreThanOnePhoto() {
1890         int countWithPicture = 0;
1891         final int numEntities = mState.size();
1892         for (int i = 0; i < numEntities; i++) {
1893             final RawContactDelta entity = mState.get(i);
1894             if (entity.isVisible()) {
1895                 final ValuesDelta primary = entity.getPrimaryEntry(Photo.CONTENT_ITEM_TYPE);
1896                 if (primary != null && primary.getPhoto() != null) {
1897                     countWithPicture++;
1898                 } else {
1899                     final long rawContactId = entity.getRawContactId();
1900                     final Uri uri = mUpdatedPhotos.getParcelable(String.valueOf(rawContactId));
1901                     if (uri != null) {
1902                         try {
1903                             mContext.getContentResolver().openInputStream(uri);
1904                             countWithPicture++;
1905                         } catch (FileNotFoundException e) {
1906                         }
1907                     }
1908                 }
1909 
1910                 if (countWithPicture > 1) {
1911                     return true;
1912                 }
1913             }
1914         }
1915         return false;
1916     }
1917 
1918     /**
1919      * The listener for the data loader
1920      */
1921     private final LoaderManager.LoaderCallbacks<Contact> mDataLoaderListener =
1922             new LoaderCallbacks<Contact>() {
1923         @Override
1924         public Loader<Contact> onCreateLoader(int id, Bundle args) {
1925             mLoaderStartTime = SystemClock.elapsedRealtime();
1926             return new ContactLoader(mContext, mLookupUri, true);
1927         }
1928 
1929         @Override
1930         public void onLoadFinished(Loader<Contact> loader, Contact data) {
1931             final long loaderCurrentTime = SystemClock.elapsedRealtime();
1932             Log.v(TAG, "Time needed for loading: " + (loaderCurrentTime-mLoaderStartTime));
1933             if (!data.isLoaded()) {
1934                 // Item has been deleted
1935                 Log.i(TAG, "No contact found. Closing activity");
1936                 if (mListener != null) mListener.onContactNotFound();
1937                 return;
1938             }
1939 
1940             mStatus = Status.EDITING;
1941             mLookupUri = data.getLookupUri();
1942             final long setDataStartTime = SystemClock.elapsedRealtime();
1943             setData(data);
1944             final long setDataEndTime = SystemClock.elapsedRealtime();
1945 
1946             Log.v(TAG, "Time needed for setting UI: " + (setDataEndTime-setDataStartTime));
1947         }
1948 
1949         @Override
1950         public void onLoaderReset(Loader<Contact> loader) {
1951         }
1952     };
1953 
1954     /**
1955      * The listener for the group meta data loader for all groups.
1956      */
1957     private final LoaderManager.LoaderCallbacks<Cursor> mGroupLoaderListener =
1958             new LoaderCallbacks<Cursor>() {
1959 
1960         @Override
1961         public CursorLoader onCreateLoader(int id, Bundle args) {
1962             return new GroupMetaDataLoader(mContext, Groups.CONTENT_URI);
1963         }
1964 
1965         @Override
1966         public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
1967             mGroupMetaData = data;
1968             bindGroupMetaData();
1969         }
1970 
1971         @Override
1972         public void onLoaderReset(Loader<Cursor> loader) {
1973         }
1974     };
1975 
1976     @Override
1977     public void onSplitContactConfirmed() {
1978         if (mState.isEmpty()) {
1979             // This may happen when this Fragment is recreated by the system during users
1980             // confirming the split action (and thus this method is called just before onCreate()),
1981             // for example.
1982             Log.e(TAG, "mState became null during the user's confirming split action. " +
1983                     "Cannot perform the save action.");
1984             return;
1985         }
1986 
1987         mState.markRawContactsForSplitting();
1988         save(SaveMode.SPLIT);
1989     }
1990 
1991     /**
1992      * Custom photo handler for the editor.  The inner listener that this creates also has a
1993      * reference to the editor and acts as an {@link EditorListener}, and uses that editor to hold
1994      * state information in several of the listener methods.
1995      */
1996     private final class PhotoHandler extends PhotoSelectionHandler {
1997 
1998         final long mRawContactId;
1999         private final BaseRawContactEditorView mEditor;
2000         private final PhotoActionListener mPhotoEditorListener;
2001 
2002         public PhotoHandler(Context context, BaseRawContactEditorView editor, int photoMode,
2003                 RawContactDeltaList state) {
2004             super(context, editor.getPhotoEditor(), photoMode, false, state);
2005             mEditor = editor;
2006             mRawContactId = editor.getRawContactId();
2007             mPhotoEditorListener = new PhotoEditorListener();
2008         }
2009 
2010         @Override
2011         public PhotoActionListener getListener() {
2012             return mPhotoEditorListener;
2013         }
2014 
2015         @Override
2016         public void startPhotoActivity(Intent intent, int requestCode, Uri photoUri) {
2017             mRawContactIdRequestingPhoto = mEditor.getRawContactId();
2018             mCurrentPhotoHandler = this;
2019             mStatus = Status.SUB_ACTIVITY;
2020             mCurrentPhotoUri = photoUri;
2021             ContactEditorFragment.this.startActivityForResult(intent, requestCode);
2022         }
2023 
2024         private final class PhotoEditorListener extends PhotoSelectionHandler.PhotoActionListener
2025                 implements EditorListener {
2026 
2027             @Override
2028             public void onRequest(int request) {
2029                 if (!hasValidState()) return;
2030 
2031                 if (request == EditorListener.REQUEST_PICK_PHOTO) {
2032                     onClick(mEditor.getPhotoEditor());
2033                 }
2034             }
2035 
2036             @Override
2037             public void onDeleteRequested(Editor removedEditor) {
2038                 // The picture cannot be deleted, it can only be removed, which is handled by
2039                 // onRemovePictureChosen()
2040             }
2041 
2042             /**
2043              * User has chosen to set the selected photo as the (super) primary photo
2044              */
2045             @Override
2046             public void onUseAsPrimaryChosen() {
2047                 // Set the IsSuperPrimary for each editor
2048                 int count = mContent.getChildCount();
2049                 for (int i = 0; i < count; i++) {
2050                     final View childView = mContent.getChildAt(i);
2051                     if (childView instanceof BaseRawContactEditorView) {
2052                         final BaseRawContactEditorView editor =
2053                                 (BaseRawContactEditorView) childView;
2054                         final PhotoEditorView photoEditor = editor.getPhotoEditor();
2055                         photoEditor.setSuperPrimary(editor == mEditor);
2056                     }
2057                 }
2058                 bindEditors();
2059             }
2060 
2061             /**
2062              * User has chosen to remove a picture
2063              */
2064             @Override
2065             public void onRemovePictureChosen() {
2066                 mEditor.setPhotoBitmap(null);
2067 
2068                 // Prevent bitmap from being restored if rotate the device.
2069                 // (only if we first chose a new photo before removing it)
2070                 mUpdatedPhotos.remove(String.valueOf(mRawContactId));
2071                 bindEditors();
2072             }
2073 
2074             @Override
2075             public void onPhotoSelected(Uri uri) throws FileNotFoundException {
2076                 final Bitmap bitmap = ContactPhotoUtils.getBitmapFromUri(mContext, uri);
2077                 setPhoto(mRawContactId, bitmap, uri);
2078                 mCurrentPhotoHandler = null;
2079                 bindEditors();
2080             }
2081 
2082             @Override
2083             public Uri getCurrentPhotoUri() {
2084                 return mCurrentPhotoUri;
2085             }
2086 
2087             @Override
2088             public void onPhotoSelectionDismissed() {
2089                 // Nothing to do.
2090             }
2091         }
2092     }
2093 }
2094