Merge "Check for Voice capability using the system resource"
diff --git a/Android.mk b/Android.mk
index e7624f7..1bebf33 100644
--- a/Android.mk
+++ b/Android.mk
@@ -5,7 +5,7 @@
 
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
-LOCAL_STATIC_JAVA_LIBRARIES := com.android.phone.common com.android.vcard
+LOCAL_STATIC_JAVA_LIBRARIES := com.android.phone.common com.android.vcard android-common
 
 LOCAL_PACKAGE_NAME := Contacts
 LOCAL_CERTIFICATE := shared
diff --git a/res/values/strings.xml b/res/values/strings.xml
index ecfe5c8..2458d80 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1088,6 +1088,8 @@
     <!-- Generic action string for starting an IM chat -->
     <string name="chat">Chat</string>
 
+    <!-- Field title for the full postal address of a contact [CHAR LIMIT=64]-->
+    <string name="postal_address">Address</string>
     <!-- Field title for the street of a structured postal address of a contact -->
     <string name="postal_street">Street</string>
     <!-- Field title for the PO box of a structured postal address of a contact -->
diff --git a/src/com/android/contacts/list/ContactEntryListFragment.java b/src/com/android/contacts/list/ContactEntryListFragment.java
index 708cdd8..1315922 100644
--- a/src/com/android/contacts/list/ContactEntryListFragment.java
+++ b/src/com/android/contacts/list/ContactEntryListFragment.java
@@ -16,13 +16,13 @@
 
 package com.android.contacts.list;
 
+import com.android.common.widget.CompositeCursorAdapter.Partition;
 import com.android.contacts.ContactEntryListView;
 import com.android.contacts.ContactListEmptyView;
 import com.android.contacts.ContactPhotoLoader;
 import com.android.contacts.ContactsSearchManager;
 import com.android.contacts.R;
 import com.android.contacts.ui.ContactsPreferences;
-import com.android.contacts.widget.CompositeCursorAdapter.Partition;
 import com.android.contacts.widget.ContextMenuAdapter;
 
 import android.accounts.Account;
@@ -42,6 +42,7 @@
 import android.database.Cursor;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.Message;
 import android.os.Parcelable;
 import android.os.RemoteException;
 import android.provider.ContactsContract;
@@ -96,6 +97,9 @@
 
     private static final int DIRECTORY_LOADER_ID = -1;
 
+    private static final int DIRECTORY_SEARCH_DELAY_MILLIS = 300;
+    private static final int DIRECTORY_SEARCH_MESSAGE = 1;
+
     private boolean mSectionHeaderDisplayEnabled;
     private boolean mPhotoLoaderEnabled;
     private boolean mSearchMode;
@@ -140,6 +144,15 @@
 
     private LoaderManager mLoaderManager;
 
+    private Handler mDelayedDirectorySearchHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            if (msg.what == DIRECTORY_SEARCH_MESSAGE) {
+                loadDirectoryPartition(msg.arg1, (DirectoryPartition) msg.obj);
+            }
+        }
+    };
+
     protected abstract View inflateView(LayoutInflater inflater, ViewGroup container);
     protected abstract T createListAdapter();
 
@@ -315,16 +328,48 @@
 
     private void startLoadingDirectoryPartition(int partitionIndex) {
         DirectoryPartition partition = (DirectoryPartition)mAdapter.getPartition(partitionIndex);
-        Bundle args = new Bundle();
         long directoryId = partition.getDirectoryId();
-        args.putLong(DIRECTORY_ID_ARG_KEY, directoryId);
         if (mForceLoad) {
-            getLoaderManager().restartLoader(partitionIndex, args, this);
+            if (directoryId == Directory.DEFAULT) {
+                loadDirectoryPartition(partitionIndex, partition);
+            } else {
+                loadDirectoryPartitionDelayed(partitionIndex, partition);
+            }
         } else {
+            Bundle args = new Bundle();
+            args.putLong(DIRECTORY_ID_ARG_KEY, directoryId);
             getLoaderManager().initLoader(partitionIndex, args, this);
         }
     }
 
+    /**
+     * Queues up a delayed request to search the specified directory. Since
+     * directory search will likely introduce a lot of network traffic, we want
+     * to wait for a pause in the user's typing before sending a directory request.
+     */
+    private void loadDirectoryPartitionDelayed(int partitionIndex, DirectoryPartition partition) {
+        mDelayedDirectorySearchHandler.removeMessages(DIRECTORY_SEARCH_MESSAGE, partition);
+        Message msg = mDelayedDirectorySearchHandler.obtainMessage(
+                DIRECTORY_SEARCH_MESSAGE, partitionIndex, 0, partition);
+        mDelayedDirectorySearchHandler.sendMessageDelayed(msg, DIRECTORY_SEARCH_DELAY_MILLIS);
+    }
+
+    /**
+     * Loads the directory partition.
+     */
+    protected void loadDirectoryPartition(int partitionIndex, DirectoryPartition partition) {
+        Bundle args = new Bundle();
+        args.putLong(DIRECTORY_ID_ARG_KEY, partition.getDirectoryId());
+        getLoaderManager().restartLoader(partitionIndex, args, this);
+    }
+
+    /**
+     * Cancels all queued directory loading requests.
+     */
+    private void removePendingDirectorySearchRequests() {
+        mDelayedDirectorySearchHandler.removeMessages(DIRECTORY_SEARCH_MESSAGE);
+    }
+
     @Override
     public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
         if (!checkProviderStatus(false)) {
@@ -336,6 +381,7 @@
 
         int loaderId = loader.getId();
         if (loaderId == DIRECTORY_LOADER_ID) {
+            removePendingDirectorySearchRequests();
             mAdapter.changeDirectories(data);
         } else {
             onPartitionLoaded(loaderId, data);
@@ -727,6 +773,7 @@
     @Override
     public void onPause() {
         super.onPause();
+        removePendingDirectorySearchRequests();
         unregisterProviderStatusObserver();
     }
 
diff --git a/src/com/android/contacts/list/DirectoryPartition.java b/src/com/android/contacts/list/DirectoryPartition.java
index d7cb9bc..b55ed31 100644
--- a/src/com/android/contacts/list/DirectoryPartition.java
+++ b/src/com/android/contacts/list/DirectoryPartition.java
@@ -15,7 +15,7 @@
  */
 package com.android.contacts.list;
 
-import com.android.contacts.widget.CompositeCursorAdapter;
+import com.android.common.widget.CompositeCursorAdapter;
 
 import android.provider.ContactsContract.Directory;
 
diff --git a/src/com/android/contacts/model/ContactsSource.java b/src/com/android/contacts/model/ContactsSource.java
index d498398..b417224 100644
--- a/src/com/android/contacts/model/ContactsSource.java
+++ b/src/com/android/contacts/model/ContactsSource.java
@@ -25,11 +25,12 @@
 import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.graphics.drawable.Drawable;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.RawContacts;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.view.View;
 import android.widget.EditText;
 
 import java.util.ArrayList;
@@ -221,10 +222,17 @@
 
         public ContentValues defaultValues;
 
+        public Class<? extends View> editorClass;
+
         public DataKind() {
         }
 
         public DataKind(String mimeType, int titleRes, int iconRes, int weight, boolean editable) {
+            this(mimeType, titleRes, iconRes, weight, editable, null);
+        }
+
+        public DataKind(String mimeType, int titleRes, int iconRes, int weight, boolean editable,
+                Class<? extends View> editorClass) {
             this.mimeType = mimeType;
             this.titleRes = titleRes;
             this.iconRes = iconRes;
@@ -232,6 +240,7 @@
             this.editable = editable;
             this.isList = true;
             this.typeOverallMax = -1;
+            this.editorClass = editorClass;
         }
     }
 
@@ -324,6 +333,11 @@
             this.longForm = longForm;
             return this;
         }
+
+        public EditField setMinLines(int minLines) {
+            this.minLines = minLines;
+            return this;
+        }
     }
 
     /**
diff --git a/src/com/android/contacts/model/Editor.java b/src/com/android/contacts/model/Editor.java
index 04e023b..c73839d 100644
--- a/src/com/android/contacts/model/Editor.java
+++ b/src/com/android/contacts/model/Editor.java
@@ -58,6 +58,8 @@
     public void setValues(DataKind kind, ValuesDelta values, EntityDelta state, boolean readOnly,
             ViewIdGenerator vig);
 
+    public void setDeletable(boolean deletable);
+
     /**
      * Add a specific {@link EditorListener} to this {@link Editor}.
      */
diff --git a/src/com/android/contacts/model/EntityDelta.java b/src/com/android/contacts/model/EntityDelta.java
index 4f018a9..e353d70 100644
--- a/src/com/android/contacts/model/EntityDelta.java
+++ b/src/com/android/contacts/model/EntityDelta.java
@@ -578,6 +578,21 @@
             }
         }
 
+        public boolean isChanged(String key) {
+            if (mAfter == null || !mAfter.containsKey(key)) {
+                return false;
+            }
+
+            Object newValue = mAfter.get(key);
+            Object oldValue = mBefore.get(key);
+
+            if (oldValue == null) {
+                return newValue != null;
+            }
+
+            return !oldValue.equals(newValue);
+        }
+
         public String getMimetype() {
             return getAsString(Data.MIMETYPE);
         }
diff --git a/src/com/android/contacts/model/FallbackSource.java b/src/com/android/contacts/model/FallbackSource.java
index a685e6d..e052fed 100644
--- a/src/com/android/contacts/model/FallbackSource.java
+++ b/src/com/android/contacts/model/FallbackSource.java
@@ -263,8 +263,6 @@
         }
 
         if (inflateLevel >= ContactsSource.LEVEL_CONSTRAINTS) {
-            final boolean useJapaneseOrder =
-                Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage());
             kind.typeColumn = StructuredPostal.TYPE;
             kind.typeList = Lists.newArrayList();
             kind.typeList.add(buildPostalType(StructuredPostal.TYPE_HOME));
@@ -274,38 +272,9 @@
                     .setCustomColumn(StructuredPostal.LABEL));
 
             kind.fieldList = Lists.newArrayList();
-
-            if (useJapaneseOrder) {
-                kind.fieldList.add(new EditField(StructuredPostal.COUNTRY,
-                        R.string.postal_country, FLAGS_POSTAL).setOptional(true));
-                kind.fieldList.add(new EditField(StructuredPostal.POSTCODE,
-                        R.string.postal_postcode, FLAGS_POSTAL));
-                kind.fieldList.add(new EditField(StructuredPostal.REGION,
-                        R.string.postal_region, FLAGS_POSTAL));
-                kind.fieldList.add(new EditField(StructuredPostal.CITY,
-                        R.string.postal_city, FLAGS_POSTAL));
-                kind.fieldList.add(new EditField(StructuredPostal.NEIGHBORHOOD,
-                        R.string.postal_neighborhood, FLAGS_POSTAL).setOptional(true));
-                kind.fieldList.add(new EditField(StructuredPostal.STREET,
-                        R.string.postal_street, FLAGS_POSTAL));
-                kind.fieldList.add(new EditField(StructuredPostal.POBOX,
-                        R.string.postal_pobox, FLAGS_POSTAL).setOptional(true));
-            } else {
-                kind.fieldList.add(new EditField(StructuredPostal.STREET,
-                        R.string.postal_street, FLAGS_POSTAL));
-                kind.fieldList.add(new EditField(StructuredPostal.POBOX,
-                        R.string.postal_pobox, FLAGS_POSTAL).setOptional(true));
-                kind.fieldList.add(new EditField(StructuredPostal.NEIGHBORHOOD,
-                        R.string.postal_neighborhood, FLAGS_POSTAL).setOptional(true));
-                kind.fieldList.add(new EditField(StructuredPostal.CITY,
-                        R.string.postal_city, FLAGS_POSTAL));
-                kind.fieldList.add(new EditField(StructuredPostal.REGION,
-                        R.string.postal_region, FLAGS_POSTAL));
-                kind.fieldList.add(new EditField(StructuredPostal.POSTCODE,
-                        R.string.postal_postcode, FLAGS_POSTAL));
-                kind.fieldList.add(new EditField(StructuredPostal.COUNTRY,
-                        R.string.postal_country, FLAGS_POSTAL).setOptional(true));
-            }
+            kind.fieldList.add(
+                    new EditField(StructuredPostal.FORMATTED_ADDRESS, R.string.postal_address,
+                            FLAGS_POSTAL).setMinLines(3));
         }
 
         return kind;
diff --git a/src/com/android/contacts/ui/widget/GenericEditorView.java b/src/com/android/contacts/ui/widget/GenericEditorView.java
index b5e0c4f..6be238f 100644
--- a/src/com/android/contacts/ui/widget/GenericEditorView.java
+++ b/src/com/android/contacts/ui/widget/GenericEditorView.java
@@ -43,6 +43,7 @@
 import android.text.TextWatcher;
 import android.util.AttributeSet;
 import android.view.ContextThemeWrapper;
+import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -164,13 +165,38 @@
 
         // summarize the EditText heights
         int totalHeight = 0;
+        int visibleFieldCount = 0;
+        EditText firstVisibleField = null;
         if (mFieldEditTexts != null) {
             for (EditText editText : mFieldEditTexts) {
                 if (editText.getVisibility() != View.GONE) {
+                    visibleFieldCount ++;
+                    if (firstVisibleField == null) {
+                        firstVisibleField = editText;
+                    }
                     totalHeight += editText.getMeasuredHeight();
                 }
             }
         }
+
+        int padding = getPaddingTop() + getPaddingBottom();
+        int minHeight = padding;
+
+        if (mMoreOrLess != null) {
+            minHeight += mMoreOrLess.getMeasuredHeight();
+        }
+
+        if (mDelete != null) {
+            minHeight += mDelete.getMeasuredHeight();
+        }
+
+        if (minHeight > totalHeight && visibleFieldCount == 1) {
+            firstVisibleField.measure(widthMeasureSpec,
+                    MeasureSpec.makeMeasureSpec(minHeight - padding, MeasureSpec.EXACTLY));
+        }
+
+        totalHeight = Math.max(minHeight, totalHeight);
+
         setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                 resolveSize(totalHeight, heightMeasureSpec));
     }
@@ -259,9 +285,7 @@
 
                         // Reconfigure GUI
                         mHideOptional = !mHideOptional;
-                        if (mListener != null) {
-                            mListener.onRequest(EditorListener.EDITOR_FORM_CHANGED);
-                        }
+                        onOptionalFieldVisibilityChange();
                         rebuildValues();
 
                         // Restore focus
@@ -285,6 +309,12 @@
         }
     }
 
+    protected void onOptionalFieldVisibilityChange() {
+        if (mListener != null) {
+            mListener.onRequest(EditorListener.EDITOR_FORM_CHANGED);
+        }
+    }
+
     public void setEditorListener(EditorListener listener) {
         mListener = listener;
     }
@@ -398,6 +428,7 @@
             final EditText fieldView = new EditText(mContext);
             fieldView.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT,
                     LayoutParams.WRAP_CONTENT));
+            fieldView.setGravity(Gravity.TOP);
             mFieldEditTexts[index] = fieldView;
             fieldView.setId(vig.getId(state, kind, entry, index));
             if (field.titleRes > 0) {
diff --git a/src/com/android/contacts/ui/widget/KindSectionView.java b/src/com/android/contacts/ui/widget/KindSectionView.java
index cb20566..cd0b6fb 100644
--- a/src/com/android/contacts/ui/widget/KindSectionView.java
+++ b/src/com/android/contacts/ui/widget/KindSectionView.java
@@ -17,12 +17,12 @@
 package com.android.contacts.ui.widget;
 
 import com.android.contacts.R;
-import com.android.contacts.model.Editor;
-import com.android.contacts.model.EntityDelta;
-import com.android.contacts.model.EntityModifier;
 import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.model.Editor;
 import com.android.contacts.model.Editor.EditorListener;
+import com.android.contacts.model.EntityDelta;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
+import com.android.contacts.model.EntityModifier;
 import com.android.contacts.ui.ViewIdGenerator;
 
 import android.content.Context;
@@ -137,13 +137,27 @@
                 if (!entry.isVisible()) continue;
                 if (isEmptyNoop(entry)) continue;
 
-                final GenericEditorView editor = new GenericEditorView(mContext);
+                final View view;
+                if (mKind.editorClass == null) {
+                    view = new GenericEditorView(mContext);
+                } else {
+                    try {
+                        view = mKind.editorClass.getConstructor(Context.class).newInstance(
+                                mContext);
+                    } catch (Exception e) {
+                        throw new RuntimeException(
+                                "Cannot allocate editor for " + mKind.editorClass);
+                    }
+                }
 
-                editor.setPadding(0, 0, getThemeScrollbarSize(mContext), 0);
-                editor.setValues(mKind, entry, mState, mReadOnly, mViewIdGenerator);
-                editor.setEditorListener(this);
-                editor.setDeletable(true);
-                mEditors.addView(editor);
+                view.setPadding(0, 0, getThemeScrollbarSize(mContext), 0);
+                if (view instanceof Editor) {
+                    Editor editor = (Editor) view;
+                    editor.setValues(mKind, entry, mState, mReadOnly, mViewIdGenerator);
+                    editor.setEditorListener(this);
+                    editor.setDeletable(true);
+                }
+                mEditors.addView(view);
                 entryIndex++;
             }
         }
diff --git a/src/com/android/contacts/ui/widget/PhotoEditorView.java b/src/com/android/contacts/ui/widget/PhotoEditorView.java
index eff39d0..da1be85 100644
--- a/src/com/android/contacts/ui/widget/PhotoEditorView.java
+++ b/src/com/android/contacts/ui/widget/PhotoEditorView.java
@@ -168,4 +168,9 @@
     public void setEditorListener(EditorListener listener) {
         mListener = listener;
     }
+
+    @Override
+    public void setDeletable(boolean deletable) {
+        // Photo is not deletable
+    }
 }
diff --git a/src/com/android/contacts/widget/CompositeCursorAdapter.java b/src/com/android/contacts/widget/CompositeCursorAdapter.java
deleted file mode 100644
index c6aa775..0000000
--- a/src/com/android/contacts/widget/CompositeCursorAdapter.java
+++ /dev/null
@@ -1,492 +0,0 @@
-/*
- * Copyright (C) 2010 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.widget;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.BaseAdapter;
-
-/**
- * A general purpose adapter that is composed of multiple cursors. It just
- * appends them in the order they are added.
- */
-public abstract class CompositeCursorAdapter extends BaseAdapter {
-
-    private static final int INITIAL_CAPACITY = 2;
-
-    public static class Partition {
-        boolean showIfEmpty;
-        boolean hasHeader;
-
-        Cursor cursor;
-        int idColumnIndex;
-        int count;
-
-        public Partition(boolean showIfEmpty, boolean hasHeader) {
-            this.showIfEmpty = showIfEmpty;
-            this.hasHeader = hasHeader;
-        }
-
-        /**
-         * True if the directory should be shown even if no contacts are found.
-         */
-        public boolean getShowIfEmpty() {
-            return showIfEmpty;
-        }
-
-        public boolean getHasHeader() {
-            return hasHeader;
-        }
-    }
-
-    private final Context mContext;
-    private Partition[] mPartitions;
-    private int mSize = 0;
-    private int mCount = 0;
-    private boolean mCacheValid = true;
-
-    public CompositeCursorAdapter(Context context) {
-        this(context, INITIAL_CAPACITY);
-    }
-
-    public CompositeCursorAdapter(Context context, int initialCapacity) {
-        mContext = context;
-        mPartitions = new Partition[INITIAL_CAPACITY];
-    }
-
-    public Context getContext() {
-        return mContext;
-    }
-
-    /**
-     * Registers a partition. The cursor for that partition can be set later.
-     * Partitions should be added in the order they are supposed to appear in the
-     * list.
-     */
-    public void addPartition(boolean showIfEmpty, boolean hasHeader) {
-        addPartition(new Partition(showIfEmpty, hasHeader));
-    }
-
-    public void addPartition(Partition partition) {
-        if (mSize >= mPartitions.length) {
-            int newCapacity = mSize + 2;
-            Partition[] newAdapters = new Partition[newCapacity];
-            System.arraycopy(mPartitions, 0, newAdapters, 0, mSize);
-            mPartitions = newAdapters;
-        }
-        mPartitions[mSize++] = partition;
-        invalidate();
-        notifyDataSetChanged();
-    }
-
-    public void removePartition(int partitionIndex) {
-        Cursor cursor = mPartitions[partitionIndex].cursor;
-        if (cursor != null && !cursor.isClosed()) {
-            cursor.close();
-        }
-
-        System.arraycopy(mPartitions, partitionIndex + 1, mPartitions, partitionIndex,
-                mSize - partitionIndex - 1);
-        mSize--;
-        invalidate();
-        notifyDataSetChanged();
-    }
-
-    /**
-     * Removes cursors for all partitions.
-     */
-    public void clearPartitions() {
-        for (int i = 0; i < mSize; i++) {
-            mPartitions[i].cursor = null;
-        }
-        invalidate();
-        notifyDataSetChanged();
-    }
-
-    public void setHasHeader(int partitionIndex, boolean flag) {
-        mPartitions[partitionIndex].hasHeader = flag;
-        invalidate();
-    }
-
-    public void setShowIfEmpty(int partitionIndex, boolean flag) {
-        mPartitions[partitionIndex].showIfEmpty = flag;
-        invalidate();
-    }
-
-    public Partition getPartition(int partitionIndex) {
-        if (partitionIndex >= mSize) {
-            throw new ArrayIndexOutOfBoundsException(partitionIndex);
-        }
-        return mPartitions[partitionIndex];
-    }
-
-    protected void invalidate() {
-        mCacheValid = false;
-    }
-
-    public int getPartitionCount() {
-        return mSize;
-    }
-
-    protected void ensureCacheValid() {
-        if (mCacheValid) {
-            return;
-        }
-
-        mCount = 0;
-        for (int i = 0; i < mSize; i++) {
-            Cursor cursor = mPartitions[i].cursor;
-            int count = cursor != null ? cursor.getCount() : 0;
-            if (mPartitions[i].hasHeader) {
-                if (count != 0 || mPartitions[i].showIfEmpty) {
-                    count++;
-                }
-            }
-            mPartitions[i].count = count;
-            mCount += count;
-        }
-
-        mCacheValid = true;
-    }
-
-    /**
-     * Returns true if the specified partition was configured to have a header.
-     */
-    public boolean hasHeader(int partition) {
-        return mPartitions[partition].hasHeader;
-    }
-
-    /**
-     * Returns the total number of list items in all partitions.
-     */
-    public int getCount() {
-        ensureCacheValid();
-        return mCount;
-    }
-
-    /**
-     * Returns the cursor for the given partition
-     */
-    public Cursor getCursor(int partition) {
-        return mPartitions[partition].cursor;
-    }
-
-    /**
-     * Changes the cursor for an individual partition.
-     */
-    public void changeCursor(int partition, Cursor cursor) {
-        Cursor prevCursor = mPartitions[partition].cursor;
-        if (prevCursor != cursor) {
-            if (prevCursor != null && !prevCursor.isClosed()) {
-                prevCursor.close();
-            }
-            mPartitions[partition].cursor = cursor;
-            if (cursor != null) {
-                mPartitions[partition].idColumnIndex = cursor.getColumnIndex("_id");
-            }
-            invalidate();
-            notifyDataSetChanged();
-        }
-    }
-
-    /**
-     * Returns true if the specified partition has no cursor or an empty cursor.
-     */
-    public boolean isPartitionEmpty(int partition) {
-        Cursor cursor = mPartitions[partition].cursor;
-        return cursor == null || cursor.getCount() == 0;
-    }
-
-    /**
-     * Given a list position, returns the index of the corresponding partition.
-     */
-    public int getPartitionForPosition(int position) {
-        ensureCacheValid();
-        int start = 0;
-        for (int i = 0; i < mSize; i++) {
-            int end = start + mPartitions[i].count;
-            if (position >= start && position < end) {
-                return i;
-            }
-            start = end;
-        }
-        return -1;
-    }
-
-    /**
-     * Given a list position, return the offset of the corresponding item in its
-     * partition.  The header, if any, will have offset -1.
-     */
-    public int getOffsetInPartition(int position) {
-        ensureCacheValid();
-        int start = 0;
-        for (int i = 0; i < mSize; i++) {
-            int end = start + mPartitions[i].count;
-            if (position >= start && position < end) {
-                int offset = position - start;
-                if (mPartitions[i].hasHeader) {
-                    offset--;
-                }
-                return offset;
-            }
-            start = end;
-        }
-        return -1;
-    }
-
-    /**
-     * Returns the first list position for the specified partition.
-     */
-    public int getPositionForPartition(int partition) {
-        ensureCacheValid();
-        int position = 0;
-        for (int i = 0; i < partition; i++) {
-            position += mPartitions[i].count;
-        }
-        return position;
-    }
-
-    @Override
-    public int getViewTypeCount() {
-        return getItemViewTypeCount() + 1;
-    }
-
-    /**
-     * Returns the overall number of item view types across all partitions. An
-     * implementation of this method needs to ensure that the returned count is
-     * consistent with the values returned by {@link #getItemViewType(int,int)}.
-     */
-    public int getItemViewTypeCount() {
-        return 1;
-    }
-
-    /**
-     * Returns the view type for the list item at the specified position in the
-     * specified partition.
-     */
-    protected int getItemViewType(int partition, int position) {
-        return 1;
-    }
-
-    @Override
-    public int getItemViewType(int position) {
-        ensureCacheValid();
-        int start = 0;
-        for (int i = 0; i < mSize; i++) {
-            int end = start  + mPartitions[i].count;
-            if (position >= start && position < end) {
-                int offset = position - start;
-                if (mPartitions[i].hasHeader && offset == 0) {
-                    return IGNORE_ITEM_VIEW_TYPE;
-                }
-                return getItemViewType(i, position);
-            }
-            start = end;
-        }
-
-        throw new ArrayIndexOutOfBoundsException(position);
-    }
-
-    public View getView(int position, View convertView, ViewGroup parent) {
-        ensureCacheValid();
-        int start = 0;
-        for (int i = 0; i < mSize; i++) {
-            int end = start + mPartitions[i].count;
-            if (position >= start && position < end) {
-                int offset = position - start;
-                if (mPartitions[i].hasHeader) {
-                    offset--;
-                }
-                View view;
-                if (offset == -1) {
-                    view = getHeaderView(i, mPartitions[i].cursor, convertView, parent);
-                } else {
-                    if (!mPartitions[i].cursor.moveToPosition(offset)) {
-                        throw new IllegalStateException("Couldn't move cursor to position "
-                                + offset);
-                    }
-                    view = getView(i, mPartitions[i].cursor, offset, convertView, parent);
-                }
-                if (view == null) {
-                    throw new NullPointerException("View should not be null, partition: " + i
-                            + " position: " + offset);
-                }
-                return view;
-            }
-            start = end;
-        }
-
-        throw new ArrayIndexOutOfBoundsException(position);
-    }
-
-    /**
-     * Returns the header view for the specified partition, creating one if needed.
-     */
-    protected View getHeaderView(int partition, Cursor cursor, View convertView,
-            ViewGroup parent) {
-        View view = convertView != null
-                ? convertView
-                : newHeaderView(mContext, partition, cursor, parent);
-        bindHeaderView(view, partition, cursor);
-        return view;
-    }
-
-    /**
-     * Creates the header view for the specified partition.
-     */
-    protected View newHeaderView(Context context, int partition, Cursor cursor,
-            ViewGroup parent) {
-        return null;
-    }
-
-    /**
-     * Binds the header view for the specified partition.
-     */
-    protected void bindHeaderView(View view, int partition, Cursor cursor) {
-    }
-
-    /**
-     * Returns an item view for the specified partition, creating one if needed.
-     */
-    protected View getView(int partition, Cursor cursor, int position, View convertView,
-            ViewGroup parent) {
-        View view;
-        if (convertView != null) {
-            view = convertView;
-        } else {
-            view = newView(mContext, partition, cursor, position, parent);
-        }
-        bindView(view, partition, cursor, position);
-        return view;
-    }
-
-    /**
-     * Creates an item view for the specified partition and position. Position
-     * corresponds directly to the current cursor position.
-     */
-    protected abstract View newView(Context context, int partition, Cursor cursor, int position,
-            ViewGroup parent);
-
-    /**
-     * Binds an item view for the specified partition and position. Position
-     * corresponds directly to the current cursor position.
-     */
-    protected abstract void bindView(View v, int partition, Cursor cursor, int position);
-
-    /**
-     * Returns a pre-positioned cursor for the specified list position.
-     */
-    public Object getItem(int position) {
-        ensureCacheValid();
-        int start = 0;
-        for (int i = 0; i < mSize; i++) {
-            int end = start + mPartitions[i].count;
-            if (position >= start && position < end) {
-                int offset = position - start;
-                if (mPartitions[i].hasHeader) {
-                    offset--;
-                }
-                if (offset == -1) {
-                    return null;
-                }
-                Cursor cursor = mPartitions[i].cursor;
-                cursor.moveToPosition(offset);
-                return cursor;
-            }
-            start = end;
-        }
-
-        return null;
-    }
-
-    /**
-     * Returns the item ID for the specified list position.
-     */
-    public long getItemId(int position) {
-        ensureCacheValid();
-        int start = 0;
-        for (int i = 0; i < mSize; i++) {
-            int end = start + mPartitions[i].count;
-            if (position >= start && position < end) {
-                int offset = position - start;
-                if (mPartitions[i].hasHeader) {
-                    offset--;
-                }
-                if (offset == -1) {
-                    return 0;
-                }
-                if (mPartitions[i].idColumnIndex == -1) {
-                    return 0;
-                }
-
-                Cursor cursor = mPartitions[i].cursor;
-                if (cursor == null || cursor.isClosed() || !cursor.moveToPosition(offset)) {
-                    return 0;
-                }
-                return cursor.getLong(mPartitions[i].idColumnIndex);
-            }
-            start = end;
-        }
-
-        return 0;
-    }
-
-    /**
-     * Returns false if any partition has a header.
-     */
-    @Override
-    public boolean areAllItemsEnabled() {
-        for (int i = 0; i < mSize; i++) {
-            if (mPartitions[i].hasHeader) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    /**
-     * Returns true for all items except headers.
-     */
-    @Override
-    public boolean isEnabled(int position) {
-        ensureCacheValid();
-        int start = 0;
-        for (int i = 0; i < mSize; i++) {
-            int end = start + mPartitions[i].count;
-            if (position >= start && position < end) {
-                int offset = position - start;
-                if (mPartitions[i].hasHeader && offset == 0) {
-                    return false;
-                } else {
-                    return isEnabled(i, offset);
-                }
-            }
-            start = end;
-        }
-
-        return false;
-    }
-
-    /**
-     * Returns true if the item at the specified offset of the specified
-     * partition is selectable and clickable.
-     */
-    protected boolean isEnabled(int partition, int position) {
-        return true;
-    }
-}
diff --git a/src/com/android/contacts/widget/PinnedHeaderListAdapter.java b/src/com/android/contacts/widget/PinnedHeaderListAdapter.java
index e39bce8..a4d375e 100644
--- a/src/com/android/contacts/widget/PinnedHeaderListAdapter.java
+++ b/src/com/android/contacts/widget/PinnedHeaderListAdapter.java
@@ -15,6 +15,8 @@
  */
 package com.android.contacts.widget;
 
+import com.android.common.widget.CompositeCursorAdapter;
+
 import android.content.Context;
 import android.view.View;
 import android.view.ViewGroup;
diff --git a/tests/src/com/android/contacts/widget/CompositeCursorAdapterTest.java b/tests/src/com/android/contacts/widget/CompositeCursorAdapterTest.java
deleted file mode 100644
index 813d2be..0000000
--- a/tests/src/com/android/contacts/widget/CompositeCursorAdapterTest.java
+++ /dev/null
@@ -1,251 +0,0 @@
-/*
- * Copyright (C) 2010 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.widget;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.database.MatrixCursor;
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * Tests for {@link CompositeCursorAdapter}.
- */
-@SmallTest
-public class CompositeCursorAdapterTest extends AndroidTestCase {
-
-    public class TestCompositeCursorAdapter extends CompositeCursorAdapter {
-
-        public TestCompositeCursorAdapter() {
-            super(CompositeCursorAdapterTest.this.getContext());
-        }
-
-        private StringBuilder mRequests = new StringBuilder();
-
-        @Override
-        protected View newHeaderView(Context context, int partition, Cursor cursor, ViewGroup parent) {
-            return new View(context);
-        }
-
-        @Override
-        protected void bindHeaderView(View view, int partition, Cursor cursor) {
-            mRequests.append(partition + (cursor == null ? "" : cursor.getColumnNames()[0])
-                    + "[H] ");
-        }
-
-        @Override
-        protected View newView(Context context, int sectionIndex, Cursor cursor, int position,
-                ViewGroup parent) {
-            return new View(context);
-        }
-
-        @Override
-        protected void bindView(View v, int partition, Cursor cursor, int position) {
-            if (!cursor.moveToPosition(position)) {
-                fail("Invalid position:" + partition + " " + cursor.getColumnNames()[0] + " "
-                        + position);
-            }
-
-            mRequests.append(partition + cursor.getColumnNames()[0] + "["
-                    + cursor.getInt(0) + "] ");
-        }
-
-        @Override
-        public String toString() {
-            return mRequests.toString().trim();
-        }
-    }
-
-    public void testGetCountNoEmptySections() {
-        TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
-        adapter.addPartition(false, false);
-        adapter.addPartition(false, false);
-
-        adapter.changeCursor(0, makeCursor("a", 2));
-        adapter.changeCursor(1, makeCursor("b", 3));
-
-        assertEquals(5, adapter.getCount());
-    }
-
-    public void testGetViewNoEmptySections() {
-        TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
-        adapter.addPartition(false, false);
-        adapter.addPartition(false, false);
-
-        adapter.changeCursor(0, makeCursor("a", 1));
-        adapter.changeCursor(1, makeCursor("b", 2));
-
-        for (int i = 0; i < adapter.getCount(); i++) {
-            adapter.getView(i, null, null);
-        }
-
-        assertEquals("0a[0] 1b[0] 1b[1]", adapter.toString());
-    }
-
-    public void testGetCountWithHeadersAndNoEmptySections() {
-        TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
-        adapter.addPartition(false, true);
-        adapter.addPartition(false, true);
-
-        adapter.changeCursor(0, makeCursor("a", 2));
-        adapter.changeCursor(1, makeCursor("b", 3));
-
-        assertEquals(7, adapter.getCount());
-    }
-
-    public void testGetViewWithHeadersNoEmptySections() {
-        TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
-        adapter.addPartition(false, true);
-        adapter.addPartition(false, true);
-
-        adapter.changeCursor(0, makeCursor("a", 1));
-        adapter.changeCursor(1, makeCursor("b", 2));
-
-        for (int i = 0; i < adapter.getCount(); i++) {
-            adapter.getView(i, null, null);
-        }
-
-        assertEquals("0a[H] 0a[0] 1b[H] 1b[0] 1b[1]", adapter.toString());
-    }
-
-    public void testGetCountWithHiddenEmptySection() {
-        TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
-        adapter.addPartition(false, true);
-        adapter.addPartition(false, true);
-
-        adapter.changeCursor(1, makeCursor("a", 2));
-
-        assertEquals(3, adapter.getCount());
-    }
-
-    public void testGetPartitionForPosition() {
-        TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
-        adapter.addPartition(true, false);
-        adapter.addPartition(true, true);
-
-        adapter.changeCursor(0, makeCursor("a", 1));
-        adapter.changeCursor(1, makeCursor("b", 2));
-
-        assertEquals(0, adapter.getPartitionForPosition(0));
-        assertEquals(1, adapter.getPartitionForPosition(1));
-        assertEquals(1, adapter.getPartitionForPosition(2));
-        assertEquals(1, adapter.getPartitionForPosition(3));
-    }
-
-    public void testGetOffsetForPosition() {
-        TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
-        adapter.addPartition(true, false);
-        adapter.addPartition(true, true);
-
-        adapter.changeCursor(0, makeCursor("a", 1));
-        adapter.changeCursor(1, makeCursor("b", 2));
-
-        assertEquals(0, adapter.getOffsetInPartition(0));
-        assertEquals(-1, adapter.getOffsetInPartition(1));
-        assertEquals(0, adapter.getOffsetInPartition(2));
-        assertEquals(1, adapter.getOffsetInPartition(3));
-    }
-
-    public void testGetPositionForPartition() {
-        TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
-        adapter.addPartition(true, true);
-        adapter.addPartition(true, true);
-
-        adapter.changeCursor(0, makeCursor("a", 1));
-        adapter.changeCursor(1, makeCursor("b", 2));
-
-        assertEquals(0, adapter.getPositionForPartition(0));
-        assertEquals(2, adapter.getPositionForPartition(1));
-    }
-
-    public void testGetViewWithHiddenEmptySections() {
-        TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
-        adapter.addPartition(false, false);
-        adapter.addPartition(false, false);
-
-        adapter.changeCursor(1, makeCursor("b", 2));
-
-        for (int i = 0; i < adapter.getCount(); i++) {
-            adapter.getView(i, null, null);
-        }
-
-        assertEquals("1b[0] 1b[1]", adapter.toString());
-    }
-
-    public void testGetCountWithShownEmptySection() {
-        TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
-        adapter.addPartition(true, true);
-        adapter.addPartition(true, true);
-
-        adapter.changeCursor(1, makeCursor("a", 2));
-
-        assertEquals(4, adapter.getCount());
-    }
-
-    public void testGetViewWithShownEmptySections() {
-        TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
-        adapter.addPartition(true, true);
-        adapter.addPartition(true, true);
-
-        adapter.changeCursor(1, makeCursor("b", 2));
-
-        for (int i = 0; i < adapter.getCount(); i++) {
-            adapter.getView(i, null, null);
-        }
-
-        assertEquals("0[H] 1b[H] 1b[0] 1b[1]", adapter.toString());
-    }
-
-    public void testAreAllItemsEnabledFalse() {
-        TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
-        adapter.addPartition(true, false);
-        adapter.addPartition(true, true);
-
-        assertFalse(adapter.areAllItemsEnabled());
-    }
-
-    public void testAreAllItemsEnabledTrue() {
-        TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
-        adapter.addPartition(true, false);
-        adapter.addPartition(true, false);
-
-        assertTrue(adapter.areAllItemsEnabled());
-    }
-
-    public void testIsEnabled() {
-        TestCompositeCursorAdapter adapter = new TestCompositeCursorAdapter();
-        adapter.addPartition(true, false);
-        adapter.addPartition(true, true);
-
-        adapter.changeCursor(0, makeCursor("a", 1));
-        adapter.changeCursor(1, makeCursor("b", 2));
-
-        assertTrue(adapter.isEnabled(0));
-        assertFalse(adapter.isEnabled(1));
-        assertTrue(adapter.isEnabled(2));
-        assertTrue(adapter.isEnabled(3));
-    }
-
-    private Cursor makeCursor(String name, int count) {
-        MatrixCursor cursor = new MatrixCursor(new String[]{name});
-        for (int i = 0; i < count; i++) {
-            cursor.addRow(new Object[]{i});
-        }
-        return cursor;
-    }
-}