OSDN Git Service

Lookup uris, delete, aggregation rules, untyped, sharing.
authorJeff Sharkey <jsharkey@android.com>
Mon, 7 Sep 2009 09:14:21 +0000 (02:14 -0700)
committerJeff Sharkey <jsharkey@android.com>
Thu, 10 Sep 2009 10:25:34 +0000 (03:25 -0700)
ContactsListView: combined together various import/export
menus under single dialog, hiding SIM import when no ICC
present.  Switched almost all cases to use soft "lookup"
uris, especially for pick and operation cases.  Brought
back delete in long-press menu, and pick modes needed for
SHOW_OR_CREATE.  These partially fix http://b/2096050 and
http://b/2096870 and http://b/2102632

ViewContactActivity: changed menus to inflate from XML,
added "Share" option to replace barcode.  Confirmed that it
sends vCard through Gmail, other apps can match MIME-type
to begin appearing in picker.  Changed EAS rules back to
untyped for Email and IM, which now allows use to use IM
type as protocol picker.  Fixes http://b/2072731 and
http://b/2092744 and http://b/2088935

EditContactActivity: restructured editing to front-load all
version assertions, and perform as single batch to prepare
for reparenting.  Correctly generate AggregationExceptions
using new API from dplotnikov, especially in cases where we
create multiple RawContacts from scratch.  Unit tests to
verify exceptions built correctly for edge cases.  Also
showing toast when saving failed.  These changes were mostly
untracked, but fixes http://b/2099211

Various untracked NPE related to untyped HardCodedSources
and cleanup of "tel" "smsto" and SMS MIME-type constants.

18 files changed:
res/menu/list.xml
res/menu/view.xml [new file with mode: 0644]
res/values/ids.xml
res/values/strings.xml
src/com/android/contacts/ContactsListActivity.java
src/com/android/contacts/ContactsUtils.java
src/com/android/contacts/ShowOrCreateActivity.java
src/com/android/contacts/TypePrecedence.java
src/com/android/contacts/ViewContactActivity.java
src/com/android/contacts/model/EntityDelta.java
src/com/android/contacts/model/EntityModifier.java
src/com/android/contacts/model/EntitySet.java [new file with mode: 0644]
src/com/android/contacts/model/HardCodedSources.java
src/com/android/contacts/ui/EditContactActivity.java
src/com/android/contacts/ui/FastTrackWindow.java
src/com/android/contacts/util/Constants.java [new file with mode: 0644]
tests/src/com/android/contacts/EntityDeltaTests.java
tests/src/com/android/contacts/EntitySetTests.java [new file with mode: 0644]

index fbcdd47..b5a2750 100644 (file)
         android:title="@string/menu_accounts" />
 
     <item
-        android:id="@+id/menu_import"
-        android:icon="@drawable/ic_menu_import_contact"
-        android:title="@string/importFromSim" />
-
-    <item
-        android:id="@+id/menu_export"
+        android:id="@+id/menu_import_export"
         android:icon="@drawable/ic_menu_export_contact"
-        android:title="@string/export_contact_list" />
+        android:title="@string/menu_import_export" />
 
 </menu>
diff --git a/res/menu/view.xml b/res/menu/view.xml
new file mode 100644 (file)
index 0000000..cf43802
--- /dev/null
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/menu_edit"
+        android:icon="@android:drawable/ic_menu_edit"
+        android:title="@string/menu_editContact"
+        android:alphabeticShortcut="e" />
+
+    <item
+        android:id="@+id/menu_share"
+        android:icon="@android:drawable/ic_menu_share"
+        android:title="@string/menu_share"
+        android:alphabeticShortcut="s" />
+
+    <item
+        android:id="@+id/menu_options"
+        android:icon="@drawable/ic_menu_mark"
+        android:title="@string/menu_contactOptions" />
+
+    <!-- TODO: use new split/join icons -->
+    <item
+        android:id="@+id/menu_split"
+        android:icon="@android:drawable/ic_menu_share"
+        android:title="@string/menu_splitAggregate" />
+
+    <item
+        android:id="@+id/menu_join"
+        android:icon="@android:drawable/ic_menu_add"
+        android:title="@string/menu_joinAggregate" />
+
+    <item
+        android:id="@+id/menu_delete"
+        android:icon="@android:drawable/ic_menu_delete"
+        android:title="@string/menu_deleteContact" />
+
+</menu>
index 48981e0..060f252 100644 (file)
@@ -29,4 +29,6 @@
 
     <item type="id" name="dialog_sync_add" />
 
+    <item type="id" name="dialog_import_export" />
+
 </resources>
index 813e212..46a4210 100644 (file)
     <!-- Toast displayed when a contact is saved -->
     <string name="contactSavedToast">Contact saved.</string>
 
+    <!-- Toast displayed when saving a contact failed -->
+    <string name="contactSavedErrorToast">Error, unable to save contact changes.</string>
+
     <!-- Separator in the contact details list describing that the items below it will place a call when clicked -->
     <string name="listSeparatorCallNumber">Dial number</string>
 
     <string name="select_import_type_title">Where would you like to import contacts from?</string>
 
     <!-- Action string for selecting SIM for importing contacts -->
-    <string name="import_from_sim">SIM Card</string>
+    <string name="import_from_sim">Import from SIM card</string>
 
     <!-- Action string for selecting SD Card for importing contacts -->
-    <string name="import_from_sdcard">SD Card</string>
+    <string name="import_from_sdcard">Import from SD card</string>
+
+    <!-- Action that exports all contacts to SD Card -->
+    <string name="export_to_sdcard">Export to SD card</string>
 
     <!-- "Import one vCard file" -->
     <string name="import_one_vcard_string">Import one vCard file</string>
     <!-- The menu item to open the list of accounts -->
     <string name="menu_accounts">Accounts</string>
 
+    <!-- The menu item to bulk import or bulk export contacts from SIM card or SD card. -->
+    <string name="menu_import_export">Import/Export</string>
+
+    <!-- Dialog title when selecting the bulk operation to perform from a list. -->
+    <string name="dialog_import_export">Import/Export contacts</string>
+
+    <!-- The menu item to share the currently viewed contact -->
+    <string name="menu_share">Share</string>
+
+    <!-- Dialog title when picking the application to share a contact with. -->
+    <string name="share_via">Share contact via</string>
+
+    <!-- Toast indicating that sharing a contact has failed. -->
+    <string name="share_error">This contact cannot be shared.</string>
+
+
+
 <!-- TODO: add comments to each of these strings to prepare for translation -->
 <string name="nameLabelsGroup">Name</string>
 <string name="nicknameLabelsGroup">Nickname</string>
 <string name="type_radio">Radio</string>
 <string name="type_assistant">Assistant</string>
 
-<string name="type_email_1">Email 1</string>
-<string name="type_email_2">Email 2</string>
-<string name="type_email_3">Email 3</string>
-
-<string name="type_im_1">IM 1</string>
-<string name="type_im_2">IM 2</string>
-<string name="type_im_3">IM 3</string>
-
 <string name="type_im_aim">AIM</string>
 <string name="type_im_msn">Windows Live</string>
 <string name="type_im_yahoo">Yahoo</string>
 <string name="email_other">Email other</string>
 <string name="email_custom">Email <xliff:g id="custom">%s</xliff:g></string>
 
-<string name="email_1">Email 1</string>
-<string name="email_2">Email 2</string>
-<string name="email_3">Email 3</string>
 <string name="email">Email</string>
 
 
 <string name="chat_jabber">Chat using Jabber</string>
 <string name="chat_other">Chat</string>
 
-<string name="im_1">Chat 1</string>
-<string name="im_2">Chat 2</string>
-<string name="im_3">Chat 3</string>
-<string name="im">Chat</string>
-
 <string name="postal_street">Street</string>
 <string name="postal_pobox">PO box</string>
 <string name="postal_neighborhood">Neighborhood</string>
index 7ce99f3..d81e20c 100644 (file)
@@ -19,9 +19,11 @@ package com.android.contacts;
 import com.android.contacts.ui.DisplayGroupsActivity;
 import com.android.contacts.ui.FastTrackWindow;
 import com.android.contacts.ui.DisplayGroupsActivity.Prefs;
+import com.android.contacts.util.Constants;
 
 import android.app.Activity;
 import android.app.AlertDialog;
+import android.app.Dialog;
 import android.app.ListActivity;
 import android.app.SearchManager;
 import android.content.AsyncQueryHandler;
@@ -61,15 +63,19 @@ import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.Intents;
 import android.provider.ContactsContract.Presence;
 import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.Photo;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.provider.ContactsContract.Contacts.AggregationSuggestions;
+import android.provider.ContactsContract.Intents.Insert;
 import android.provider.ContactsContract.Intents.UI;
+import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.ContextMenu;
+import android.view.ContextThemeWrapper;
 import android.view.Gravity;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
@@ -83,6 +89,7 @@ import android.view.inputmethod.InputMethodManager;
 import android.widget.AbsListView;
 import android.widget.AdapterView;
 import android.widget.AlphabetIndexer;
+import android.widget.ArrayAdapter;
 import android.widget.Filter;
 import android.widget.ImageView;
 import android.widget.ListView;
@@ -196,8 +203,7 @@ public final class ContactsListActivity extends ListActivity implements
     /** Run a search query */
     static final int MODE_QUERY = 60 | MODE_MASK_NO_FILTER;
     /** Run a search query in PICK mode, but that still launches to VIEW */
-    // TODO Remove this mode if we decided it is really not needed.
-    /*static final int MODE_QUERY_PICK_TO_VIEW = 65 | MODE_MASK_NO_FILTER | MODE_MASK_PICKER;*/
+    static final int MODE_QUERY_PICK_TO_VIEW = 65 | MODE_MASK_NO_FILTER | MODE_MASK_PICKER;
 
     /** Show join suggestions followed by an A-Z list */
     static final int MODE_JOIN_CONTACT = 70 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE
@@ -209,12 +215,6 @@ public final class ContactsListActivity extends ListActivity implements
     static final String NAME_COLUMN = Contacts.DISPLAY_NAME;
     //static final String SORT_STRING = People.SORT_STRING;
 
-    static final String[] CONTACTS_PROJECTION = new String[] {
-        Contacts._ID, // 0
-        Contacts.DISPLAY_NAME, // 1
-        Contacts.STARRED, //2
-    };
-
     static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] {
         Contacts._ID, // 0
         Contacts.DISPLAY_NAME, // 1
@@ -223,6 +223,7 @@ public final class ContactsListActivity extends ListActivity implements
         Presence.PRESENCE_STATUS, //4
         Contacts.PHOTO_ID, //5
         Contacts.HAS_PHONE_NUMBER, //6
+        Contacts.LOOKUP_KEY, //7
     };
     static final String[] LEGACY_PEOPLE_PROJECTION = new String[] {
         People._ID, // 0
@@ -231,13 +232,14 @@ public final class ContactsListActivity extends ListActivity implements
         PeopleColumns.TIMES_CONTACTED, //3
         People.PRESENCE_STATUS, //4
     };
-    static final int ID_COLUMN_INDEX = 0;
+    static final int SUMMARY_ID_COLUMN_INDEX = 0;
     static final int SUMMARY_NAME_COLUMN_INDEX = 1;
     static final int SUMMARY_STARRED_COLUMN_INDEX = 2;
     static final int SUMMARY_TIMES_CONTACTED_COLUMN_INDEX = 3;
     static final int SUMMARY_PRESENCE_STATUS_COLUMN_INDEX = 4;
     static final int SUMMARY_PHOTO_ID_COLUMN_INDEX = 5;
     static final int SUMMARY_HAS_PHONE_COLUMN_INDEX = 6;
+    static final int SUMMARY_LOOKUP_KEY = 7;
 
     static final String[] PHONES_PROJECTION = new String[] {
         Data._ID, //0
@@ -296,13 +298,6 @@ public final class ContactsListActivity extends ListActivity implements
 //    private boolean mDisplayAll;
     private boolean mDisplayOnlyPhones;
 
-    /**
-     * Cursor row index that holds reference back to {@link People#_ID}, such as
-     * {@link ContactMethods#PERSON_ID}. Used when responding to a
-     * {@link Intent#ACTION_SEARCH} in mode {@link #MODE_QUERY_PICK_TO_VIEW}.
-     */
-    private int mQueryPersonIdIndex;
-
     private Uri mGroupUri;
 
     private long mQueryAggregateId;
@@ -314,9 +309,6 @@ public final class ContactsListActivity extends ListActivity implements
     private boolean mListHasFocus;
 
     private String mShortcutAction;
-    private boolean mDefaultMode = false;
-
-    private boolean mCreateShortcut;
 
     /**
      * Internal query type when in mode {@link #MODE_QUERY_PICK_TO_VIEW}.
@@ -335,31 +327,6 @@ public final class ContactsListActivity extends ListActivity implements
 
     private Handler mHandler = new Handler();
 
-    private class ImportTypeSelectedListener implements DialogInterface.OnClickListener {
-        public static final int IMPORT_FROM_SIM = 0;
-        public static final int IMPORT_FROM_SDCARD = 1;
-
-        private int mIndex;
-
-        public ImportTypeSelectedListener() {
-            mIndex = IMPORT_FROM_SIM;
-        }
-
-        public void onClick(DialogInterface dialog, int which) {
-            if (which == DialogInterface.BUTTON_POSITIVE) {
-                if (mIndex == IMPORT_FROM_SIM) {
-                    doImportFromSim();
-                } else {
-                    doImportFromSDCard();
-                }
-            } else if (which == DialogInterface.BUTTON_NEGATIVE) {
-
-            } else {
-                mIndex = which;
-            }
-        }
-    }
-
     private static final String CLAUSE_ONLY_VISIBLE = Contacts.IN_VISIBLE_GROUP + "=1";
     private static final String CLAUSE_ONLY_PHONES = Contacts.HAS_PHONE_NUMBER + "=1";
 
@@ -449,7 +416,6 @@ public final class ContactsListActivity extends ListActivity implements
                 mShortcutAction = Intent.ACTION_VIEW;
                 setTitle(R.string.shortcutActivityTitle);
             }
-            mCreateShortcut = true;
         } else if (Intent.ACTION_GET_CONTENT.equals(action)) {
             final String type = intent.resolveType(this);
             if (Contacts.CONTENT_ITEM_TYPE.equals(type)) {
@@ -479,7 +445,7 @@ public final class ContactsListActivity extends ListActivity implements
             }
 
             // See if search request has extras to specify query
-            /*if (intent.hasExtra(Insert.EMAIL)) {
+            if (intent.hasExtra(Insert.EMAIL)) {
                 mMode = MODE_QUERY_PICK_TO_VIEW;
                 mQueryMode = QUERY_MODE_MAILTO;
                 mQueryData = intent.getStringExtra(Insert.EMAIL);
@@ -491,7 +457,6 @@ public final class ContactsListActivity extends ListActivity implements
                 // Otherwise handle the more normal search case
                 mMode = MODE_QUERY;
             }
-            */
             mMode = MODE_QUERY;
 
         // Since this is the filter activity it receives all intents
@@ -762,10 +727,6 @@ public final class ContactsListActivity extends ListActivity implements
     public boolean onPrepareOptionsMenu(Menu menu) {
         final boolean defaultMode = (mMode == MODE_DEFAULT);
         menu.findItem(R.id.menu_display_groups).setVisible(defaultMode);
-
-        final boolean allowExport = getResources().getBoolean(R.bool.config_allow_export_to_sdcard);
-        menu.findItem(R.id.menu_export).setVisible(allowExport);
-
         return true;
     }
 
@@ -786,26 +747,8 @@ public final class ContactsListActivity extends ListActivity implements
                 startActivity(intent);
                 return true;
             }
-            case R.id.menu_import: {
-                if (getResources().getBoolean(R.bool.config_allow_import_from_sdcard)) {
-                    ImportTypeSelectedListener listener =
-                            new ImportTypeSelectedListener();
-                    AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this)
-                            .setTitle(R.string.select_import_type_title)
-                            .setPositiveButton(android.R.string.ok, listener)
-                            .setNegativeButton(android.R.string.cancel, null);
-                    dialogBuilder.setSingleChoiceItems(new String[] {
-                            getString(R.string.import_from_sim),
-                            getString(R.string.import_from_sdcard)},
-                            ImportTypeSelectedListener.IMPORT_FROM_SIM, listener);
-                    dialogBuilder.show();
-                } else {
-                    doImportFromSim();
-                }
-                return true;
-            }
-            case R.id.menu_export: {
-                handleExportContacts();
+            case R.id.menu_import_export: {
+                showDialog(R.id.dialog_import_export);
                 return true;
             }
             case R.id.menu_accounts: {
@@ -820,6 +763,82 @@ public final class ContactsListActivity extends ListActivity implements
         return false;
     }
 
+    @Override
+    protected Dialog onCreateDialog(int id) {
+        switch (id) {
+            case R.id.dialog_import_export: {
+                return createImportExportDialog();
+            }
+        }
+        return super.onCreateDialog(id);
+    }
+
+    /**
+     * Create a {@link Dialog} that allows the user to pick from a bulk import
+     * or bulk export task across all contacts.
+     */
+    private Dialog createImportExportDialog() {
+        // Wrap our context to inflate list items using correct theme
+        final Context dialogContext = new ContextThemeWrapper(this, android.R.style.Theme_Light);
+        final Resources res = dialogContext.getResources();
+        final LayoutInflater dialogInflater = (LayoutInflater)dialogContext
+                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+        // Adapter that shows a list of string resources
+        final ArrayAdapter<Integer> adapter = new ArrayAdapter<Integer>(this,
+                android.R.layout.simple_list_item_1) {
+            @Override
+            public View getView(int position, View convertView, ViewGroup parent) {
+                if (convertView == null) {
+                    convertView = dialogInflater.inflate(android.R.layout.simple_list_item_1,
+                            parent, false);
+                }
+
+                final int resId = this.getItem(position);
+                ((TextView)convertView).setText(resId);
+                return convertView;
+            }
+        };
+
+        if (TelephonyManager.getDefault().hasIccCard()) {
+            adapter.add(R.string.import_from_sim);
+        }
+        if (res.getBoolean(R.bool.config_allow_import_from_sdcard)) {
+            adapter.add(R.string.import_from_sdcard);
+        }
+        if (res.getBoolean(R.bool.config_allow_export_to_sdcard)) {
+            adapter.add(R.string.export_to_sdcard);
+        }
+
+        final DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() {
+            public void onClick(DialogInterface dialog, int which) {
+                dialog.dismiss();
+
+                final int resId = adapter.getItem(which);
+                switch (resId) {
+                    case R.string.import_from_sim: {
+                        doImportFromSim();
+                        break;
+                    }
+                    case R.string.import_from_sdcard: {
+                        doImportFromSdCard();
+                        break;
+                    }
+                    case R.string.export_to_sdcard: {
+                        doExportToSdCard();
+                        break;
+                    }
+                }
+            }
+        };
+
+        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+        builder.setTitle(R.string.dialog_import_export);
+        builder.setNegativeButton(android.R.string.cancel, null);
+        builder.setSingleChoiceItems(adapter, -1, clickListener);
+        return builder.create();
+    }
+
     private void doImportFromSim() {
         Intent importIntent = new Intent(Intent.ACTION_VIEW);
         importIntent.setType("vnd.android.cursor.item/sim-contact");
@@ -827,12 +846,12 @@ public final class ContactsListActivity extends ListActivity implements
         startActivity(importIntent);
     }
 
-    private void doImportFromSDCard() {
+    private void doImportFromSdCard() {
         Intent intent = new Intent(this, ImportVCardActivity.class);
         startActivity(intent);
     }
 
-    private void handleExportContacts() {
+    private void doExportToSdCard() {
         VCardExporter exporter = new VCardExporter(ContactsListActivity.this, mHandler);
         exporter.startExportVCardToSdCard();
     }
@@ -948,26 +967,16 @@ public final class ContactsListActivity extends ListActivity implements
                 // Toggle the star
                 ContentValues values = new ContentValues(1);
                 values.put(Contacts.STARRED, cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX) == 0 ? 1 : 0);
-                Uri aggUri = ContentUris.withAppendedId(Contacts.CONTENT_URI,
-                        cursor.getInt(ID_COLUMN_INDEX));
-                getContentResolver().update(aggUri, values, null, null);
+                final Uri selectedUri = this.getContactUri(info.position);
+                getContentResolver().update(selectedUri, values, null, null);
                 return true;
             }
 
-            /* case MENU_ITEM_DELETE: {
-                // Get confirmation
-                Uri uri = ContentUris.withAppendedId(People.CONTENT_URI,
-                        cursor.getLong(ID_COLUMN_INDEX));
-                //TODO make this dialog persist across screen rotations
-                new AlertDialog.Builder(ContactsListActivity.this)
-                    .setTitle(R.string.deleteConfirmation_title)
-                    .setIcon(android.R.drawable.ic_dialog_alert)
-                    .setMessage(R.string.deleteConfirmation)
-                    .setNegativeButton(android.R.string.cancel, null)
-                    .setPositiveButton(android.R.string.ok, new DeleteClickListener(uri))
-                    .show();
+            case MENU_ITEM_DELETE: {
+                final Uri selectedUri = getContactUri(info.position);
+                doContactDelete(selectedUri);
                 return true;
-            } */
+            }
         }
 
         return super.onContextItemSelected(item);
@@ -991,20 +1000,10 @@ public final class ContactsListActivity extends ListActivity implements
                 break;
             }
             case KeyEvent.KEYCODE_DEL: {
-                Object o = getListView().getSelectedItem();
-                if (o != null) {
-                    Cursor cursor = (Cursor) o;
-                    Uri uri = ContentUris.withAppendedId(Contacts.CONTENT_URI,
-                            cursor.getLong(ID_COLUMN_INDEX));
-                    //TODO make this dialog persist across screen rotations
-                    new AlertDialog.Builder(ContactsListActivity.this)
-                        .setTitle(R.string.deleteConfirmation_title)
-                        .setIcon(android.R.drawable.ic_dialog_alert)
-                        .setMessage(R.string.deleteConfirmation)
-                        .setNegativeButton(android.R.string.cancel, null)
-                        .setPositiveButton(android.R.string.ok, new DeleteClickListener(uri))
-                        .setCancelable(false)
-                        .show();
+                final int position = getListView().getSelectedItemPosition();
+                if (position != ListView.INVALID_POSITION) {
+                    final Uri selectedUri = getContactUri(position);
+                    doContactDelete(selectedUri);
                     return true;
                 }
                 break;
@@ -1014,6 +1013,19 @@ public final class ContactsListActivity extends ListActivity implements
         return super.onKeyDown(keyCode, event);
     }
 
+    /**
+     * Prompt the user before deleting the given {@link Contacts} entry.
+     */
+    protected void doContactDelete(Uri contactUri) {
+        new AlertDialog.Builder(ContactsListActivity.this)
+            .setTitle(R.string.deleteConfirmation_title)
+            .setIcon(android.R.drawable.ic_dialog_alert)
+            .setMessage(R.string.deleteConfirmation)
+            .setNegativeButton(android.R.string.cancel, null)
+            .setPositiveButton(android.R.string.ok, new DeleteClickListener(contactUri))
+            .show();
+    }
+
     @Override
     protected void onListItemClick(ListView l, View v, int position, long id) {
         // Hide soft keyboard, if visible
@@ -1028,8 +1040,8 @@ public final class ContactsListActivity extends ListActivity implements
                 intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
             } else {
                 // Edit
-                intent = new Intent(Intent.ACTION_EDIT,
-                        ContentUris.withAppendedId(Contacts.CONTENT_URI, id));
+                final Uri uri = getSelectedUri(position);
+                intent = new Intent(Intent.ACTION_EDIT, uri);
             }
             intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
             final Bundle extras = getIntent().getExtras();
@@ -1039,24 +1051,18 @@ public final class ContactsListActivity extends ListActivity implements
             startActivity(intent);
             finish();
         } else if (id != -1) {
-            Uri uri = getPickerResultUri(id);
+            final Uri uri = getSelectedUri(position);
             if ((mMode & MODE_MASK_PICKER) == 0) {
-                Intent intent = new Intent(Intent.ACTION_VIEW,
-                        ContentUris.withAppendedId(Contacts.CONTENT_URI, id));
+                final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
                 startActivityForResult(intent, SUBACTIVITY_VIEW_CONTACT);
             } else if (mMode == MODE_JOIN_CONTACT) {
                 returnPickerResult(null, null, uri, id);
-            }
-
-            /*else if (mMode == MODE_QUERY_PICK_TO_VIEW) {
+            } else if (mMode == MODE_QUERY_PICK_TO_VIEW) {
                 // Started with query that should launch to view contact
-                Cursor c = (Cursor) mAdapter.getItem(position);
-                long personId = c.getLong(mQueryPersonIdIndex);
-                Intent intent = new Intent(Intent.ACTION_VIEW,
-                        ContentUris.withAppendedId(People.CONTENT_URI, personId));
+                final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
                 startActivity(intent);
                 finish();
-            }*/ else if (mMode == MODE_PICK_CONTACT
+            } else if (mMode == MODE_PICK_CONTACT
                     || mMode == MODE_PICK_OR_CREATE_CONTACT
                     || mMode == MODE_LEGACY_PICK_PERSON
                     || mMode == MODE_LEGACY_PICK_OR_CREATE_PERSON) {
@@ -1083,7 +1089,7 @@ public final class ContactsListActivity extends ListActivity implements
             }
         } else if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW
                 && position == 0) {
-            // Hook this up to new edit contact activity (bug 2092559)
+            // TODO: Hook this up to new edit contact activity (bug 2092559)
             /*Intent newContact = new Intent(Intents.Insert.ACTION, People.CONTENT_URI);
             startActivityForResult(newContact, SUBACTIVITY_NEW_CONTACT);*/
         } else {
@@ -1091,6 +1097,10 @@ public final class ContactsListActivity extends ListActivity implements
         }
     }
 
+    /**
+     * @param uri In most cases, this should be a lookup {@link Uri}, possibly
+     *            generated through {@link Contacts#getLookupUri(long, String)}.
+     */
     private void returnPickerResult(Cursor c, String name, Uri uri, long id) {
         final Intent intent = new Intent();
 
@@ -1098,8 +1108,7 @@ public final class ContactsListActivity extends ListActivity implements
             Intent shortcutIntent;
             if (Intent.ACTION_VIEW.equals(mShortcutAction)) {
                 // This is a simple shortcut to view a contact.
-                Uri lookupUri = Contacts.getLookupUri(getContentResolver(), uri);
-                shortcutIntent = new Intent(mShortcutAction, lookupUri);
+                shortcutIntent = new Intent(mShortcutAction, uri);
                 final Bitmap icon = loadContactPhoto(id, null);
                 if (icon != null) {
                     intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon);
@@ -1115,10 +1124,10 @@ public final class ContactsListActivity extends ListActivity implements
                 String scheme;
                 int resid;
                 if (Intent.ACTION_CALL.equals(mShortcutAction)) {
-                    scheme = "tel";
+                    scheme = Constants.SCHEME_TEL;
                     resid = R.drawable.badge_action_call;
                 } else {
-                    scheme = "smsto";
+                    scheme = Constants.SCHEME_SMSTO;
                     resid = R.drawable.badge_action_sms;
                 }
 
@@ -1272,19 +1281,54 @@ public final class ContactsListActivity extends ListActivity implements
             case MODE_LEGACY_PICK_POSTAL: {
                 return ContactMethods.CONTENT_URI;
             }
-            default: {
-                return null;
+            case MODE_QUERY_PICK_TO_VIEW: {
+                if (mQueryMode == QUERY_MODE_MAILTO) {
+                    return Uri.withAppendedPath(Email.CONTENT_FILTER_URI, Uri.encode(mQueryData));
+                } else if (mQueryMode == QUERY_MODE_TEL) {
+                    return Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(mQueryData));
+                }
             }
         }
+        return null;
     }
 
-    Uri getPickerResultUri(long id) {
+    /**
+     * Build the {@link Contacts#CONTENT_LOOKUP_URI} for the given
+     * {@link ListView} position, using {@link #mAdapter}.
+     */
+    private Uri getContactUri(int position) {
+        if (position == ListView.INVALID_POSITION) {
+            throw new IllegalArgumentException("Position not in list bounds");
+        }
+
+        final Cursor cursor = (Cursor)mAdapter.getItem(position);
         switch(mMode) {
-            case MODE_PICK_CONTACT:
-            case MODE_PICK_OR_CREATE_CONTACT:
-            case MODE_JOIN_CONTACT: {
-                return ContentUris.withAppendedId(Contacts.CONTENT_URI, id);
+            case MODE_LEGACY_PICK_PERSON:
+            case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
+                final long personId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
+                return ContentUris.withAppendedId(People.CONTENT_URI, personId);
+            }
+
+            default: {
+                // Build and return soft, lookup reference
+                final long contactId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
+                final String lookupKey = cursor.getString(SUMMARY_LOOKUP_KEY);
+                return Contacts.getLookupUri(contactId, lookupKey);
             }
+        }
+    }
+
+    /**
+     * Build the {@link Uri} for the given {@link ListView} position, which can
+     * be used as result when in {@link #MODE_MASK_PICKER} mode.
+     */
+    private Uri getSelectedUri(int position) {
+        if (position == ListView.INVALID_POSITION) {
+            throw new IllegalArgumentException("Position not in list bounds");
+        }
+
+        final long id = mAdapter.getItemId(position);
+        switch(mMode) {
             case MODE_LEGACY_PICK_PERSON:
             case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
                 return ContentUris.withAppendedId(People.CONTENT_URI, id);
@@ -1302,16 +1346,14 @@ public final class ContactsListActivity extends ListActivity implements
                 return ContentUris.withAppendedId(ContactMethods.CONTENT_URI, id);
             }
             default: {
-                return null;
+                return getContactUri(position);
             }
         }
     }
 
     String[] getProjectionForQuery() {
         switch(mMode) {
-            case MODE_JOIN_CONTACT: {
-                return CONTACTS_PROJECTION;
-            }
+            case MODE_JOIN_CONTACT:
             case MODE_STREQUENT:
             case MODE_FREQUENT:
             case MODE_STARRED:
@@ -1339,6 +1381,14 @@ public final class ContactsListActivity extends ListActivity implements
             case MODE_LEGACY_PICK_POSTAL: {
                 return LEGACY_POSTALS_PROJECTION;
             }
+            case MODE_QUERY_PICK_TO_VIEW: {
+                if (mQueryMode == QUERY_MODE_MAILTO) {
+                    return CONTACTS_SUMMARY_PROJECTION;
+                } else if (mQueryMode == QUERY_MODE_TEL) {
+                    return PHONES_PROJECTION;
+                }
+                break;
+            }
         }
 
         // Default to normal aggregate projection
@@ -1444,26 +1494,11 @@ public final class ContactsListActivity extends ListActivity implements
                 break;
             }
 
-            /*
             case MODE_QUERY_PICK_TO_VIEW: {
-                if (mQueryMode == QUERY_MODE_MAILTO) {
-                    // Find all contacts with the given search string as E-mail.
-                    Uri uri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_EMAIL_URI,
-                            Uri.encode(mQueryData));
-                    mQueryHandler.startQuery(QUERY_TOKEN, null,
-                            uri, SIMPLE_CONTACTS_PROJECTION, null, null,
-                            getSortOrder(CONTACTS_PROJECTION));
-
-                } else if (mQueryMode == QUERY_MODE_TEL) {
-                    mQueryAggIdIndex = PHONES_PERSON_ID_INDEX;
-                    mQueryHandler.startQuery(QUERY_TOKEN, null,
-                            Uri.withAppendedPath(Phones.CONTENT_FILTER_URL, mQueryData),
-                            PHONES_PROJECTION, null, null,
-                            getSortOrder(PHONES_PROJECTION));
-                }
+                mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, null, null,
+                        getSortOrder(projection));
                 break;
             }
-            */
 
             case MODE_STARRED:
                 mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
@@ -1595,15 +1630,17 @@ public final class ContactsListActivity extends ListActivity implements
                     return false;
                 }
 
-                String phone = ContactsUtils.querySuperPrimaryPhone(getContentResolver(), cursor.
-                        getLong(ID_COLUMN_INDEX));
+                // TODO: transition to use lookup instead of strong id
+                final long contactId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
+                final String phone = ContactsUtils.querySuperPrimaryPhone(getContentResolver(),
+                        contactId);
                 if (phone == null) {
                     signalError();
                     return false;
                 }
 
                 Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
-                        Uri.fromParts("tel", phone, null));
+                        Uri.fromParts(Constants.SCHEME_TEL, phone, null));
                 startActivity(intent);
                 return true;
             }
@@ -1689,9 +1726,9 @@ public final class ContactsListActivity extends ListActivity implements
                 } else {
                     activity.mAdapter.setSuggestionsCursor(null);
                 }
-                startQuery(QUERY_TOKEN, null, Contacts.CONTENT_URI, CONTACTS_PROJECTION,
+                startQuery(QUERY_TOKEN, null, Contacts.CONTENT_URI, CONTACTS_SUMMARY_PROJECTION,
                         Contacts._ID + " != " + mAggregateId, null,
-                        getSortOrder(CONTACTS_PROJECTION));
+                        getSortOrder(CONTACTS_SUMMARY_PROJECTION));
 
             } else {
                 cursor.close();
index dd0abd4..7537d30 100644 (file)
@@ -19,6 +19,7 @@ package com.android.contacts;
 
 import com.android.contacts.model.ContactsSource;
 import com.android.contacts.ui.FastTrackWindow;
+import com.android.contacts.util.Constants;
 
 import java.io.ByteArrayInputStream;
 
@@ -69,9 +70,8 @@ public class ContactsUtils {
         int colType;
         int colLabel;
 
-        // TODO: move the SMS mime-type to a central location
         if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)
-                || FastTrackWindow.MIME_SMS_ADDRESS.equals(mimeType)) {
+                || Constants.MIME_SMS_ADDRESS.equals(mimeType)) {
             // Reset to phone mimetype so we generate a label for SMS case
             mimeType = Phone.CONTENT_ITEM_TYPE;
             colType = cursor.getColumnIndex(Phone.TYPE);
@@ -266,7 +266,7 @@ public class ContactsUtils {
         String phone = null;
         try {
             Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
-            Uri dataUri = Uri.withAppendedPath(baseUri, "data");
+            Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY);
 
             c = cr.query(dataUri,
                     new String[] {Phone.NUMBER},
index c11240e..ed8568a 100755 (executable)
@@ -17,6 +17,7 @@
 package com.android.contacts;
 
 import com.android.contacts.ui.FastTrackWindow;
+import com.android.contacts.util.Constants;
 import com.android.contacts.util.NotifyingAsyncQueryHandler;
 
 import android.app.Activity;
@@ -65,9 +66,6 @@ public final class ShowOrCreateActivity extends Activity implements
         RawContacts.CONTACT_ID,
     };
 
-    static final String SCHEME_MAILTO = "mailto";
-    static final String SCHEME_TEL = "tel";
-
     static final int AGGREGATE_ID_INDEX = 0;
 
     static final int QUERY_TOKEN = 42;
@@ -119,13 +117,13 @@ public final class ShowOrCreateActivity extends Activity implements
         mCreateForce = intent.getBooleanExtra(Intents.EXTRA_FORCE_CREATE, false);
 
         // Handle specific query request
-        if (SCHEME_MAILTO.equals(scheme)) {
+        if (Constants.SCHEME_MAILTO.equals(scheme)) {
             mCreateExtras.putString(Intents.Insert.EMAIL, ssp);
 
             Uri uri = Uri.withAppendedPath(Email.CONTENT_FILTER_EMAIL_URI, Uri.encode(ssp));
             mQueryHandler.startQuery(QUERY_TOKEN, null, uri, CONTACTS_PROJECTION, null, null, null);
 
-        } else if (SCHEME_TEL.equals(scheme)) {
+        } else if (Constants.SCHEME_TEL.equals(scheme)) {
             mCreateExtras.putString(Intents.Insert.PHONE, ssp);
 
             Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, ssp);
index 5b51ba6..62520a0 100644 (file)
 
 package com.android.contacts;
 
-import com.android.contacts.ui.FastTrackWindow;
+import com.android.contacts.model.EntityModifier;
+import com.android.contacts.util.Constants;
 
+import android.accounts.Account;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.Im;
 import android.provider.ContactsContract.CommonDataKinds.Organization;
@@ -25,9 +27,13 @@ import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 
 /**
- * This class contains utility functions for determining the precedence of different types
- * associated with contact data items.
+ * This class contains utility functions for determining the precedence of
+ * different types associated with contact data items.
+ *
+ * @deprecated use {@link EntityModifier#getTypePrecedence} instead, since this
+ *             list isn't {@link Account} based.
  */
+@Deprecated
 public final class TypePrecedence {
 
     /* This utility class has cannot be instantiated.*/
@@ -74,6 +80,7 @@ public final class TypePrecedence {
      * @param type The integer type as defined in {@Link ContactsContract#CommonDataKinds}.
      * @return The integer precedence, where 1 is the highest.
      */
+    @Deprecated
     public static int getTypePrecedence(String mimetype, int type) {
         int[] typePrecedence = getTypePrecedenceList(mimetype);
         if (typePrecedence == null) {
@@ -88,10 +95,11 @@ public final class TypePrecedence {
         return typePrecedence.length;
     }
 
+    @Deprecated
     private static int[] getTypePrecedenceList(String mimetype) {
         if (mimetype.equals(Phone.CONTENT_ITEM_TYPE)) {
             return TYPE_PRECEDENCE_PHONES;
-        } else if (mimetype.equals(FastTrackWindow.MIME_SMS_ADDRESS)) {
+        } else if (mimetype.equals(Constants.MIME_SMS_ADDRESS)) {
             return TYPE_PRECEDENCE_PHONES;
         } else if (mimetype.equals(Email.CONTENT_ITEM_TYPE)) {
             return TYPE_PRECEDENCE_EMAIL;
index bbba2c2..ddd0abb 100644 (file)
@@ -20,10 +20,11 @@ import com.android.contacts.Collapser.Collapsible;
 import com.android.contacts.ScrollingTabWidget.OnTabSelectionChangedListener;
 import com.android.contacts.SplitAggregateView.OnContactSelectedListener;
 import com.android.contacts.model.ContactsSource;
+import com.android.contacts.model.EntityModifier;
 import com.android.contacts.model.Sources;
 import com.android.contacts.model.ContactsSource.DataKind;
-import com.android.contacts.model.HardCodedSources.SimpleInflater;
 import com.android.contacts.ui.FastTrackWindow;
+import com.android.contacts.util.Constants;
 import com.android.contacts.util.NotifyingAsyncQueryHandler;
 import com.android.internal.telephony.ITelephony;
 import com.android.internal.widget.ContactHeaderWidget;
@@ -42,20 +43,15 @@ import android.content.EntityIterator;
 import android.content.Intent;
 import android.content.DialogInterface.OnClickListener;
 import android.content.Entity.NamedContentValues;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.database.Cursor;
-import android.database.DatabaseUtils;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.RemoteException;
 import android.os.ServiceManager;
-import android.provider.BaseColumns;
-import android.provider.ContactsContract;
 import android.provider.ContactsContract.AggregationExceptions;
 import android.provider.ContactsContract.CommonDataKinds;
 import android.provider.ContactsContract.Contacts;
@@ -64,6 +60,7 @@ import android.provider.ContactsContract.Presence;
 import android.provider.ContactsContract.RawContacts;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.SparseArray;
@@ -72,6 +69,7 @@ import android.view.ContextThemeWrapper;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.Menu;
+import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
@@ -85,7 +83,6 @@ import android.widget.TextView;
 import android.widget.Toast;
 
 import java.util.ArrayList;
-import java.util.Iterator;
 
 /**
  * Displays the details of a specific contact.
@@ -95,7 +92,6 @@ public class ViewContactActivity extends Activity
         AdapterView.OnItemClickListener, NotifyingAsyncQueryHandler.AsyncQueryListener,
         OnTabSelectionChangedListener {
     private static final String TAG = "ViewContact";
-    private static final String SHOW_BARCODE_INTENT = "com.google.zxing.client.android.ENCODE";
 
     public static final String RAW_CONTACT_ID_EXTRA = "rawContactIdExtra";
 
@@ -106,13 +102,7 @@ public class ViewContactActivity extends Activity
     private static final int REQUEST_JOIN_CONTACT = 1;
     private static final int REQUEST_EDIT_CONTACT = 2;
 
-    public static final int MENU_ITEM_EDIT = 1;
-    public static final int MENU_ITEM_DELETE = 2;
     public static final int MENU_ITEM_MAKE_DEFAULT = 3;
-    public static final int MENU_ITEM_SHOW_BARCODE = 4;
-    public static final int MENU_ITEM_SPLIT_AGGREGATE = 5;
-    public static final int MENU_ITEM_JOIN_AGGREGATE = 6;
-    public static final int MENU_ITEM_OPTIONS = 7;
 
     protected Uri mLookupUri;
     private Uri mUri;
@@ -288,12 +278,9 @@ public class ViewContactActivity extends Activity
 
     // TAB CODE //
     /**
-     * Adds a tab for each {@link RawContact} associated with this contact.
+     * Adds a tab for each {@link RawContacts} associated with this contact.
      * Override this method if you want to additional tabs and/or different
      * tabs for your activity.
-     *
-     * @param entities An {@link ArrayList} of {@link Entity}s of all the RawContacts
-     * associated with the contact being displayed.
      */
     protected void bindTabs() {
         if (mEntities.size() > 1) {
@@ -322,7 +309,7 @@ public class ViewContactActivity extends Activity
     /**
      * Add a tab to be displayed in the {@link ScrollingTabWidget}.
      *
-     * @param contactId The contact id associated with the tab.
+     * @param rawContactId The contact id associated with the tab.
      * @param view A view to use as the tab indicator.
      */
     protected void addTab(long rawContactId, View view) {
@@ -486,52 +473,26 @@ public class ViewContactActivity extends Activity
 
     @Override
     public boolean onCreateOptionsMenu(Menu menu) {
-        menu.add(0, MENU_ITEM_DELETE, 0, R.string.menu_deleteContact)
-                .setIcon(android.R.drawable.ic_menu_delete);
-        menu.add(0, MENU_ITEM_SPLIT_AGGREGATE, 0, R.string.menu_splitAggregate)
-                .setIcon(android.R.drawable.ic_menu_share);
-        menu.add(0, MENU_ITEM_JOIN_AGGREGATE, 0, R.string.menu_joinAggregate)
-                .setIcon(android.R.drawable.ic_menu_add);
-        menu.add(0, MENU_ITEM_OPTIONS, 0, R.string.menu_contactOptions)
-                .setIcon(R.drawable.ic_menu_mark);
+        super.onCreateOptionsMenu(menu);
+
+        final MenuInflater inflater = getMenuInflater();
+        inflater.inflate(R.menu.view, menu);
         return true;
     }
 
     @Override
     public boolean onPrepareOptionsMenu(Menu menu) {
         super.onPrepareOptionsMenu(menu);
-        // Perform this check each time the menu is about to be shown, because the Barcode Scanner
-        // could be installed or uninstalled at any time.
-        if (isBarcodeScannerInstalled()) {
-            if (menu.findItem(MENU_ITEM_SHOW_BARCODE) == null) {
-                menu.add(0, MENU_ITEM_SHOW_BARCODE, 0, R.string.menu_showBarcode)
-                        .setIcon(R.drawable.ic_menu_show_barcode);
-            }
-        } else {
-            menu.removeItem(MENU_ITEM_SHOW_BARCODE);
-        }
 
-        // Only show the edit option if we have a selected tab.
-        if (mSelectedRawContactId != null) {
-            if (menu.findItem(MENU_ITEM_EDIT) == null) {
-                menu.add(0, MENU_ITEM_EDIT, 0, R.string.menu_editContact)
-                    .setIcon(android.R.drawable.ic_menu_edit)
-                    .setAlphabeticShortcut('e');
-            }
-        } else {
-            menu.removeItem(MENU_ITEM_EDIT);
-        }
+        // Only allow edit if we have a selected tab
+        final boolean contactSelected = (mSelectedRawContactId != null);
+        menu.findItem(R.id.menu_edit).setEnabled(contactSelected);
 
-        boolean isAggregate = mRawContactIds.size() > 1;
-        menu.findItem(MENU_ITEM_SPLIT_AGGREGATE).setEnabled(isAggregate);
-        return true;
-    }
+        // Only allow split when more than one contact
+        final boolean isAggregate = (mRawContactIds.size() > 1);
+        menu.findItem(R.id.menu_split).setEnabled(isAggregate);
 
-    private boolean isBarcodeScannerInstalled() {
-        final Intent intent = new Intent(SHOW_BARCODE_INTENT);
-        ResolveInfo ri = getPackageManager().resolveActivity(intent,
-                PackageManager.MATCH_DEFAULT_ONLY);
-        return ri != null;
+        return true;
     }
 
     @Override
@@ -574,7 +535,7 @@ public class ViewContactActivity extends Activity
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         switch (item.getItemId()) {
-            case MENU_ITEM_EDIT: {
+            case R.id.menu_edit: {
                 Long rawContactIdToEdit = mSelectedRawContactId;
                 if (rawContactIdToEdit == null) {
                     // This shouldn't be possible. We only show the edit option if
@@ -591,13 +552,12 @@ public class ViewContactActivity extends Activity
                         REQUEST_EDIT_CONTACT);
                 break;
             }
-            case MENU_ITEM_DELETE: {
+            case R.id.menu_delete: {
                 // Get confirmation
                 showDialog(DIALOG_CONFIRM_DELETE);
                 return true;
             }
-
-            case MENU_ITEM_SPLIT_AGGREGATE: {
+            case R.id.menu_split: {
                 if (mRawContactIds.size() == 2) {
                     splitContact(mRawContactIds.get(1));
                 } else {
@@ -605,59 +565,30 @@ public class ViewContactActivity extends Activity
                 }
                 return true;
             }
-
-            case MENU_ITEM_JOIN_AGGREGATE: {
+            case R.id.menu_join: {
                 showJoinAggregateActivity();
                 return true;
             }
-
-            case MENU_ITEM_OPTIONS: {
+            case R.id.menu_options: {
                 showOptionsActivity();
                 return true;
             }
+            case R.id.menu_share: {
+                final Intent intent = new Intent(Intent.ACTION_SEND);
+                intent.setType(Contacts.CONTENT_ITEM_TYPE);
+                intent.putExtra(Intent.EXTRA_STREAM, mLookupUri);
 
-            // TODO(emillar) Bring this back.
-            /*case MENU_ITEM_SHOW_BARCODE:
-                if (mCursor.moveToFirst()) {
-                    Intent intent = new Intent(SHOW_BARCODE_INTENT);
-                    intent.putExtra("ENCODE_TYPE", "CONTACT_TYPE");
-                    Bundle bundle = new Bundle();
-                    String name = mCursor.getString(AGGREGATE_DISPLAY_NAME_COLUMN);
-                    if (!TextUtils.isEmpty(name)) {
-                        // Correctly handle when section headers are hidden
-                        int sepAdjust = SHOW_SEPARATORS ? 1 : 0;
-
-                        bundle.putString(Contacts.Intents.Insert.NAME, name);
-                        // The 0th ViewEntry in each ArrayList below is a separator item
-                        int entriesToAdd = Math.min(mPhoneEntries.size() - sepAdjust, PHONE_KEYS.length);
-                        for (int x = 0; x < entriesToAdd; x++) {
-                            ViewEntry entry = mPhoneEntries.get(x + sepAdjust);
-                            bundle.putString(PHONE_KEYS[x], entry.data);
-                        }
-                        entriesToAdd = Math.min(mEmailEntries.size() - sepAdjust, EMAIL_KEYS.length);
-                        for (int x = 0; x < entriesToAdd; x++) {
-                            ViewEntry entry = mEmailEntries.get(x + sepAdjust);
-                            bundle.putString(EMAIL_KEYS[x], entry.data);
-                        }
-                        if (mPostalEntries.size() >= 1 + sepAdjust) {
-                            ViewEntry entry = mPostalEntries.get(sepAdjust);
-                            bundle.putString(Contacts.Intents.Insert.POSTAL, entry.data);
-                        }
-                        intent.putExtra("ENCODE_DATA", bundle);
-                        try {
-                            startActivity(intent);
-                        } catch (ActivityNotFoundException e) {
-                            // The check in onPrepareOptionsMenu() should make this impossible, but
-                            // for safety I'm catching the exception rather than crashing. Ideally
-                            // I'd call Menu.removeItem() here too, but I don't see a way to get
-                            // the options menu.
-                            Log.e(TAG, "Show barcode menu item was clicked but Barcode Scanner " +
-                                    "was not installed.");
-                        }
-                        return true;
-                    }
+                // Launch chooser to share contact via
+                final CharSequence chooseTitle = getText(R.string.share_via);
+                final Intent chooseIntent = Intent.createChooser(intent, chooseTitle);
+
+                try {
+                    startActivity(chooseIntent);
+                } catch (ActivityNotFoundException ex) {
+                    Toast.makeText(this, R.string.share_error, Toast.LENGTH_SHORT).show();
                 }
-                break; */
+                return true;
+            }
         }
         return super.onOptionsItemSelected(item);
     }
@@ -953,7 +884,7 @@ public class ViewContactActivity extends Activity
                     entry.mimetype = mimetype;
                     entry.label = buildActionString(kind, entryValues, true);
                     entry.data = buildDataString(kind, entryValues);
-                    if (kind.typeColumn != null) {
+                    if (kind.typeColumn != null && entryValues.containsKey(kind.typeColumn)) {
                         entry.type = entryValues.getAsInteger(kind.typeColumn);
                     }
                     if (kind.iconRes > 0) {
@@ -1032,7 +963,7 @@ public class ViewContactActivity extends Activity
                             String host = null;
 
                             if (TextUtils.isEmpty(entry.label)) {
-                                entry.label = getString(R.string.im).toLowerCase();
+                                entry.label = getString(R.string.chat_other).toLowerCase();
                             }
 
                             if (protocolObj instanceof Number) {
@@ -1287,7 +1218,7 @@ public class ViewContactActivity extends Activity
             TextView data = views.data;
             if (data != null) {
                 if (entry.mimetype.equals(Phone.CONTENT_ITEM_TYPE)
-                        || entry.mimetype.equals(FastTrackWindow.MIME_SMS_ADDRESS)) {
+                        || entry.mimetype.equals(Constants.MIME_SMS_ADDRESS)) {
                     data.setText(PhoneNumberUtils.formatNumber(entry.data));
                 } else {
                     data.setText(entry.data);
index 69e1e51..f096cc7 100644 (file)
@@ -29,7 +29,6 @@ import android.net.Uri;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.provider.BaseColumns;
-import android.provider.ContactsContract.AggregationExceptions;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.RawContacts;
 import android.view.View;
@@ -98,6 +97,8 @@ public class EntityDelta implements Parcelable {
         // Always take after values from new state
         this.mValues.mAfter = remote.mValues.mAfter;
 
+        // TODO: log before/after versions to track re-parenting
+
         // Find matching local entry for each remote values, or create
         for (ArrayList<ValuesDelta> mimeEntries : remote.mEntries.values()) {
             for (ValuesDelta remoteEntry : mimeEntries) {
@@ -120,6 +121,10 @@ public class EntityDelta implements Parcelable {
         return mValues;
     }
 
+    public boolean isContactInsert() {
+        return mValues.isInsert();
+    }
+
     /**
      * Get the {@link ValuesDelta} child marked as {@link Data#IS_PRIMARY},
      * which may return null when no entry exists.
@@ -279,25 +284,44 @@ public class EntityDelta implements Parcelable {
     }
 
     /**
+     * Build a list of {@link ContentProviderOperation} that will assert any
+     * "before" state hasn't changed. This is maintained separately so that all
+     * asserts can take place before any updates occur.
+     */
+    public void buildAssert(ArrayList<ContentProviderOperation> buildInto) {
+        final boolean isContactInsert = mValues.isInsert();
+        if (!isContactInsert) {
+            // Assert version is consistent while persisting changes
+            final Long beforeId = mValues.getId();
+            final Long beforeVersion = mValues.getAsLong(RawContacts.VERSION);
+
+            final ContentProviderOperation.Builder builder = ContentProviderOperation
+                    .newAssertQuery(RawContacts.CONTENT_URI);
+            builder.withSelection(RawContacts._ID + "=" + beforeId, null);
+            builder.withValue(RawContacts.VERSION, beforeVersion);
+            buildInto.add(builder.build());
+        }
+    }
+
+    /**
      * Build a list of {@link ContentProviderOperation} that will transform the
      * current "before" {@link Entity} state into the modified state which this
      * {@link EntityDelta} represents.
      */
-    public ArrayList<ContentProviderOperation> buildDiff() {
-        final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+    public void buildDiff(ArrayList<ContentProviderOperation> buildInto) {
+        final int firstIndex = buildInto.size();
 
         final boolean isContactInsert = mValues.isInsert();
         final boolean isContactDelete = mValues.isDelete();
         final boolean isContactUpdate = !isContactInsert && !isContactDelete;
 
         final Long beforeId = mValues.getId();
-        final Long beforeVersion = mValues.getAsLong(RawContacts.VERSION);
 
         Builder builder;
 
         // Build possible operation at Contact level
         builder = mValues.buildDiff(RawContacts.CONTENT_URI);
-        possibleAdd(diff, builder);
+        possibleAdd(buildInto, builder);
 
         // Build operations for all children
         for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
@@ -309,7 +333,7 @@ public class EntityDelta implements Parcelable {
                 if (child.isInsert()) {
                     if (isContactInsert) {
                         // Parent is brand new insert, so back-reference _id
-                        builder.withValueBackReference(Data.RAW_CONTACT_ID, 0);
+                        builder.withValueBackReference(Data.RAW_CONTACT_ID, firstIndex);
                     } else {
                         // Inserting under existing, so fill with known _id
                         builder.withValue(Data.RAW_CONTACT_ID, beforeId);
@@ -318,43 +342,20 @@ public class EntityDelta implements Parcelable {
                     // Child must be insert when Contact insert
                     throw new IllegalArgumentException("When parent insert, child must be also");
                 }
-                possibleAdd(diff, builder);
+                possibleAdd(buildInto, builder);
             }
         }
 
-        // Create exception when insert requested aggregate membership
-        final Long contactId = mValues.getAsLong(RawContacts.CONTACT_ID);
-        if (isContactInsert && contactId != null) {
-            builder = ContentProviderOperation.newUpdate(AggregationExceptions.CONTENT_URI);
-            builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_IN);
-            builder.withValue(AggregationExceptions.CONTACT_ID, contactId);
-            builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID, 0);
-            possibleAdd(diff, builder);
-        }
-
-        final boolean hasOperations = diff.size() > 0;
-
-        if (hasOperations && isContactUpdate) {
+        final boolean addedOperations = buildInto.size() > firstIndex;
+        if (addedOperations && isContactUpdate) {
             // Suspend aggregation while persisting updates
             builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_SUSPENDED);
-            diff.add(0, builder.build());
+            buildInto.add(firstIndex, builder.build());
 
             // Restore aggregation as last operation
             builder = buildSetAggregationMode(beforeId, RawContacts.AGGREGATION_MODE_DEFAULT);
-            diff.add(builder.build());
+            buildInto.add(builder.build());
         }
-
-        if (hasOperations && (isContactUpdate || isContactDelete)) {
-            // Assert version is consistent while persisting changes
-            builder = ContentProviderOperation.newAssertQuery(RawContacts.CONTENT_URI);
-            builder.withSelection(RawContacts._ID + "=" + beforeId, null);
-            builder.withValue(RawContacts.VERSION, beforeVersion);
-            // Sneak version check at beginning of list--we only depend on
-            // back-references during insert cases.
-            diff.add(0, builder.build());
-        }
-
-        return diff;
     }
 
     /**
index 7a114e0..e904a8b 100644 (file)
@@ -204,7 +204,8 @@ public class EntityModifier {
      */
     public static EditType getCurrentType(ContentValues entry, DataKind kind) {
         if (kind.typeColumn == null) return null;
-        final int rawValue = entry.getAsInteger(kind.typeColumn);
+        final Integer rawValue = entry.getAsInteger(kind.typeColumn);
+        if (rawValue == null) return null;
         return getType(kind, rawValue);
     }
 
@@ -215,6 +216,7 @@ public class EntityModifier {
     public static EditType getCurrentType(Cursor cursor, DataKind kind) {
         if (kind.typeColumn == null) return null;
         final int index = cursor.getColumnIndex(kind.typeColumn);
+        if (index == -1) return null;
         final int rawValue = cursor.getInt(index);
         return getType(kind, rawValue);
     }
@@ -232,6 +234,20 @@ public class EntityModifier {
     }
 
     /**
+     * Return the precedence for the the given {@link EditType#rawValue}, where
+     * lower numbers are higher precedence.
+     */
+    public static int getTypePrecedence(DataKind kind, int rawValue) {
+        for (int i = 0; i < kind.typeList.size(); i++) {
+            final EditType type = kind.typeList.get(i);
+            if (type.rawValue == rawValue) {
+                return i;
+            }
+        }
+        return Integer.MAX_VALUE;
+    }
+
+    /**
      * Find the best {@link EditType} for a potential insert. The "best" is the
      * first primary type that doesn't already exist. When all valid types
      * exist, we pick the last valid option.
diff --git a/src/com/android/contacts/model/EntitySet.java b/src/com/android/contacts/model/EntitySet.java
new file mode 100644 (file)
index 0000000..f9425b9
--- /dev/null
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.model;
+
+import com.google.android.collect.Lists;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
+import android.content.Entity;
+import android.content.EntityIterator;
+import android.content.ContentProviderOperation.Builder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+
+import java.util.ArrayList;
+
+/**
+ * Container for multiple {@link EntityDelta} objects, usually when editing
+ * together as an entire aggregate. Provides convenience methods for parceling
+ * and applying another {@link EntitySet} over it.
+ */
+public class EntitySet extends ArrayList<EntityDelta> implements Parcelable {
+    private EntitySet() {
+    }
+
+    /**
+     * Create an {@link EntitySet} that contains the given {@link EntityDelta},
+     * usually when inserting a new {@link Contacts} entry.
+     */
+    public static EntitySet fromSingle(EntityDelta delta) {
+        final EntitySet state = new EntitySet();
+        state.add(delta);
+        return state;
+    }
+
+    /**
+     * Create an {@link EntitySet} based on {@link Contacts} specified by the
+     * given query parameters. This closes the {@link EntityIterator} when
+     * finished, so it doesn't subscribe to updates.
+     */
+    public static EntitySet fromQuery(ContentResolver resolver, String selection,
+            String[] selectionArgs, String sortOrder) {
+        EntityIterator iterator = null;
+        final EntitySet state = new EntitySet();
+        try {
+            // Perform background query to pull contact details
+            iterator = resolver.queryEntities(RawContacts.CONTENT_URI, selection, selectionArgs,
+                    sortOrder);
+            while (iterator.hasNext()) {
+                // Read all contacts into local deltas to prepare for edits
+                final Entity before = iterator.next();
+                final EntityDelta entity = EntityDelta.fromBefore(before);
+                state.add(entity);
+            }
+        } catch (RemoteException e) {
+            throw new IllegalStateException("Problem querying contact details", e);
+        } finally {
+            if (iterator != null) {
+                iterator.close();
+            }
+        }
+        return state;
+    }
+
+    /**
+     * Merge the "after" values from the given {@link EntitySet}.
+     */
+    public void mergeAfter(EntitySet remote) {
+        // TODO: write this folding logic to re-parent
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Build a list of {@link ContentProviderOperation} that will transform all
+     * the "before" {@link Entity} states into the modified state which all
+     * {@link EntityDelta} objects represent. This method specifically creates
+     * any {@link AggregationExceptions} rules needed to groups edits together.
+     */
+    public ArrayList<ContentProviderOperation> buildDiff() {
+        final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+
+        final long rawContactId = this.findRawContactId();
+        int firstInsertRow = -1;
+
+        // First pass enforces versions remain consistent
+        for (EntityDelta delta : this) {
+            delta.buildAssert(diff);
+        }
+
+        final int assertMark = diff.size();
+
+        // Second pass builds actual operations
+        for (EntityDelta delta : this) {
+            final int firstBatch = diff.size();
+            delta.buildDiff(diff);
+
+            // Only create rules for inserts
+            if (!delta.isContactInsert()) continue;
+
+            if (rawContactId != -1) {
+                // Has existing contact, so bind to it strongly
+                final Builder builder = beginKeepTogether();
+                builder.withValue(AggregationExceptions.RAW_CONTACT_ID1, rawContactId);
+                builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, firstBatch);
+                diff.add(builder.build());
+
+            } else if (firstInsertRow == -1) {
+                // First insert case, so record row
+                firstInsertRow = firstBatch;
+
+            } else {
+                // Additional insert case, so point at first insert
+                final Builder builder = beginKeepTogether();
+                builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID1, firstInsertRow);
+                builder.withValueBackReference(AggregationExceptions.RAW_CONTACT_ID2, firstBatch);
+                diff.add(builder.build());
+            }
+        }
+
+        // No real changes if only left with asserts
+        if (diff.size() == assertMark) {
+            diff.clear();
+        }
+
+        return diff;
+    }
+
+    /**
+     * Start building a {@link ContentProviderOperation} that will keep two
+     * {@link RawContacts} together.
+     */
+    protected Builder beginKeepTogether() {
+        final Builder builder = ContentProviderOperation
+                .newUpdate(AggregationExceptions.CONTENT_URI);
+        builder.withValue(AggregationExceptions.TYPE, AggregationExceptions.TYPE_KEEP_TOGETHER);
+        return builder;
+    }
+
+    /**
+     * Search all contained {@link EntityDelta} for the first one with an
+     * existing {@link RawContacts#_ID} value. Usually used when creating
+     * {@link AggregationExceptions} during an update.
+     */
+    public long findRawContactId() {
+        for (EntityDelta delta : this) {
+            final Long rawContactId = delta.getValues().getAsLong(RawContacts._ID);
+            if (rawContactId != null && rawContactId >= 0) {
+                return rawContactId;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Find {@link RawContacts#_ID} of the requested {@link EntityDelta}.
+     */
+    public long getRawContactId(int index) {
+        if (index >=0 && index < this.size()) {
+            final EntityDelta delta = this.get(index);
+            return delta.getValues().getAsLong(RawContacts._ID);
+        } else {
+            return -1;
+        }
+    }
+
+    /** {@inheritDoc} */
+    public int describeContents() {
+        // Nothing special about this parcel
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    public void writeToParcel(Parcel dest, int flags) {
+        final int size = this.size();
+        dest.writeInt(size);
+        for (EntityDelta delta : this) {
+            dest.writeParcelable(delta, flags);
+        }
+    }
+
+    public void readFromParcel(Parcel source) {
+        final int size = source.readInt();
+        for (int i = 0; i < size; i++) {
+            this.add(source.<EntityDelta> readParcelable(null));
+        }
+    }
+
+    public static final Parcelable.Creator<EntitySet> CREATOR = new Parcelable.Creator<EntitySet>() {
+        public EntitySet createFromParcel(Parcel in) {
+            final EntitySet state = new EntitySet();
+            state.readFromParcel(in);
+            return state;
+        }
+
+        public EntitySet[] newArray(int size) {
+            return new EntitySet[size];
+        }
+    };
+}
index 5afaef7..885a06b 100644 (file)
@@ -384,6 +384,7 @@ public class HardCodedSources {
     private static final String GOOGLE_MY_CONTACTS_GROUP = "System Group: My Contacts";
 
     public static final void attemptMyContactsMembership(EntityDelta state, Context context) {
+        // TODO: create group when it doesnt exist (syncadapter will fold in)
         final ContentResolver resolver = context.getContentResolver();
         final Cursor cursor = resolver.query(Groups.CONTENT_URI, new String[] { Groups.SOURCE_ID },
                 Groups.TITLE + "=?", new String[] { GOOGLE_MY_CONTACTS_GROUP }, null);
@@ -404,14 +405,6 @@ public class HardCodedSources {
      * The constants below are shared with the Exchange sync adapter, and are
      * currently static. These values should be maintained in parallel.
      */
-    private static final int TYPE_EMAIL1 = 20;
-    private static final int TYPE_EMAIL2 = 21;
-    private static final int TYPE_EMAIL3 = 22;
-
-    private static final int TYPE_IM1 = 23;
-    private static final int TYPE_IM2 = 24;
-    private static final int TYPE_IM3 = 25;
-
     private static final int TYPE_WORK2 = 26;
     private static final int TYPE_HOME2 = 27;
     private static final int TYPE_CAR = 28;
@@ -511,20 +504,12 @@ public class HardCodedSources {
 
             kind.actionHeader = new ActionInflater(list.resPackageName, kind);
             kind.actionBody = new SimpleInflater(Email.DATA);
-
-            kind.typeColumn = Email.TYPE;
-            kind.typeList = Lists.newArrayList();
-            kind.typeList.add(new EditType(TYPE_EMAIL1, R.string.type_email_1)
-                    .setSpecificMax(1));
-            kind.typeList.add(new EditType(TYPE_EMAIL2, R.string.type_email_2)
-                    .setSpecificMax(1));
-            kind.typeList.add(new EditType(TYPE_EMAIL3, R.string.type_email_3)
-                    .setSpecificMax(1));
+            kind.typeOverallMax = 3;
 
             kind.fieldList = Lists.newArrayList();
             kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
-            kind.fieldList.add(new EditField(Email.DISPLAY_NAME, R.string.label_email_display_name,
-                    FLAGS_PERSON_NAME));
+//            kind.fieldList.add(new EditField(Email.DISPLAY_NAME, R.string.label_email_display_name,
+//                    FLAGS_PERSON_NAME));
 
             list.add(kind);
         }
@@ -536,15 +521,33 @@ public class HardCodedSources {
 
             kind.actionHeader = new ActionInflater(list.resPackageName, kind);
             kind.actionBody = new SimpleInflater(Im.DATA);
+            kind.typeOverallMax = 3;
 
-            kind.typeColumn = Im.TYPE;
-            kind.typeList = new ArrayList<EditType>();
-            kind.typeList.add(new EditType(TYPE_IM1, R.string.type_im_1).
-                    setSpecificMax(1));
-            kind.typeList.add(new EditType(TYPE_IM2, R.string.type_im_2).
-                    setSpecificMax(1));
-            kind.typeList.add(new EditType(TYPE_IM3, R.string.type_im_3).
-                    setSpecificMax(1));
+            // NOTE: even though a traditional "type" exists, for editing
+            // purposes we're using the network to pick labels
+
+            kind.defaultValues = new ContentValues();
+            kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER);
+
+            kind.typeColumn = Im.PROTOCOL;
+            kind.typeList = Lists.newArrayList();
+            kind.typeList.add(new EditType(Im.PROTOCOL_AIM, R.string.type_im_aim,
+                    R.string.chat_aim));
+            kind.typeList.add(new EditType(Im.PROTOCOL_MSN, R.string.type_im_msn,
+                    R.string.chat_msn));
+            kind.typeList.add(new EditType(Im.PROTOCOL_YAHOO, R.string.type_im_yahoo,
+                    R.string.chat_yahoo));
+            kind.typeList.add(new EditType(Im.PROTOCOL_SKYPE, R.string.type_im_skype,
+                    R.string.chat_skype));
+            kind.typeList.add(new EditType(Im.PROTOCOL_QQ, R.string.type_im_qq, R.string.chat_qq));
+            kind.typeList.add(new EditType(Im.PROTOCOL_GOOGLE_TALK, R.string.type_im_google_talk,
+                    R.string.chat_gtalk));
+            kind.typeList.add(new EditType(Im.PROTOCOL_ICQ, R.string.type_im_icq,
+                    R.string.chat_icq));
+            kind.typeList.add(new EditType(Im.PROTOCOL_JABBER, R.string.type_im_jabber,
+                    R.string.chat_jabber));
+            kind.typeList.add(new EditType(Im.PROTOCOL_CUSTOM, R.string.type_custom,
+                    R.string.chat_other).setSecondary(true).setCustomColumn(Im.CUSTOM_PROTOCOL));
 
             kind.fieldList = Lists.newArrayList();
             kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
@@ -715,30 +718,28 @@ public class HardCodedSources {
         public CharSequence inflateUsing(Context context, Cursor cursor) {
             final EditType type = EntityModifier.getCurrentType(cursor, mKind);
             final boolean validString = (type != null && type.actionRes != 0);
-            CharSequence actionString;
+            if (!validString) return null;
+
             if (type.customColumn != null) {
                 final int index = cursor.getColumnIndex(type.customColumn);
                 final String customLabel = cursor.getString(index);
-                actionString = String.format(context.getString(type.actionRes),
-                        customLabel);
+                return String.format(context.getString(type.actionRes), customLabel);
             } else {
-                actionString = context.getText(type.actionRes);
+                return context.getText(type.actionRes);
             }
-            return validString ? actionString : null;
         }
 
         public CharSequence inflateUsing(Context context, ContentValues values) {
             final EditType type = EntityModifier.getCurrentType(values, mKind);
             final boolean validString = (type != null && type.actionRes != 0);
-            CharSequence actionString;
+            if (!validString) return null;
+
             if (type.customColumn != null) {
                 final String customLabel = values.getAsString(type.customColumn);
-                actionString = String.format(context.getString(type.actionRes),
-                        customLabel);
+                return String.format(context.getString(type.actionRes), customLabel);
             } else {
-                actionString = context.getText(type.actionRes);
+                return context.getText(type.actionRes);
             }
-            return validString ? actionString : null;
         }
     }
 
index d5bae7d..7e73568 100644 (file)
@@ -23,6 +23,7 @@ import com.android.contacts.ViewContactActivity;
 import com.android.contacts.model.ContactsSource;
 import com.android.contacts.model.EntityDelta;
 import com.android.contacts.model.EntityModifier;
+import com.android.contacts.model.EntitySet;
 import com.android.contacts.model.HardCodedSources;
 import com.android.contacts.model.Sources;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
@@ -44,19 +45,15 @@ import android.content.ContentValues;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Entity;
-import android.content.EntityIterator;
 import android.content.Intent;
 import android.content.OperationApplicationException;
 import android.net.Uri;
 import android.os.Bundle;
-import android.os.Parcel;
-import android.os.Parcelable;
 import android.os.RemoteException;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.RawContacts;
 import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.ContactsContract.Contacts.Data;
@@ -101,68 +98,14 @@ public final class EditContactActivity extends Activity implements View.OnClickL
 //    private long mSelectedRawContactId = -1;
 //    private long mContactId = -1;
 
+    private String mQuerySelection;
+
     private ScrollingTabWidget mTabWidget;
     private ContactHeaderWidget mHeader;
 
     private ContactEditorView mEditor;
 
-    private EditState mState = new EditState();
-
-    private static class EditState extends ArrayList<EntityDelta> implements Parcelable {
-        public long getContactId() {
-            if (this.size() > 0) {
-                // Assume the aggregate tied to first child
-                final EntityDelta first = this.get(0);
-                return first.getValues().getAsLong(RawContacts.CONTACT_ID);
-            } else {
-                // Otherwise return invalid value
-                return -1;
-            }
-        }
-
-        public long getRawContactId(int index) {
-            if (index >=0 && index < this.size()) {
-                final EntityDelta delta = this.get(index);
-                return delta.getValues().getAsLong(RawContacts._ID);
-            } else {
-                return -1;
-            }
-        }
-
-        /** {@inheritDoc} */
-        public int describeContents() {
-            // Nothing special about this parcel
-            return 0;
-        }
-
-        /** {@inheritDoc} */
-        public void writeToParcel(Parcel dest, int flags) {
-            final int size = this.size();
-            dest.writeInt(size);
-            for (EntityDelta delta : this) {
-                dest.writeParcelable(delta, flags);
-            }
-        }
-
-        public void readFromParcel(Parcel source) {
-            final int size = source.readInt();
-            for (int i = 0; i < size; i++) {
-                this.add(source.<EntityDelta> readParcelable(null));
-            }
-        }
-
-        public static final Parcelable.Creator<EditState> CREATOR = new Parcelable.Creator<EditState>() {
-            public EditState createFromParcel(Parcel in) {
-                final EditState state = new EditState();
-                state.readFromParcel(in);
-                return state;
-            }
-
-            public EditState[] newArray(int size) {
-                return new EditState[size];
-            }
-        };
-    }
+    private EntitySet mState;
 
     @Override
     protected void onCreate(Bundle icicle) {
@@ -237,27 +180,8 @@ public final class EditContactActivity extends Activity implements View.OnClickL
                 selection = RawContacts._ID + "=" + rawContactId;
             }
 
-            EntityIterator iterator = null;
-            final EditState state = new EditState();
-            try {
-                // Perform background query to pull contact details
-                iterator = resolver.queryEntities(RawContacts.CONTENT_URI,
-                        selection, null, null);
-                while (iterator.hasNext()) {
-                    // Read all contacts into local deltas to prepare for edits
-                    final Entity before = iterator.next();
-                    final EntityDelta entity = EntityDelta.fromBefore(before);
-                    state.add(entity);
-                }
-            } catch (RemoteException e) {
-                throw new IllegalStateException("Problem querying contact details", e);
-            } finally {
-                if (iterator != null) {
-                    iterator.close();
-                }
-            }
-
-            target.mState = state;
+            target.mQuerySelection = selection;
+            target.mState = EntitySet.fromQuery(resolver, selection, null, null);
             return null;
         }
 
@@ -270,6 +194,7 @@ public final class EditContactActivity extends Activity implements View.OnClickL
     }
 
 
+
 //    /**
 //     * Instance state for {@link #mEditor} from a previous instance.
 //     */
@@ -301,7 +226,7 @@ public final class EditContactActivity extends Activity implements View.OnClickL
     @Override
     protected void onRestoreInstanceState(Bundle savedInstanceState) {
         // Read modifications from instance
-        mState = savedInstanceState.<EditState> getParcelable(KEY_EDIT_STATE);
+        mState = savedInstanceState.<EntitySet> getParcelable(KEY_EDIT_STATE);
 
 //        mSelectedRawContactId = savedInstanceState.getLong(KEY_SELECTED_TAB_ID);
 //        mContactId = savedInstanceState.getLong(KEY_CONTACT_ID);
@@ -506,55 +431,70 @@ public final class EditContactActivity extends Activity implements View.OnClickL
      * {@link EmptyService} to make sure the background thread can finish
      * persisting in cases where the system wants to reclaim our process.
      */
-    public static class PersistTask extends WeakAsyncTask<EditState, Void, Boolean, Context> {
-        public PersistTask(Context context) {
-            super(context);
+    public static class PersistTask extends
+            WeakAsyncTask<EntitySet, Void, Integer, EditContactActivity> {
+        private static final int PERSIST_TRIES = 3;
+
+        private static final int RESULT_UNCHANGED = 0;
+        private static final int RESULT_SUCCESS = 1;
+        private static final int RESULT_FAILURE = 2;
+
+        public PersistTask(EditContactActivity target) {
+            super(target);
         }
 
         /** {@inheritDoc} */
         @Override
-        protected void onPreExecute(Context context) {
+        protected void onPreExecute(EditContactActivity target) {
             // Before starting this task, start an empty service to protect our
             // process from being reclaimed by the system.
+            final Context context = target;
             context.startService(new Intent(context, EmptyService.class));
         }
 
         /** {@inheritDoc} */
         @Override
-        protected Boolean doInBackground(Context context, EditState... params) {
-            final EditState state = params[0];
-            final ContentResolver resolver = context.getContentResolver();
-
-            boolean savedChanges = false;
-            for (EntityDelta entity : state) {
-                // TODO: remove this extremely verbose debugging
-                Log.d(TAG, "trying to persist " + entity.toString());
-                final ArrayList<ContentProviderOperation> diff = entity.buildDiff();
-
-                // Skip updates that don't change
-                if (diff.size() == 0) continue;
-                savedChanges = true;
-
-                // TODO: handle failed operations by re-reading entity
-                // may also need backoff algorithm to give failed msg after n tries
+        protected Integer doInBackground(EditContactActivity target, EntitySet... params) {
+            final ContentResolver resolver = target.getContentResolver();
 
+            int tries = 0;
+            Integer result = RESULT_FAILURE;
+            EntitySet state = params[0];
+            while (tries < PERSIST_TRIES) {
                 try {
+                    // Build operations and try applying
+                    final ArrayList<ContentProviderOperation> diff = state.buildDiff();
                     resolver.applyBatch(ContactsContract.AUTHORITY, diff);
+                    result = (diff.size() > 0) ? RESULT_SUCCESS : RESULT_UNCHANGED;
+                    break;
+
                 } catch (RemoteException e) {
-                    Log.w(TAG, "problem writing rawcontact diff", e);
+                    // Something went wrong, bail without success
+                    Log.e(TAG, "Problem persisting user edits", e);
+                    break;
+
                 } catch (OperationApplicationException e) {
-                    Log.w(TAG, "problem writing rawcontact diff", e);
+                    // Version consistency failed, re-parent change and try again
+                    Log.w(TAG, "Version consistency failed, re-parenting", e);
+                    final EntitySet newState = EntitySet.fromQuery(resolver,
+                            target.mQuerySelection, null, null);
+                    newState.mergeAfter(state);
+                    state = newState;
                 }
             }
 
-            return savedChanges;
+            return result;
         }
 
         /** {@inheritDoc} */
         @Override
-        protected void onPostExecute(Context context, Boolean result) {
-            if (result) {
+        protected void onPostExecute(EditContactActivity target, Integer result) {
+            final Context context = target;
+
+            if (result == RESULT_SUCCESS) {
                 Toast.makeText(context, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
+            } else if (result == RESULT_FAILURE) {
+                Toast.makeText(context, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show();
             }
 
             // Stop the service that was protecting us
@@ -708,13 +648,6 @@ public final class EditContactActivity extends Activity implements View.OnClickL
                     values.put(RawContacts.ACCOUNT_NAME, account.name);
                     values.put(RawContacts.ACCOUNT_TYPE, account.type);
 
-                    // Tie directly to an existing aggregate, which is turned
-                    // into an AggregationException later during persisting.
-                    final long aggregateId = target.mState.getContactId();
-                    if (aggregateId >= 0) {
-                        values.put(RawContacts.CONTACT_ID, aggregateId);
-                    }
-
                     // Parse any values from incoming intent
                     final EntityDelta insert = new EntityDelta(ValuesDelta.fromAfter(values));
                     final ContactsSource source = sources.getInflatedSource(account.type,
@@ -732,7 +665,13 @@ public final class EditContactActivity extends Activity implements View.OnClickL
                         HardCodedSources.attemptMyContactsMembership(insert, target);
                     }
 
-                    target.mState.add(insert);
+                    if (target.mState == null) {
+                        // Create state if none exists yet
+                        target.mState = EntitySet.fromSingle(insert);
+                    } else {
+                        // Add contact onto end of existing state
+                        target.mState.add(insert);
+                    }
 
                     target.bindTabs();
                     target.bindHeader();
index 80e2b39..01164f8 100644 (file)
@@ -19,6 +19,7 @@ import com.android.contacts.R;
 import com.android.contacts.model.ContactsSource;
 import com.android.contacts.model.Sources;
 import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.util.Constants;
 import com.android.contacts.util.NotifyingAsyncQueryHandler;
 import com.android.internal.policy.PolicyManager;
 
@@ -143,25 +144,13 @@ public class FastTrackWindow implements Window.Callback,
     private String[] mExcludeMimes;
 
     /**
-     * Specific MIME-type for {@link Phone#CONTENT_ITEM_TYPE} entries that
-     * distinguishes actions that should initiate a text message.
-     */
-    // TODO: We should move this to someplace more general as it is needed in a
-    // few places in the app code.
-    public static final String MIME_SMS_ADDRESS = "vnd.android.cursor.item/sms-address";
-
-    private static final String SCHEME_TEL = "tel";
-    private static final String SCHEME_SMSTO = "smsto";
-    private static final String SCHEME_MAILTO = "mailto";
-
-    /**
      * Specific mime-types that should be bumped to the front of the fast-track.
      * Other mime-types not appearing in this list follow in alphabetic order.
      */
     private static final String[] ORDERED_MIMETYPES = new String[] {
         Phone.CONTENT_ITEM_TYPE,
         Contacts.CONTENT_ITEM_TYPE,
-        MIME_SMS_ADDRESS,
+        Constants.MIME_SMS_ADDRESS,
         Email.CONTENT_ITEM_TYPE,
     };
 
@@ -605,7 +594,7 @@ public class FastTrackWindow implements Window.Callback,
             mMimeType = mimeType;
 
             // Inflate strings from cursor
-            mAlternate = MIME_SMS_ADDRESS.equals(mimeType);
+            mAlternate = Constants.MIME_SMS_ADDRESS.equals(mimeType);
             if (mAlternate && mKind.actionAltHeader != null) {
                 mHeader = mKind.actionAltHeader.inflateUsing(context, cursor);
             } else if (mKind.actionHeader != null) {
@@ -620,21 +609,21 @@ public class FastTrackWindow implements Window.Callback,
             if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
                 final String number = getAsString(cursor, Phone.NUMBER);
                 if (!TextUtils.isEmpty(number)) {
-                    final Uri callUri = Uri.fromParts(SCHEME_TEL, number, null);
+                    final Uri callUri = Uri.fromParts(Constants.SCHEME_TEL, number, null);
                     mIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, callUri);
                 }
 
-            } else if (MIME_SMS_ADDRESS.equals(mimeType)) {
+            } else if (Constants.MIME_SMS_ADDRESS.equals(mimeType)) {
                 final String number = getAsString(cursor, Phone.NUMBER);
                 if (!TextUtils.isEmpty(number)) {
-                    final Uri smsUri = Uri.fromParts(SCHEME_SMSTO, number, null);
+                    final Uri smsUri = Uri.fromParts(Constants.SCHEME_SMSTO, number, null);
                     mIntent = new Intent(Intent.ACTION_SENDTO, smsUri);
                 }
 
             } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
                 final String address = getAsString(cursor, Email.DATA);
                 if (!TextUtils.isEmpty(address)) {
-                    final Uri mailUri = Uri.fromParts(SCHEME_MAILTO, address, null);
+                    final Uri mailUri = Uri.fromParts(Constants.SCHEME_MAILTO, address, null);
                     mIntent = new Intent(Intent.ACTION_SENDTO, mailUri);
                 }
 
@@ -902,9 +891,9 @@ public class FastTrackWindow implements Window.Callback,
 
             // If phone number, also insert as text message action
             if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) && kind != null) {
-                final Action action = new DataAction(mContext, source, MIME_SMS_ADDRESS, kind,
-                        cursor);
-                considerAdd(action, MIME_SMS_ADDRESS);
+                final Action action = new DataAction(mContext, source, Constants.MIME_SMS_ADDRESS,
+                        kind, cursor);
+                considerAdd(action, Constants.MIME_SMS_ADDRESS);
             }
         }
 
diff --git a/src/com/android/contacts/util/Constants.java b/src/com/android/contacts/util/Constants.java
new file mode 100644 (file)
index 0000000..696717e
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.util;
+
+import android.app.Service;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+
+/**
+ * Background {@link Service} that is used to keep our process alive long enough
+ * for background threads to finish. Started and stopped directly by specific
+ * background tasks when needed.
+ */
+public class Constants {
+    /**
+     * Specific MIME-type for {@link Phone#CONTENT_ITEM_TYPE} entries that
+     * distinguishes actions that should initiate a text message.
+     */
+    public static final String MIME_SMS_ADDRESS = "vnd.android.cursor.item/sms-address";
+
+    public static final String SCHEME_TEL = "tel";
+    public static final String SCHEME_SMSTO = "smsto";
+    public static final String SCHEME_MAILTO = "mailto";
+
+}
index be2dd35..73318cb 100644 (file)
@@ -18,6 +18,7 @@ package com.android.contacts;
 
 import com.android.contacts.model.EntityDelta;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.google.android.collect.Lists;
 
 import static android.content.ContentProviderOperation.TYPE_INSERT;
 import static android.content.ContentProviderOperation.TYPE_UPDATE;
@@ -44,15 +45,15 @@ import java.util.ArrayList;
  */
 @LargeTest
 public class EntityDeltaTests extends AndroidTestCase {
-    public static final String TAG = "AugmentedEntityTests";
+    public static final String TAG = "EntityDeltaTests";
 
-    private static final long TEST_CONTACT_ID = 12;
-    private static final long TEST_PHONE_ID = 24;
+    public static final long TEST_CONTACT_ID = 12;
+    public static final long TEST_PHONE_ID = 24;
 
-    private static final String TEST_PHONE_NUMBER_1 = "218-555-1111";
-    private static final String TEST_PHONE_NUMBER_2 = "218-555-2222";
+    public static final String TEST_PHONE_NUMBER_1 = "218-555-1111";
+    public static final String TEST_PHONE_NUMBER_2 = "218-555-2222";
 
-    private static final String TEST_ACCOUNT_NAME = "TEST";
+    public static final String TEST_ACCOUNT_NAME = "TEST";
 
     public EntityDeltaTests() {
         super();
@@ -63,7 +64,7 @@ public class EntityDeltaTests extends AndroidTestCase {
         mContext = getContext();
     }
 
-    protected Entity getEntity(long contactId, long phoneId) {
+    public static Entity getEntity(long contactId, long phoneId) {
         // Build an existing contact read from database
         final ContentValues contact = new ContentValues();
         contact.put(RawContacts.VERSION, 43);
@@ -81,10 +82,10 @@ public class EntityDeltaTests extends AndroidTestCase {
     }
 
     /**
-     * Test that {@link EntityDelta#augmentTo(Parcel)} correctly passes any
-     * changes through the {@link Parcel} object. This enforces that
-     * {@link EntityDelta} should be identical when serialized against the
-     * same "before" {@link Entity}.
+     * Test that {@link EntityDelta#mergeAfter(EntityDelta)} correctly passes
+     * any changes through the {@link Parcel} object. This enforces that
+     * {@link EntityDelta} should be identical when serialized against the same
+     * "before" {@link Entity}.
      */
     public void testParcelChangesNone() {
         final Entity before = getEntity(TEST_CONTACT_ID, TEST_PHONE_ID);
@@ -142,9 +143,9 @@ public class EntityDeltaTests extends AndroidTestCase {
     }
 
     /**
-     * Test that {@link ValuesDelta#buildDiff()} is correctly built for
-     * insert, update, and delete cases. Note this only tests behavior for
-     * individual {@link Data} rows.
+     * Test that {@link ValuesDelta#buildDiff(android.net.Uri)} is correctly
+     * built for insert, update, and delete cases. Note this only tests behavior
+     * for individual {@link Data} rows.
      */
     public void testValuesDiffNone() {
         final ContentValues before = new ContentValues();
@@ -199,7 +200,7 @@ public class EntityDeltaTests extends AndroidTestCase {
     }
 
     /**
-     * Test that {@link EntityDelta#buildDiff()} is correctly built for
+     * Test that {@link EntityDelta#buildDiff(ArrayList)} is correctly built for
      * insert, update, and delete cases. This only tests a subset of possible
      * {@link Data} row changes.
      */
@@ -208,7 +209,8 @@ public class EntityDeltaTests extends AndroidTestCase {
         final EntityDelta source = EntityDelta.fromBefore(before);
 
         // Assert that writing unchanged produces few operations
-        final ArrayList<ContentProviderOperation> diff = source.buildDiff();
+        final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+        source.buildDiff(diff);
 
         assertTrue("Created changes when none needed", (diff.size() == 0));
     }
@@ -225,7 +227,9 @@ public class EntityDeltaTests extends AndroidTestCase {
         source.addEntry(ValuesDelta.fromAfter(phone));
 
         // Assert two operations: insert Data row and enforce version
-        final ArrayList<ContentProviderOperation> diff = source.buildDiff();
+        final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+        source.buildAssert(diff);
+        source.buildDiff(diff);
         assertEquals("Unexpected operations", 4, diff.size());
         {
             final ContentProviderOperation oper = diff.get(0);
@@ -263,7 +267,9 @@ public class EntityDeltaTests extends AndroidTestCase {
         source.addEntry(ValuesDelta.fromAfter(phone));
 
         // Assert three operations: update Contact, insert Data row, enforce version
-        final ArrayList<ContentProviderOperation> diff = source.buildDiff();
+        final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+        source.buildAssert(diff);
+        source.buildDiff(diff);
         assertEquals("Unexpected operations", 5, diff.size());
         {
             final ContentProviderOperation oper = diff.get(0);
@@ -299,8 +305,10 @@ public class EntityDeltaTests extends AndroidTestCase {
         final ValuesDelta child = source.getEntry(TEST_PHONE_ID);
         child.put(Phone.NUMBER, TEST_PHONE_NUMBER_2);
 
-        // Assert two operations: update Data and enforce version
-        final ArrayList<ContentProviderOperation> diff = source.buildDiff();
+        // Assert that version is enforced
+        final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+        source.buildAssert(diff);
+        source.buildDiff(diff);
         assertEquals("Unexpected operations", 4, diff.size());
         {
             final ContentProviderOperation oper = diff.get(0);
@@ -331,7 +339,9 @@ public class EntityDeltaTests extends AndroidTestCase {
         source.getValues().markDeleted();
 
         // Assert two operations: delete Contact and enforce version
-        final ArrayList<ContentProviderOperation> diff = source.buildDiff();
+        final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+        source.buildAssert(diff);
+        source.buildDiff(diff);
         assertEquals("Unexpected operations", 2, diff.size());
         {
             final ContentProviderOperation oper = diff.get(0);
@@ -354,7 +364,9 @@ public class EntityDeltaTests extends AndroidTestCase {
         final EntityDelta source = new EntityDelta(values);
 
         // Assert two operations: delete Contact and enforce version
-        final ArrayList<ContentProviderOperation> diff = source.buildDiff();
+        final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+        source.buildAssert(diff);
+        source.buildDiff(diff);
         assertEquals("Unexpected operations", 1, diff.size());
         {
             final ContentProviderOperation oper = diff.get(0);
@@ -380,7 +392,9 @@ public class EntityDeltaTests extends AndroidTestCase {
         source.addEntry(ValuesDelta.fromAfter(phone));
 
         // Assert two operations: delete Contact and enforce version
-        final ArrayList<ContentProviderOperation> diff = source.buildDiff();
+        final ArrayList<ContentProviderOperation> diff = Lists.newArrayList();
+        source.buildAssert(diff);
+        source.buildDiff(diff);
         assertEquals("Unexpected operations", 2, diff.size());
         {
             final ContentProviderOperation oper = diff.get(0);
diff --git a/tests/src/com/android/contacts/EntitySetTests.java b/tests/src/com/android/contacts/EntitySetTests.java
new file mode 100644 (file)
index 0000000..89fb455
--- /dev/null
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts;
+
+import com.android.contacts.model.EntityDelta;
+import com.android.contacts.model.EntitySet;
+import com.android.contacts.model.EntityDelta.ValuesDelta;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentValues;
+import android.content.Entity;
+import android.provider.ContactsContract.AggregationExceptions;
+import android.provider.ContactsContract.RawContacts;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import java.util.ArrayList;
+
+/**
+ * Tests for {@link EntitySet} which focus on "diff" operations that should
+ * create {@link AggregationExceptions} in certain cases.
+ */
+@LargeTest
+public class EntitySetTests extends AndroidTestCase {
+    public static final String TAG = "EntitySetTests";
+
+    private static final long CONTACT_FIRST = 1;
+    private static final long CONTACT_SECOND = 2;
+
+    public EntitySetTests() {
+        super();
+    }
+
+    @Override
+    public void setUp() {
+        mContext = getContext();
+    }
+
+    protected EntityDelta getUpdate(long rawContactId) {
+        final Entity before = EntityDeltaTests.getEntity(rawContactId,
+                EntityDeltaTests.TEST_PHONE_ID);
+        return EntityDelta.fromBefore(before);
+    }
+
+    protected EntityDelta getInsert() {
+        final ContentValues after = new ContentValues();
+        after.put(RawContacts.ACCOUNT_NAME, EntityDeltaTests.TEST_ACCOUNT_NAME);
+        after.put(RawContacts.SEND_TO_VOICEMAIL, 1);
+
+        final ValuesDelta values = ValuesDelta.fromAfter(after);
+        return new EntityDelta(values);
+    }
+
+    protected EntitySet setFrom(EntityDelta... deltas) {
+        final EntitySet set = EntitySet.fromSingle(deltas[0]);
+        for (int i = 1; i < deltas.length; i++) {
+            set.add(deltas[i]);
+        }
+        return set;
+    }
+
+    /**
+     * Count number of {@link AggregationExceptions} updates contained in the
+     * given list of {@link ContentProviderOperation}.
+     */
+    protected int countExceptionUpdates(ArrayList<ContentProviderOperation> diff) {
+        int updateCount = 0;
+        for (ContentProviderOperation oper : diff) {
+            if (AggregationExceptions.CONTENT_URI.equals(oper.getUri())
+                    && oper.getType() == ContentProviderOperation.TYPE_UPDATE) {
+                updateCount++;
+            }
+        }
+        return updateCount;
+    }
+
+    public void testInsert() {
+        final EntityDelta insert = getInsert();
+        final EntitySet set = setFrom(insert);
+
+        // Inserting single shouldn't create rules
+        final ArrayList<ContentProviderOperation> diff = set.buildDiff();
+        final int exceptionCount = countExceptionUpdates(diff);
+        assertEquals("Unexpected exception updates", 0, exceptionCount);
+    }
+
+    public void testUpdateUpdate() {
+        final EntityDelta updateFirst = getUpdate(CONTACT_FIRST);
+        final EntityDelta updateSecond = getUpdate(CONTACT_SECOND);
+        final EntitySet set = setFrom(updateFirst, updateSecond);
+
+        // Updating two existing shouldn't create rules
+        final ArrayList<ContentProviderOperation> diff = set.buildDiff();
+        final int exceptionCount = countExceptionUpdates(diff);
+        assertEquals("Unexpected exception updates", 0, exceptionCount);
+    }
+
+    public void testUpdateInsert() {
+        final EntityDelta update = getUpdate(CONTACT_FIRST);
+        final EntityDelta insert = getInsert();
+        final EntitySet set = setFrom(update, insert);
+
+        // New insert should only create one rule
+        final ArrayList<ContentProviderOperation> diff = set.buildDiff();
+        final int exceptionCount = countExceptionUpdates(diff);
+        assertEquals("Unexpected exception updates", 1, exceptionCount);
+    }
+
+    public void testInsertUpdateInsert() {
+        final EntityDelta insertFirst = getInsert();
+        final EntityDelta update = getUpdate(CONTACT_FIRST);
+        final EntityDelta insertSecond = getInsert();
+        final EntitySet set = setFrom(insertFirst, update, insertSecond);
+
+        // Two inserts should create two rules to bind against single existing
+        final ArrayList<ContentProviderOperation> diff = set.buildDiff();
+        final int exceptionCount = countExceptionUpdates(diff);
+        assertEquals("Unexpected exception updates", 2, exceptionCount);
+    }
+
+    public void testInsertInsertInsert() {
+        final EntityDelta insertFirst = getInsert();
+        final EntityDelta insertSecond = getInsert();
+        final EntityDelta insertThird = getInsert();
+        final EntitySet set = setFrom(insertFirst, insertSecond, insertThird);
+
+        // Three new inserts should create only two binding rules
+        final ArrayList<ContentProviderOperation> diff = set.buildDiff();
+        final int exceptionCount = countExceptionUpdates(diff);
+        assertEquals("Unexpected exception updates", 2, exceptionCount);
+    }
+}