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