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