OSDN Git Service

Fixed NPE in adding a phone number to an existing contact from call log
[android-x86/packages-apps-Contacts.git] / src / com / android / contacts / ui / EditContactActivity.java
1 /*
2  * Copyright (C) 2009 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.ui;
18
19 import com.android.contacts.ContactsListActivity;
20 import com.android.contacts.ContactsSearchManager;
21 import com.android.contacts.ContactsUtils;
22 import com.android.contacts.R;
23 import com.android.contacts.model.ContactsSource;
24 import com.android.contacts.model.Editor;
25 import com.android.contacts.model.EntityDelta;
26 import com.android.contacts.model.EntityModifier;
27 import com.android.contacts.model.EntitySet;
28 import com.android.contacts.model.GoogleSource;
29 import com.android.contacts.model.Sources;
30 import com.android.contacts.model.ContactsSource.EditType;
31 import com.android.contacts.model.Editor.EditorListener;
32 import com.android.contacts.model.EntityDelta.ValuesDelta;
33 import com.android.contacts.ui.widget.BaseContactEditorView;
34 import com.android.contacts.ui.widget.PhotoEditorView;
35 import com.android.contacts.util.EmptyService;
36 import com.android.contacts.util.WeakAsyncTask;
37 import com.google.android.collect.Lists;
38
39 import android.accounts.Account;
40 import android.app.Activity;
41 import android.app.AlertDialog;
42 import android.app.Dialog;
43 import android.app.ProgressDialog;
44 import android.content.ActivityNotFoundException;
45 import android.content.ContentProviderOperation;
46 import android.content.ContentProviderResult;
47 import android.content.ContentResolver;
48 import android.content.ContentUris;
49 import android.content.ContentValues;
50 import android.content.Context;
51 import android.content.DialogInterface;
52 import android.content.Entity;
53 import android.content.Intent;
54 import android.content.OperationApplicationException;
55 import android.content.ContentProviderOperation.Builder;
56 import android.database.Cursor;
57 import android.graphics.Bitmap;
58 import android.media.MediaScannerConnection;
59 import android.net.Uri;
60 import android.os.Bundle;
61 import android.os.Environment;
62 import android.os.RemoteException;
63 import android.provider.ContactsContract;
64 import android.provider.MediaStore;
65 import android.provider.ContactsContract.AggregationExceptions;
66 import android.provider.ContactsContract.Contacts;
67 import android.provider.ContactsContract.RawContacts;
68 import android.provider.ContactsContract.CommonDataKinds.Email;
69 import android.provider.ContactsContract.CommonDataKinds.Phone;
70 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
71 import android.provider.ContactsContract.Contacts.Data;
72 import android.util.Log;
73 import android.view.ContextThemeWrapper;
74 import android.view.LayoutInflater;
75 import android.view.Menu;
76 import android.view.MenuInflater;
77 import android.view.MenuItem;
78 import android.view.View;
79 import android.view.ViewGroup;
80 import android.widget.ArrayAdapter;
81 import android.widget.LinearLayout;
82 import android.widget.ListAdapter;
83 import android.widget.TextView;
84 import android.widget.Toast;
85
86 import java.io.File;
87 import java.lang.ref.WeakReference;
88 import java.text.SimpleDateFormat;
89 import java.util.ArrayList;
90 import java.util.Collections;
91 import java.util.Comparator;
92 import java.util.Date;
93
94 /**
95  * Activity for editing or inserting a contact.
96  */
97 public final class EditContactActivity extends Activity
98         implements View.OnClickListener, Comparator<EntityDelta> {
99
100     private static final String TAG = "EditContactActivity";
101
102     /** The launch code when picking a photo and the raw data is returned */
103     private static final int PHOTO_PICKED_WITH_DATA = 3021;
104
105     /** The launch code when a contact to join with is returned */
106     private static final int REQUEST_JOIN_CONTACT = 3022;
107
108     /** The launch code when taking a picture */
109     private static final int CAMERA_WITH_DATA = 3023;
110
111     private static final String KEY_EDIT_STATE = "state";
112     private static final String KEY_RAW_CONTACT_ID_REQUESTING_PHOTO = "photorequester";
113     private static final String KEY_VIEW_ID_GENERATOR = "viewidgenerator";
114
115     /** The result code when view activity should close after edit returns */
116     public static final int RESULT_CLOSE_VIEW_ACTIVITY = 777;
117
118     public static final int SAVE_MODE_DEFAULT = 0;
119     public static final int SAVE_MODE_SPLIT = 1;
120     public static final int SAVE_MODE_JOIN = 2;
121
122     private long mRawContactIdRequestingPhoto = -1;
123
124     private static final int DIALOG_CONFIRM_DELETE = 1;
125     private static final int DIALOG_CONFIRM_READONLY_DELETE = 2;
126     private static final int DIALOG_CONFIRM_MULTIPLE_DELETE = 3;
127     private static final int DIALOG_CONFIRM_READONLY_HIDE = 4;
128
129     private static final int ICON_SIZE = 96;
130
131     private static final File PHOTO_DIR = new File(Environment.getExternalStorageDirectory(),
132             "com.android.contacts.icon");
133
134     private File mCurrentPhotoFile;
135
136     String mQuerySelection;
137
138     private long mContactIdForJoin;
139
140     private static final int STATUS_LOADING = 0;
141     private static final int STATUS_EDITING = 1;
142     private static final int STATUS_SAVING = 2;
143
144     private int mStatus;
145
146     EntitySet mState;
147
148     /** The linear layout holding the ContactEditorViews */
149     LinearLayout mContent;
150
151     private ArrayList<Dialog> mManagedDialogs = Lists.newArrayList();
152
153     private ViewIdGenerator mViewIdGenerator;
154
155     @Override
156     protected void onCreate(Bundle icicle) {
157         super.onCreate(icicle);
158
159         final Intent intent = getIntent();
160         final String action = intent.getAction();
161
162         setContentView(R.layout.act_edit);
163
164         // Build editor and listen for photo requests
165         mContent = (LinearLayout) findViewById(R.id.editors);
166
167         findViewById(R.id.btn_done).setOnClickListener(this);
168         findViewById(R.id.btn_discard).setOnClickListener(this);
169
170         // Handle initial actions only when existing state missing
171         final boolean hasIncomingState = icicle != null && icicle.containsKey(KEY_EDIT_STATE);
172
173         if (Intent.ACTION_EDIT.equals(action) && !hasIncomingState) {
174             setTitle(R.string.editContact_title_edit);
175             mStatus = STATUS_LOADING;
176
177             // Read initial state from database
178             new QueryEntitiesTask(this).execute(intent);
179         } else if (Intent.ACTION_INSERT.equals(action) && !hasIncomingState) {
180             setTitle(R.string.editContact_title_insert);
181             mStatus = STATUS_EDITING;
182             // Trigger dialog to pick account type
183             doAddAction();
184         }
185
186         if (icicle == null) {
187             // If icicle is non-null, onRestoreInstanceState() will restore the generator.
188             mViewIdGenerator = new ViewIdGenerator();
189         }
190     }
191
192     private static class QueryEntitiesTask extends
193             WeakAsyncTask<Intent, Void, EntitySet, EditContactActivity> {
194
195         private String mSelection;
196
197         public QueryEntitiesTask(EditContactActivity target) {
198             super(target);
199         }
200
201         @Override
202         protected EntitySet doInBackground(EditContactActivity target, Intent... params) {
203             final Intent intent = params[0];
204
205             final ContentResolver resolver = target.getContentResolver();
206
207             // Handle both legacy and new authorities
208             final Uri data = intent.getData();
209             final String authority = data.getAuthority();
210             final String mimeType = intent.resolveType(resolver);
211
212             mSelection = "0";
213             if (ContactsContract.AUTHORITY.equals(authority)) {
214                 if (Contacts.CONTENT_ITEM_TYPE.equals(mimeType)) {
215                     // Handle selected aggregate
216                     final long contactId = ContentUris.parseId(data);
217                     mSelection = RawContacts.CONTACT_ID + "=" + contactId;
218                 } else if (RawContacts.CONTENT_ITEM_TYPE.equals(mimeType)) {
219                     final long rawContactId = ContentUris.parseId(data);
220                     final long contactId = ContactsUtils.queryForContactId(resolver, rawContactId);
221                     mSelection = RawContacts.CONTACT_ID + "=" + contactId;
222                 }
223             } else if (android.provider.Contacts.AUTHORITY.equals(authority)) {
224                 final long rawContactId = ContentUris.parseId(data);
225                 mSelection = Data.RAW_CONTACT_ID + "=" + rawContactId;
226             }
227
228             return EntitySet.fromQuery(target.getContentResolver(), mSelection, null, null);
229         }
230
231         @Override
232         protected void onPostExecute(EditContactActivity target, EntitySet entitySet) {
233             target.mQuerySelection = mSelection;
234
235             // Load edit details in background
236             final Context context = target;
237             final Sources sources = Sources.getInstance(context);
238
239             // Handle any incoming values that should be inserted
240             final Bundle extras = target.getIntent().getExtras();
241             final boolean hasExtras = extras != null && extras.size() > 0;
242             final boolean hasState = entitySet.size() > 0;
243             if (hasExtras && hasState) {
244                 // Find source defining the first RawContact found
245                 final EntityDelta state = entitySet.get(0);
246                 final String accountType = state.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
247                 final ContactsSource source = sources.getInflatedSource(accountType,
248                         ContactsSource.LEVEL_CONSTRAINTS);
249                 EntityModifier.parseExtras(context, source, state, extras);
250             }
251
252             target.mState = entitySet;
253
254             // Bind UI to new background state
255             target.bindEditors();
256         }
257     }
258
259     @Override
260     protected void onSaveInstanceState(Bundle outState) {
261         if (hasValidState()) {
262             // Store entities with modifications
263             outState.putParcelable(KEY_EDIT_STATE, mState);
264         }
265
266         outState.putLong(KEY_RAW_CONTACT_ID_REQUESTING_PHOTO, mRawContactIdRequestingPhoto);
267         outState.putParcelable(KEY_VIEW_ID_GENERATOR, mViewIdGenerator);
268         super.onSaveInstanceState(outState);
269     }
270
271     @Override
272     protected void onRestoreInstanceState(Bundle savedInstanceState) {
273         // Read modifications from instance
274         mState = savedInstanceState.<EntitySet> getParcelable(KEY_EDIT_STATE);
275         mRawContactIdRequestingPhoto = savedInstanceState.getLong(
276                 KEY_RAW_CONTACT_ID_REQUESTING_PHOTO);
277         mViewIdGenerator = savedInstanceState.getParcelable(KEY_VIEW_ID_GENERATOR);
278         bindEditors();
279
280         super.onRestoreInstanceState(savedInstanceState);
281     }
282
283     @Override
284     protected void onDestroy() {
285         super.onDestroy();
286
287         for (Dialog dialog : mManagedDialogs) {
288             dismissDialog(dialog);
289         }
290     }
291
292     @Override
293     protected Dialog onCreateDialog(int id, Bundle bundle) {
294         switch (id) {
295             case DIALOG_CONFIRM_DELETE:
296                 return new AlertDialog.Builder(this)
297                         .setTitle(R.string.deleteConfirmation_title)
298                         .setIcon(android.R.drawable.ic_dialog_alert)
299                         .setMessage(R.string.deleteConfirmation)
300                         .setNegativeButton(android.R.string.cancel, null)
301                         .setPositiveButton(android.R.string.ok, new DeleteClickListener())
302                         .setCancelable(false)
303                         .create();
304             case DIALOG_CONFIRM_READONLY_DELETE:
305                 return new AlertDialog.Builder(this)
306                         .setTitle(R.string.deleteConfirmation_title)
307                         .setIcon(android.R.drawable.ic_dialog_alert)
308                         .setMessage(R.string.readOnlyContactDeleteConfirmation)
309                         .setNegativeButton(android.R.string.cancel, null)
310                         .setPositiveButton(android.R.string.ok, new DeleteClickListener())
311                         .setCancelable(false)
312                         .create();
313             case DIALOG_CONFIRM_MULTIPLE_DELETE:
314                 return new AlertDialog.Builder(this)
315                         .setTitle(R.string.deleteConfirmation_title)
316                         .setIcon(android.R.drawable.ic_dialog_alert)
317                         .setMessage(R.string.multipleContactDeleteConfirmation)
318                         .setNegativeButton(android.R.string.cancel, null)
319                         .setPositiveButton(android.R.string.ok, new DeleteClickListener())
320                         .setCancelable(false)
321                         .create();
322             case DIALOG_CONFIRM_READONLY_HIDE:
323                 return new AlertDialog.Builder(this)
324                         .setTitle(R.string.deleteConfirmation_title)
325                         .setIcon(android.R.drawable.ic_dialog_alert)
326                         .setMessage(R.string.readOnlyContactWarning)
327                         .setPositiveButton(android.R.string.ok, new DeleteClickListener())
328                         .setCancelable(false)
329                         .create();
330         }
331         return null;
332     }
333
334     /**
335      * Start managing this {@link Dialog} along with the {@link Activity}.
336      */
337     private void startManagingDialog(Dialog dialog) {
338         synchronized (mManagedDialogs) {
339             mManagedDialogs.add(dialog);
340         }
341     }
342
343     /**
344      * Show this {@link Dialog} and manage with the {@link Activity}.
345      */
346     void showAndManageDialog(Dialog dialog) {
347         startManagingDialog(dialog);
348         dialog.show();
349     }
350
351     /**
352      * Dismiss the given {@link Dialog}.
353      */
354     static void dismissDialog(Dialog dialog) {
355         try {
356             // Only dismiss when valid reference and still showing
357             if (dialog != null && dialog.isShowing()) {
358                 dialog.dismiss();
359             }
360         } catch (Exception e) {
361             Log.w(TAG, "Ignoring exception while dismissing dialog: " + e.toString());
362         }
363     }
364
365     /**
366      * Check if our internal {@link #mState} is valid, usually checked before
367      * performing user actions.
368      */
369     protected boolean hasValidState() {
370         return mStatus == STATUS_EDITING && mState != null && mState.size() > 0;
371     }
372
373     /**
374      * Rebuild the editors to match our underlying {@link #mState} object, usually
375      * called once we've parsed {@link Entity} data or have inserted a new
376      * {@link RawContacts}.
377      */
378     protected void bindEditors() {
379         if (mState == null) {
380             return;
381         }
382
383         final LayoutInflater inflater = (LayoutInflater) getSystemService(
384                 Context.LAYOUT_INFLATER_SERVICE);
385         final Sources sources = Sources.getInstance(this);
386
387         // Sort the editors
388         Collections.sort(mState, this);
389
390         // Remove any existing editors and rebuild any visible
391         mContent.removeAllViews();
392         int size = mState.size();
393         for (int i = 0; i < size; i++) {
394             // TODO ensure proper ordering of entities in the list
395             EntityDelta entity = mState.get(i);
396             final ValuesDelta values = entity.getValues();
397             if (!values.isVisible()) continue;
398
399             final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
400             final ContactsSource source = sources.getInflatedSource(accountType,
401                     ContactsSource.LEVEL_CONSTRAINTS);
402             final long rawContactId = values.getAsLong(RawContacts._ID);
403
404             BaseContactEditorView editor;
405             if (!source.readOnly) {
406                 editor = (BaseContactEditorView) inflater.inflate(R.layout.item_contact_editor,
407                         mContent, false);
408             } else {
409                 editor = (BaseContactEditorView) inflater.inflate(
410                         R.layout.item_read_only_contact_editor, mContent, false);
411             }
412             PhotoEditorView photoEditor = editor.getPhotoEditor();
413             photoEditor.setEditorListener(new PhotoListener(rawContactId, source.readOnly,
414                     photoEditor));
415
416             mContent.addView(editor);
417             editor.setState(entity, source, mViewIdGenerator);
418         }
419
420         // Show editor now that we've loaded state
421         mContent.setVisibility(View.VISIBLE);
422         mStatus = STATUS_EDITING;
423     }
424
425     /**
426      * Class that listens to requests coming from photo editors
427      */
428     private class PhotoListener implements EditorListener, DialogInterface.OnClickListener {
429         private long mRawContactId;
430         private boolean mReadOnly;
431         private PhotoEditorView mEditor;
432
433         public PhotoListener(long rawContactId, boolean readOnly, PhotoEditorView editor) {
434             mRawContactId = rawContactId;
435             mReadOnly = readOnly;
436             mEditor = editor;
437         }
438
439         public void onDeleted(Editor editor) {
440             // Do nothing
441         }
442
443         public void onRequest(int request) {
444             if (!hasValidState()) return;
445
446             if (request == EditorListener.REQUEST_PICK_PHOTO) {
447                 if (mEditor.hasSetPhoto()) {
448                     // There is an existing photo, offer to remove, replace, or promoto to primary
449                     createPhotoDialog().show();
450                 } else if (!mReadOnly) {
451                     // No photo set and not read-only, try to set the photo
452                     doPickPhotoAction(mRawContactId);
453                 }
454             }
455         }
456
457         /**
458          * Prepare dialog for picking a new {@link EditType} or entering a
459          * custom label. This dialog is limited to the valid types as determined
460          * by {@link EntityModifier}.
461          */
462         public Dialog createPhotoDialog() {
463             Context context = EditContactActivity.this;
464
465             // Wrap our context to inflate list items using correct theme
466             final Context dialogContext = new ContextThemeWrapper(context,
467                     android.R.style.Theme_Light);
468
469             String[] choices;
470             if (mReadOnly) {
471                 choices = new String[1];
472                 choices[0] = getString(R.string.use_photo_as_primary);
473             } else {
474                 choices = new String[3];
475                 choices[0] = getString(R.string.use_photo_as_primary);
476                 choices[1] = getString(R.string.removePicture);
477                 choices[2] = getString(R.string.changePicture);
478             }
479             final ListAdapter adapter = new ArrayAdapter<String>(dialogContext,
480                     android.R.layout.simple_list_item_1, choices);
481
482             final AlertDialog.Builder builder = new AlertDialog.Builder(dialogContext);
483             builder.setTitle(R.string.attachToContact);
484             builder.setSingleChoiceItems(adapter, -1, this);
485             return builder.create();
486         }
487
488         /**
489          * Called when something in the dialog is clicked
490          */
491         public void onClick(DialogInterface dialog, int which) {
492             dialog.dismiss();
493
494             switch (which) {
495                 case 0:
496                     // Set the photo as super primary
497                     mEditor.setSuperPrimary(true);
498
499                     // And set all other photos as not super primary
500                     int count = mContent.getChildCount();
501                     for (int i = 0; i < count; i++) {
502                         View childView = mContent.getChildAt(i);
503                         if (childView instanceof BaseContactEditorView) {
504                             BaseContactEditorView editor = (BaseContactEditorView) childView;
505                             PhotoEditorView photoEditor = editor.getPhotoEditor();
506                             if (!photoEditor.equals(mEditor)) {
507                                 photoEditor.setSuperPrimary(false);
508                             }
509                         }
510                     }
511                     break;
512
513                 case 1:
514                     // Remove the photo
515                     mEditor.setPhotoBitmap(null);
516                     break;
517
518                 case 2:
519                     // Pick a new photo for the contact
520                     doPickPhotoAction(mRawContactId);
521                     break;
522             }
523         }
524     }
525
526     /** {@inheritDoc} */
527     public void onClick(View view) {
528         switch (view.getId()) {
529             case R.id.btn_done:
530                 doSaveAction(SAVE_MODE_DEFAULT);
531                 break;
532             case R.id.btn_discard:
533                 doRevertAction();
534                 break;
535         }
536     }
537
538     /** {@inheritDoc} */
539     @Override
540     public void onBackPressed() {
541         doSaveAction(SAVE_MODE_DEFAULT);
542     }
543
544     /** {@inheritDoc} */
545     @Override
546     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
547         // Ignore failed requests
548         if (resultCode != RESULT_OK) return;
549
550         switch (requestCode) {
551             case PHOTO_PICKED_WITH_DATA: {
552                 BaseContactEditorView requestingEditor = null;
553                 for (int i = 0; i < mContent.getChildCount(); i++) {
554                     View childView = mContent.getChildAt(i);
555                     if (childView instanceof BaseContactEditorView) {
556                         BaseContactEditorView editor = (BaseContactEditorView) childView;
557                         if (editor.getRawContactId() == mRawContactIdRequestingPhoto) {
558                             requestingEditor = editor;
559                             break;
560                         }
561                     }
562                 }
563
564                 if (requestingEditor != null) {
565                     final Bitmap photo = data.getParcelableExtra("data");
566                     requestingEditor.setPhotoBitmap(photo);
567                     mRawContactIdRequestingPhoto = -1;
568                 } else {
569                     // The contact that requested the photo is no longer present.
570                     // TODO: Show error message
571                 }
572
573                 break;
574             }
575
576             case CAMERA_WITH_DATA: {
577                 doCropPhoto(mCurrentPhotoFile);
578                 break;
579             }
580
581             case REQUEST_JOIN_CONTACT: {
582                 if (resultCode == RESULT_OK && data != null) {
583                     final long contactId = ContentUris.parseId(data.getData());
584                     joinAggregate(contactId);
585                 }
586             }
587         }
588     }
589
590     @Override
591     public boolean onCreateOptionsMenu(Menu menu) {
592         super.onCreateOptionsMenu(menu);
593
594         MenuInflater inflater = getMenuInflater();
595         inflater.inflate(R.menu.edit, menu);
596
597
598         return true;
599     }
600
601     @Override
602     public boolean onPrepareOptionsMenu(Menu menu) {
603         menu.findItem(R.id.menu_split).setVisible(mState != null && mState.size() > 1);
604         return true;
605     }
606
607     @Override
608     public boolean onOptionsItemSelected(MenuItem item) {
609         switch (item.getItemId()) {
610             case R.id.menu_done:
611                 return doSaveAction(SAVE_MODE_DEFAULT);
612             case R.id.menu_discard:
613                 return doRevertAction();
614             case R.id.menu_add:
615                 return doAddAction();
616             case R.id.menu_delete:
617                 return doDeleteAction();
618             case R.id.menu_split:
619                 return doSplitContactAction();
620             case R.id.menu_join:
621                 return doJoinContactAction();
622         }
623         return false;
624     }
625
626     /**
627      * Background task for persisting edited contact data, using the changes
628      * defined by a set of {@link EntityDelta}. This task starts
629      * {@link EmptyService} to make sure the background thread can finish
630      * persisting in cases where the system wants to reclaim our process.
631      */
632     public static class PersistTask extends
633             WeakAsyncTask<EntitySet, Void, Integer, EditContactActivity> {
634         private static final int PERSIST_TRIES = 3;
635
636         private static final int RESULT_UNCHANGED = 0;
637         private static final int RESULT_SUCCESS = 1;
638         private static final int RESULT_FAILURE = 2;
639
640         private WeakReference<ProgressDialog> mProgress;
641
642         private int mSaveMode;
643         private Uri mContactLookupUri = null;
644
645         public PersistTask(EditContactActivity target, int saveMode) {
646             super(target);
647             mSaveMode = saveMode;
648         }
649
650         /** {@inheritDoc} */
651         @Override
652         protected void onPreExecute(EditContactActivity target) {
653             mProgress = new WeakReference<ProgressDialog>(ProgressDialog.show(target, null,
654                     target.getText(R.string.savingContact)));
655
656             // Before starting this task, start an empty service to protect our
657             // process from being reclaimed by the system.
658             final Context context = target;
659             context.startService(new Intent(context, EmptyService.class));
660         }
661
662         /** {@inheritDoc} */
663         @Override
664         protected Integer doInBackground(EditContactActivity target, EntitySet... params) {
665             final Context context = target;
666             final ContentResolver resolver = context.getContentResolver();
667
668             EntitySet state = params[0];
669
670             // Trim any empty fields, and RawContacts, before persisting
671             final Sources sources = Sources.getInstance(context);
672             EntityModifier.trimEmpty(state, sources);
673
674             // Attempt to persist changes
675             int tries = 0;
676             Integer result = RESULT_FAILURE;
677             while (tries++ < PERSIST_TRIES) {
678                 try {
679                     // Build operations and try applying
680                     final ArrayList<ContentProviderOperation> diff = state.buildDiff();
681                     ContentProviderResult[] results = null;
682                     if (!diff.isEmpty()) {
683                          results = resolver.applyBatch(ContactsContract.AUTHORITY, diff);
684                     }
685
686                     final long rawContactId = getRawContactId(state, diff, results);
687                     if (rawContactId != -1) {
688                         final Uri rawContactUri = ContentUris.withAppendedId(
689                                 RawContacts.CONTENT_URI, rawContactId);
690
691                         // convert the raw contact URI to a contact URI
692                         mContactLookupUri = RawContacts.getContactLookupUri(resolver,
693                                 rawContactUri);
694                     }
695                     result = (diff.size() > 0) ? RESULT_SUCCESS : RESULT_UNCHANGED;
696                     break;
697
698                 } catch (RemoteException e) {
699                     // Something went wrong, bail without success
700                     Log.e(TAG, "Problem persisting user edits", e);
701                     break;
702
703                 } catch (OperationApplicationException e) {
704                     // Version consistency failed, re-parent change and try again
705                     Log.w(TAG, "Version consistency failed, re-parenting: " + e.toString());
706                     final EntitySet newState = EntitySet.fromQuery(resolver,
707                             target.mQuerySelection, null, null);
708                     state = EntitySet.mergeAfter(newState, state);
709                 }
710             }
711
712             return result;
713         }
714
715         private long getRawContactId(EntitySet state,
716                 final ArrayList<ContentProviderOperation> diff,
717                 final ContentProviderResult[] results) {
718             long rawContactId = state.findRawContactId();
719             if (rawContactId != -1) {
720                 return rawContactId;
721             }
722
723             // we gotta do some searching for the id
724             final int diffSize = diff.size();
725             for (int i = 0; i < diffSize; i++) {
726                 ContentProviderOperation operation = diff.get(i);
727                 if (operation.getType() == ContentProviderOperation.TYPE_INSERT
728                         && operation.getUri().getEncodedPath().contains(
729                                 RawContacts.CONTENT_URI.getEncodedPath())) {
730                     return ContentUris.parseId(results[i].uri);
731                 }
732             }
733             return -1;
734         }
735
736         /** {@inheritDoc} */
737         @Override
738         protected void onPostExecute(EditContactActivity target, Integer result) {
739             final Context context = target;
740             final ProgressDialog progress = mProgress.get();
741
742             if (result == RESULT_SUCCESS && mSaveMode != SAVE_MODE_JOIN) {
743                 Toast.makeText(context, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
744             } else if (result == RESULT_FAILURE) {
745                 Toast.makeText(context, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
746             }
747
748             dismissDialog(progress);
749
750             // Stop the service that was protecting us
751             context.stopService(new Intent(context, EmptyService.class));
752
753             target.onSaveCompleted(result != RESULT_FAILURE, mSaveMode, mContactLookupUri);
754         }
755     }
756
757     /**
758      * Saves or creates the contact based on the mode, and if successful
759      * finishes the activity.
760      */
761     boolean doSaveAction(int saveMode) {
762         if (!hasValidState()) {
763             return false;
764         }
765
766         mStatus = STATUS_SAVING;
767         final PersistTask task = new PersistTask(this, saveMode);
768         task.execute(mState);
769
770         return true;
771     }
772
773     private class DeleteClickListener implements DialogInterface.OnClickListener {
774
775         public void onClick(DialogInterface dialog, int which) {
776             Sources sources = Sources.getInstance(EditContactActivity.this);
777             // Mark all raw contacts for deletion
778             for (EntityDelta delta : mState) {
779                 delta.markDeleted();
780             }
781             // Save the deletes
782             doSaveAction(SAVE_MODE_DEFAULT);
783             finish();
784         }
785     }
786
787     private void onSaveCompleted(boolean success, int saveMode, Uri contactLookupUri) {
788         switch (saveMode) {
789             case SAVE_MODE_DEFAULT:
790                 if (success && contactLookupUri != null) {
791                     final Intent resultIntent = new Intent();
792
793                     final Uri requestData = getIntent().getData();
794                     final String requestAuthority = requestData == null ? null : requestData
795                             .getAuthority();
796
797                     if (android.provider.Contacts.AUTHORITY.equals(requestAuthority)) {
798                         // Build legacy Uri when requested by caller
799                         final long contactId = ContentUris.parseId(Contacts.lookupContact(
800                                 getContentResolver(), contactLookupUri));
801                         final Uri legacyUri = ContentUris.withAppendedId(
802                                 android.provider.Contacts.People.CONTENT_URI, contactId);
803                         resultIntent.setData(legacyUri);
804                     } else {
805                         // Otherwise pass back a lookup-style Uri
806                         resultIntent.setData(contactLookupUri);
807                     }
808
809                     setResult(RESULT_OK, resultIntent);
810                 } else {
811                     setResult(RESULT_CANCELED, null);
812                 }
813                 finish();
814                 break;
815
816             case SAVE_MODE_SPLIT:
817                 if (success) {
818                     Intent intent = new Intent();
819                     intent.setData(contactLookupUri);
820                     setResult(RESULT_CLOSE_VIEW_ACTIVITY, intent);
821                 }
822                 finish();
823                 break;
824
825             case SAVE_MODE_JOIN:
826                 mStatus = STATUS_EDITING;
827                 if (success) {
828                     showJoinAggregateActivity(contactLookupUri);
829                 }
830                 break;
831         }
832     }
833
834     /**
835      * Shows a list of aggregates that can be joined into the currently viewed aggregate.
836      *
837      * @param contactLookupUri the fresh URI for the currently edited contact (after saving it)
838      */
839     public void showJoinAggregateActivity(Uri contactLookupUri) {
840         if (contactLookupUri == null) {
841             return;
842         }
843
844         mContactIdForJoin = ContentUris.parseId(contactLookupUri);
845         Intent intent = new Intent(ContactsListActivity.JOIN_AGGREGATE);
846         intent.putExtra(ContactsListActivity.EXTRA_AGGREGATE_ID, mContactIdForJoin);
847         startActivityForResult(intent, REQUEST_JOIN_CONTACT);
848     }
849
850     private interface JoinContactQuery {
851         String[] PROJECTION = {
852                 RawContacts._ID,
853                 RawContacts.CONTACT_ID,
854                 RawContacts.NAME_VERIFIED,
855         };
856
857         String SELECTION = RawContacts.CONTACT_ID + "=? OR " + RawContacts.CONTACT_ID + "=?";
858
859         int _ID = 0;
860         int CONTACT_ID = 1;
861         int NAME_VERIFIED = 2;
862     }
863
864     /**
865      * Performs aggregation with the contact selected by the user from suggestions or A-Z list.
866      */
867     private void joinAggregate(final long contactId) {
868         ContentResolver resolver = getContentResolver();
869
870         // Load raw contact IDs for all raw contacts involved - currently edited and selected
871         // in the join UIs
872         Cursor c = resolver.query(RawContacts.CONTENT_URI,
873                 JoinContactQuery.PROJECTION,
874                 JoinContactQuery.SELECTION,
875                 new String[]{String.valueOf(contactId), String.valueOf(mContactIdForJoin)}, null);
876
877         long rawContactIds[];
878         long verifiedNameRawContactId = -1;
879         try {
880             rawContactIds = new long[c.getCount()];
881             for (int i = 0; i < rawContactIds.length; i++) {
882                 c.moveToNext();
883                 long rawContactId = c.getLong(JoinContactQuery._ID);
884                 rawContactIds[i] = rawContactId;
885                 if (c.getLong(JoinContactQuery.CONTACT_ID) == mContactIdForJoin) {
886                     if (verifiedNameRawContactId == -1
887                             || c.getInt(JoinContactQuery.NAME_VERIFIED) != 0) {
888                         verifiedNameRawContactId = rawContactId;
889                     }
890                 }
891             }
892         } finally {
893             c.close();
894         }
895
896         // For each pair of raw contacts, insert an aggregation exception
897         ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
898         for (int i = 0; i < rawContactIds.length; i++) {
899             for (int j = 0; j < rawContactIds.length; j++) {
900                 if (i != j) {
901                     buildJoinContactDiff(operations, rawContactIds[i], rawContactIds[j]);
902                 }
903             }
904         }
905
906         // Mark the original contact as "name verified" to make sure that the contact
907         // display name does not change as a result of the join
908         Builder builder = ContentProviderOperation.newUpdate(
909                     ContentUris.withAppendedId(RawContacts.CONTENT_URI, verifiedNameRawContactId));
910         builder.withValue(RawContacts.NAME_VERIFIED, 1);
911         operations.add(builder.build());
912
913         // Apply all aggregation exceptions as one batch
914         try {
915             getContentResolver().applyBatch(ContactsContract.AUTHORITY, operations);
916
917             // We can use any of the constituent raw contacts to refresh the UI - why not the first
918             Intent intent = new Intent();
919             intent.setData(ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactIds[0]));
920
921             // Reload the new state from database
922             new QueryEntitiesTask(this).execute(intent);
923
924             Toast.makeText(this, R.string.contactsJoinedMessage, Toast.LENGTH_LONG).show();
925         } catch (RemoteException e) {
926             Log.e(TAG, "Failed to apply aggregation exception batch", e);
927             Toast.makeText(this, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
928         } catch (OperationApplicationException e) {
929             Log.e(TAG, "Failed to apply aggregation exception batch", e);
930             Toast.makeText(this, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
931         }
932     }
933
934     /**
935      * Construct a {@link AggregationExceptions#TYPE_KEEP_TOGETHER} ContentProviderOperation.
936      */
937     private void buildJoinContactDiff(ArrayList<ContentProviderOperation> operations,
938             long rawContactId1, long rawContactId2) {
939         Builder builder =
940                 ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
941         builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
942         builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
943         builder.withValue(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
944         operations.add(builder.build());
945     }
946
947     /**
948      * Revert any changes the user has made, and finish the activity.
949      */
950     private boolean doRevertAction() {
951         finish();
952         return true;
953     }
954
955     /**
956      * Create a new {@link RawContacts} which will exist as another
957      * {@link EntityDelta} under the currently edited {@link Contacts}.
958      */
959     private boolean doAddAction() {
960         if (mStatus != STATUS_EDITING) {
961             return false;
962         }
963
964         // Adding is okay when missing state
965         new AddContactTask(this).execute();
966         return true;
967     }
968
969     /**
970      * Delete the entire contact currently being edited, which usually asks for
971      * user confirmation before continuing.
972      */
973     private boolean doDeleteAction() {
974         if (!hasValidState())
975             return false;
976         int readOnlySourcesCnt = 0;
977         int writableSourcesCnt = 0;
978         Sources sources = Sources.getInstance(EditContactActivity.this);
979         for (EntityDelta delta : mState) {
980             final String accountType = delta.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
981             final ContactsSource contactsSource = sources.getInflatedSource(accountType,
982                     ContactsSource.LEVEL_CONSTRAINTS);
983             if (contactsSource != null && contactsSource.readOnly) {
984                 readOnlySourcesCnt += 1;
985             } else {
986                 writableSourcesCnt += 1;
987             }
988         }
989
990         if (readOnlySourcesCnt > 0 && writableSourcesCnt > 0) {
991             showDialog(DIALOG_CONFIRM_READONLY_DELETE);
992         } else if (readOnlySourcesCnt > 0 && writableSourcesCnt == 0) {
993             showDialog(DIALOG_CONFIRM_READONLY_HIDE);
994         } else if (readOnlySourcesCnt == 0 && writableSourcesCnt > 1) {
995             showDialog(DIALOG_CONFIRM_MULTIPLE_DELETE);
996         } else {
997             showDialog(DIALOG_CONFIRM_DELETE);
998         }
999         return true;
1000     }
1001
1002     /**
1003      * Pick a specific photo to be added under the currently selected tab.
1004      */
1005     boolean doPickPhotoAction(long rawContactId) {
1006         if (!hasValidState()) return false;
1007
1008         mRawContactIdRequestingPhoto = rawContactId;
1009
1010         showAndManageDialog(createPickPhotoDialog());
1011
1012         return true;
1013     }
1014
1015     /**
1016      * Creates a dialog offering two options: take a photo or pick a photo from the gallery.
1017      */
1018     private Dialog createPickPhotoDialog() {
1019         Context context = EditContactActivity.this;
1020
1021         // Wrap our context to inflate list items using correct theme
1022         final Context dialogContext = new ContextThemeWrapper(context,
1023                 android.R.style.Theme_Light);
1024
1025         String[] choices;
1026         choices = new String[2];
1027         choices[0] = getString(R.string.take_photo);
1028         choices[1] = getString(R.string.pick_photo);
1029         final ListAdapter adapter = new ArrayAdapter<String>(dialogContext,
1030                 android.R.layout.simple_list_item_1, choices);
1031
1032         final AlertDialog.Builder builder = new AlertDialog.Builder(dialogContext);
1033         builder.setTitle(R.string.attachToContact);
1034         builder.setSingleChoiceItems(adapter, -1, new DialogInterface.OnClickListener() {
1035             public void onClick(DialogInterface dialog, int which) {
1036                 dialog.dismiss();
1037                 switch(which) {
1038                     case 0:
1039                         doTakePhoto();
1040                         break;
1041                     case 1:
1042                         doPickPhotoFromGallery();
1043                         break;
1044                 }
1045             }
1046         });
1047         return builder.create();
1048     }
1049
1050     /**
1051      * Create a file name for the icon photo using current time.
1052      */
1053     private String getPhotoFileName() {
1054         Date date = new Date(System.currentTimeMillis());
1055         SimpleDateFormat dateFormat = new SimpleDateFormat("'IMG'_yyyyMMdd_HHmmss");
1056         return dateFormat.format(date) + ".jpg";
1057     }
1058
1059     /**
1060      * Launches Camera to take a picture and store it in a file.
1061      */
1062     protected void doTakePhoto() {
1063         try {
1064             // Launch camera to take photo for selected contact
1065             PHOTO_DIR.mkdirs();
1066             mCurrentPhotoFile = new File(PHOTO_DIR, getPhotoFileName());
1067             final Intent intent = getTakePickIntent(mCurrentPhotoFile);
1068             startActivityForResult(intent, CAMERA_WITH_DATA);
1069         } catch (ActivityNotFoundException e) {
1070             Toast.makeText(this, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
1071         }
1072     }
1073
1074     /**
1075      * Constructs an intent for capturing a photo and storing it in a temporary file.
1076      */
1077     public static Intent getTakePickIntent(File f) {
1078         Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE, null);
1079         intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(f));
1080         return intent;
1081     }
1082
1083     /**
1084      * Sends a newly acquired photo to Gallery for cropping
1085      */
1086     protected void doCropPhoto(File f) {
1087         try {
1088
1089             // Add the image to the media store
1090             MediaScannerConnection.scanFile(
1091                     this,
1092                     new String[] { f.getAbsolutePath() },
1093                     new String[] { null },
1094                     null);
1095
1096             // Launch gallery to crop the photo
1097             final Intent intent = getCropImageIntent(Uri.fromFile(f));
1098             startActivityForResult(intent, PHOTO_PICKED_WITH_DATA);
1099         } catch (Exception e) {
1100             Log.e(TAG, "Cannot crop image", e);
1101             Toast.makeText(this, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
1102         }
1103     }
1104
1105     /**
1106      * Constructs an intent for image cropping.
1107      */
1108     public static Intent getCropImageIntent(Uri photoUri) {
1109         Intent intent = new Intent("com.android.camera.action.CROP");
1110         intent.setDataAndType(photoUri, "image/*");
1111         intent.putExtra("crop", "true");
1112         intent.putExtra("aspectX", 1);
1113         intent.putExtra("aspectY", 1);
1114         intent.putExtra("outputX", ICON_SIZE);
1115         intent.putExtra("outputY", ICON_SIZE);
1116         intent.putExtra("return-data", true);
1117         return intent;
1118     }
1119
1120     /**
1121      * Launches Gallery to pick a photo.
1122      */
1123     protected void doPickPhotoFromGallery() {
1124         try {
1125             // Launch picker to choose photo for selected contact
1126             final Intent intent = getPhotoPickIntent();
1127             startActivityForResult(intent, PHOTO_PICKED_WITH_DATA);
1128         } catch (ActivityNotFoundException e) {
1129             Toast.makeText(this, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
1130         }
1131     }
1132
1133     /**
1134      * Constructs an intent for picking a photo from Gallery, cropping it and returning the bitmap.
1135      */
1136     public static Intent getPhotoPickIntent() {
1137         Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
1138         intent.setType("image/*");
1139         intent.putExtra("crop", "true");
1140         intent.putExtra("aspectX", 1);
1141         intent.putExtra("aspectY", 1);
1142         intent.putExtra("outputX", ICON_SIZE);
1143         intent.putExtra("outputY", ICON_SIZE);
1144         intent.putExtra("return-data", true);
1145         return intent;
1146     }
1147
1148     /** {@inheritDoc} */
1149     public void onDeleted(Editor editor) {
1150         // Ignore any editor deletes
1151     }
1152
1153     private boolean doSplitContactAction() {
1154         if (!hasValidState()) return false;
1155
1156         showAndManageDialog(createSplitDialog());
1157         return true;
1158     }
1159
1160     private Dialog createSplitDialog() {
1161         final AlertDialog.Builder builder = new AlertDialog.Builder(this);
1162         builder.setTitle(R.string.splitConfirmation_title);
1163         builder.setIcon(android.R.drawable.ic_dialog_alert);
1164         builder.setMessage(R.string.splitConfirmation);
1165         builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
1166             public void onClick(DialogInterface dialog, int which) {
1167                 // Split the contacts
1168                 mState.splitRawContacts();
1169                 doSaveAction(SAVE_MODE_SPLIT);
1170             }
1171         });
1172         builder.setNegativeButton(android.R.string.cancel, null);
1173         builder.setCancelable(false);
1174         return builder.create();
1175     }
1176
1177     private boolean doJoinContactAction() {
1178         return doSaveAction(SAVE_MODE_JOIN);
1179     }
1180
1181     /**
1182      * Build dialog that handles adding a new {@link RawContacts} after the user
1183      * picks a specific {@link ContactsSource}.
1184      */
1185     private static class AddContactTask extends
1186             WeakAsyncTask<Void, Void, ArrayList<Account>, EditContactActivity> {
1187
1188         public AddContactTask(EditContactActivity target) {
1189             super(target);
1190         }
1191
1192         @Override
1193         protected ArrayList<Account> doInBackground(final EditContactActivity target,
1194                 Void... params) {
1195             return Sources.getInstance(target).getAccounts(true);
1196         }
1197
1198         @Override
1199         protected void onPostExecute(final EditContactActivity target, ArrayList<Account> accounts) {
1200             target.selectAccountAndCreateContact(accounts);
1201         }
1202     }
1203
1204     public void selectAccountAndCreateContact(ArrayList<Account> accounts) {
1205         // No Accounts available.  Create a phone-local contact.
1206         if (accounts.isEmpty()) {
1207             createContact(null);
1208             return;  // Don't show a dialog.
1209         }
1210
1211         // In the common case of a single account being writable, auto-select
1212         // it without showing a dialog.
1213         if (accounts.size() == 1) {
1214             createContact(accounts.get(0));
1215             return;  // Don't show a dialog.
1216         }
1217
1218         // Wrap our context to inflate list items using correct theme
1219         final Context dialogContext = new ContextThemeWrapper(this, android.R.style.Theme_Light);
1220         final LayoutInflater dialogInflater =
1221             (LayoutInflater)dialogContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
1222
1223         final Sources sources = Sources.getInstance(this);
1224
1225         final ArrayAdapter<Account> accountAdapter = new ArrayAdapter<Account>(this,
1226                 android.R.layout.simple_list_item_2, accounts) {
1227             @Override
1228             public View getView(int position, View convertView, ViewGroup parent) {
1229                 if (convertView == null) {
1230                     convertView = dialogInflater.inflate(android.R.layout.simple_list_item_2,
1231                             parent, false);
1232                 }
1233
1234                 // TODO: show icon along with title
1235                 final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
1236                 final TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
1237
1238                 final Account account = this.getItem(position);
1239                 final ContactsSource source = sources.getInflatedSource(account.type,
1240                         ContactsSource.LEVEL_SUMMARY);
1241
1242                 text1.setText(account.name);
1243                 text2.setText(source.getDisplayLabel(EditContactActivity.this));
1244
1245                 return convertView;
1246             }
1247         };
1248
1249         final DialogInterface.OnClickListener clickListener =
1250                 new DialogInterface.OnClickListener() {
1251             public void onClick(DialogInterface dialog, int which) {
1252                 dialog.dismiss();
1253
1254                 // Create new contact based on selected source
1255                 final Account account = accountAdapter.getItem(which);
1256                 createContact(account);
1257             }
1258         };
1259
1260         final DialogInterface.OnCancelListener cancelListener =
1261                 new DialogInterface.OnCancelListener() {
1262             public void onCancel(DialogInterface dialog) {
1263                 // If nothing remains, close activity
1264                 if (!hasValidState()) {
1265                     finish();
1266                 }
1267             }
1268         };
1269
1270         final AlertDialog.Builder builder = new AlertDialog.Builder(this);
1271         builder.setTitle(R.string.dialog_new_contact_account);
1272         builder.setSingleChoiceItems(accountAdapter, 0, clickListener);
1273         builder.setOnCancelListener(cancelListener);
1274         showAndManageDialog(builder.create());
1275     }
1276
1277     /**
1278      * @param account may be null to signal a device-local contact should
1279      *     be created.
1280      */
1281     private void createContact(Account account) {
1282         final Sources sources = Sources.getInstance(this);
1283         final ContentValues values = new ContentValues();
1284         if (account != null) {
1285             values.put(RawContacts.ACCOUNT_NAME, account.name);
1286             values.put(RawContacts.ACCOUNT_TYPE, account.type);
1287         } else {
1288             values.putNull(RawContacts.ACCOUNT_NAME);
1289             values.putNull(RawContacts.ACCOUNT_TYPE);
1290         }
1291
1292         // Parse any values from incoming intent
1293         EntityDelta insert = new EntityDelta(ValuesDelta.fromAfter(values));
1294         final ContactsSource source = sources.getInflatedSource(
1295             account != null ? account.type : null,
1296             ContactsSource.LEVEL_CONSTRAINTS);
1297         final Bundle extras = getIntent().getExtras();
1298         EntityModifier.parseExtras(this, source, insert, extras);
1299
1300         // Ensure we have some default fields
1301         EntityModifier.ensureKindExists(insert, source, Phone.CONTENT_ITEM_TYPE);
1302         EntityModifier.ensureKindExists(insert, source, Email.CONTENT_ITEM_TYPE);
1303
1304         // Create "My Contacts" membership for Google contacts
1305         // TODO: move this off into "templates" for each given source
1306         if (GoogleSource.ACCOUNT_TYPE.equals(source.accountType)) {
1307             GoogleSource.attemptMyContactsMembership(insert, this);
1308         }
1309
1310         if (mState == null) {
1311             // Create state if none exists yet
1312             mState = EntitySet.fromSingle(insert);
1313         } else {
1314             // Add contact onto end of existing state
1315             mState.add(insert);
1316         }
1317
1318         bindEditors();
1319     }
1320
1321     /**
1322      * Compare EntityDeltas for sorting the stack of editors.
1323      */
1324     public int compare(EntityDelta one, EntityDelta two) {
1325         // Check direct equality
1326         if (one.equals(two)) {
1327             return 0;
1328         }
1329
1330         final Sources sources = Sources.getInstance(this);
1331         String accountType = one.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
1332         final ContactsSource oneSource = sources.getInflatedSource(accountType,
1333                 ContactsSource.LEVEL_SUMMARY);
1334         accountType = two.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
1335         final ContactsSource twoSource = sources.getInflatedSource(accountType,
1336                 ContactsSource.LEVEL_SUMMARY);
1337
1338         // Check read-only
1339         if (oneSource.readOnly && !twoSource.readOnly) {
1340             return 1;
1341         } else if (twoSource.readOnly && !oneSource.readOnly) {
1342             return -1;
1343         }
1344
1345         // Check account type
1346         boolean skipAccountTypeCheck = false;
1347         boolean oneIsGoogle = oneSource instanceof GoogleSource;
1348         boolean twoIsGoogle = twoSource instanceof GoogleSource;
1349         if (oneIsGoogle && !twoIsGoogle) {
1350             return -1;
1351         } else if (twoIsGoogle && !oneIsGoogle) {
1352             return 1;
1353         } else {
1354             skipAccountTypeCheck = true;
1355         }
1356
1357         int value;
1358         if (!skipAccountTypeCheck) {
1359             value = oneSource.accountType.compareTo(twoSource.accountType);
1360             if (value != 0) {
1361                 return value;
1362             }
1363         }
1364
1365         // Check account name
1366         ValuesDelta oneValues = one.getValues();
1367         String oneAccount = oneValues.getAsString(RawContacts.ACCOUNT_NAME);
1368         if (oneAccount == null) oneAccount = "";
1369         ValuesDelta twoValues = two.getValues();
1370         String twoAccount = twoValues.getAsString(RawContacts.ACCOUNT_NAME);
1371         if (twoAccount == null) twoAccount = "";
1372         value = oneAccount.compareTo(twoAccount);
1373         if (value != 0) {
1374             return value;
1375         }
1376
1377         // Both are in the same account, fall back to contact ID
1378         long oneId = oneValues.getAsLong(RawContacts._ID);
1379         long twoId = twoValues.getAsLong(RawContacts._ID);
1380         return (int)(oneId - twoId);
1381     }
1382
1383     @Override
1384     public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData,
1385             boolean globalSearch) {
1386         if (globalSearch) {
1387             super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
1388         } else {
1389             ContactsSearchManager.startSearch(this, initialQuery);
1390         }
1391     }
1392 }