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