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