Use AccountManager for details, handle INSERT cases.

Connected Sources to use AccountManager and inflate details
through registered sync adapters.  Each ContactsSource now
has a "level" of inflation, since deeper levels aren't
always needed right away.  Several places we're making
blocking calls into other processes that are tied to the UI
thread.  (This would take a large effort to fix.)

Turned most background Edit activity operations into
WeakAsyncTask, which helps finish background tasks while
preventing leaked Contexts.  This allows us to hold the UI
thread while saving, but release it just before ANR,
allowing the background operation to complete.

Enabled INSERT case, both from overall list and when already
editing an aggregate.  Finally, cleaned up the manifest
intent-filters to directly match authorities.
diff --git a/src/com/android/contacts/AsyncQueryHandler.java b/src/com/android/contacts/AsyncQueryHandler.java
deleted file mode 100644
index fa63ad4..0000000
--- a/src/com/android/contacts/AsyncQueryHandler.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.android.contacts;
-
-import android.content.Context;
-import android.database.Cursor;
-
-import java.lang.ref.WeakReference;
-
-/**
- * Slightly more abstract {@link android.content.AsyncQueryHandler} that helps
- * keep a {@link WeakReference} back to a callback interface. Will properly
- * close the completed query if the listener ceases to exist.
- * <p>
- * Using this pattern will help keep you from leaking a {@link Context}.
- */
-public class AsyncQueryHandler extends android.content.AsyncQueryHandler {
-    private final WeakReference<QueryCompleteListener> mListener;
-
-    /**
-     * Interface to listen for completed queries.
-     */
-    public static interface QueryCompleteListener {
-        public void onQueryComplete(int token, Object cookie, Cursor cursor);
-    }
-
-    public AsyncQueryHandler(Context context, QueryCompleteListener listener) {
-        super(context.getContentResolver());
-        mListener = new WeakReference<QueryCompleteListener>(listener);
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
-        final QueryCompleteListener listener = mListener.get();
-        if (listener != null) {
-            listener.onQueryComplete(token, cookie, cursor);
-        } else {
-            cursor.close();
-        }
-    }
-}
diff --git a/src/com/android/contacts/BaseContactCardActivity.java b/src/com/android/contacts/BaseContactCardActivity.java
index b4980f6..58ea990 100644
--- a/src/com/android/contacts/BaseContactCardActivity.java
+++ b/src/com/android/contacts/BaseContactCardActivity.java
@@ -18,9 +18,9 @@
 
 import java.util.ArrayList;
 
-import com.android.contacts.NotifyingAsyncQueryHandler.AsyncQueryListener;
 import com.android.contacts.ScrollingTabWidget.OnTabSelectionChangedListener;
 import com.android.contacts.model.ContactsSource;
+import com.android.contacts.util.NotifyingAsyncQueryHandler;
 import com.android.contacts.model.Sources;
 import com.android.internal.widget.ContactHeaderWidget;
 
@@ -49,9 +49,8 @@
 /**
  * The base Activity class for viewing and editing a contact.
  */
-public abstract class BaseContactCardActivity extends Activity
-        implements AsyncQueryListener, OnTabSelectionChangedListener,
-        Sources.SourcesCompleteListener {
+public abstract class BaseContactCardActivity extends Activity implements
+        NotifyingAsyncQueryHandler.AsyncQueryListener, OnTabSelectionChangedListener {
 
     private static final String TAG = "BaseContactCardActivity";
 
@@ -75,8 +74,6 @@
 
     private static final int TOKEN_TABS = 0;
 
-    private Sources mSources;
-
     @Override
     protected void onCreate(Bundle icicle) {
         super.onCreate(icicle);
@@ -100,12 +97,7 @@
 
         mHandler = new NotifyingAsyncQueryHandler(this, this);
 
-        Sources.requestInstance(this, this);
-
-    }
-
-    public void onSourcesComplete(Sources sources) {
-        mSources = sources;
+        // TODO: turn this into async call instead of blocking ui
         asyncSetupTabs();
     }
 
@@ -165,12 +157,17 @@
      * associated with the contact being displayed.
      */
     protected void bindTabs(ArrayList<Entity> entities) {
+        final Sources sources = Sources.getInstance(this);
+
         for (Entity entity : entities) {
             final String accountType = entity.getEntityValues().
                     getAsString(RawContacts.ACCOUNT_TYPE);
             final Long rawContactId = entity.getEntityValues().
                     getAsLong(RawContacts._ID);
-            ContactsSource source = mSources.getSourceForType(accountType);
+
+            // TODO: ensure inflation on background task so we don't block UI thread here
+            final ContactsSource source = sources.getInflatedSource(accountType,
+                    ContactsSource.LEVEL_SUMMARY);
             addTab(rawContactId, createTabIndicatorView(mTabWidget, source));
         }
         mTabWidget.setCurrentTab(0);
@@ -271,5 +268,4 @@
         }
         return createTabIndicatorView(parent, null, icon);
     }
-
 }
diff --git a/src/com/android/contacts/ContactsListActivity.java b/src/com/android/contacts/ContactsListActivity.java
index 39c7468..4ec6df6 100644
--- a/src/com/android/contacts/ContactsListActivity.java
+++ b/src/com/android/contacts/ContactsListActivity.java
@@ -655,18 +655,16 @@
             return false;
         }
 
+        // TODO: move this into a resource-based menu
+
         // Search
         menu.add(0, MENU_SEARCH, 0, R.string.menu_search)
                 .setIcon(android.R.drawable.ic_menu_search);
 
         // New contact
-        //TODO Hook this up to new create contact activity.
-        /*
-        menu.add(0, MENU_NEW_CONTACT, 0, R.string.menu_newContact)
-                .setIcon(android.R.drawable.ic_menu_add)
-                .setIntent(new Intent(Intents.Insert.ACTION, People.CONTENT_URI))
-                .setAlphabeticShortcut('n');
-                */
+        menu.add(0, MENU_NEW_CONTACT, 0, R.string.menu_newContact).setIcon(
+                android.R.drawable.ic_menu_add).setIntent(
+                new Intent(Intents.Insert.ACTION, Contacts.CONTENT_URI)).setAlphabeticShortcut('n');
 
         // Display group
         if (mMode == MODE_DEFAULT) {
diff --git a/src/com/android/contacts/ContactsUtils.java b/src/com/android/contacts/ContactsUtils.java
index 9bc8f74..d2a01ca 100644
--- a/src/com/android/contacts/ContactsUtils.java
+++ b/src/com/android/contacts/ContactsUtils.java
@@ -28,6 +28,7 @@
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
+import android.content.Intent;
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
@@ -218,4 +219,15 @@
         return null;
     }
 
+    public static Intent getPhotoPickIntent() {
+        Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
+        intent.setType("image/*");
+        intent.putExtra("crop", "true");
+        intent.putExtra("aspectX", 1);
+        intent.putExtra("aspectY", 1);
+        intent.putExtra("outputX", 96);
+        intent.putExtra("outputY", 96);
+        intent.putExtra("return-data", true);
+        return intent;
+    }
 }
diff --git a/src/com/android/contacts/DisplayGroupsActivity.java b/src/com/android/contacts/DisplayGroupsActivity.java
index 6da2531..7215966 100644
--- a/src/com/android/contacts/DisplayGroupsActivity.java
+++ b/src/com/android/contacts/DisplayGroupsActivity.java
@@ -16,7 +16,7 @@
 
 package com.android.contacts;
 
-import com.android.contacts.NotifyingAsyncQueryHandler.AsyncQueryListener;
+import com.android.contacts.util.NotifyingAsyncQueryHandler;
 
 import android.app.ExpandableListActivity;
 import android.content.ContentResolver;
@@ -60,7 +60,7 @@
  * select which ones they want to be visible.
  */
 public final class DisplayGroupsActivity extends ExpandableListActivity implements
-        AsyncQueryListener, OnItemClickListener {
+        NotifyingAsyncQueryHandler.AsyncQueryListener, OnItemClickListener {
     private static final String TAG = "DisplayGroupsActivity";
 
     public interface Prefs {
@@ -170,6 +170,7 @@
                 Projections.PROJ_SUMMARY, null, null, Projections.SORT_ORDER);
     }
 
+    /** {@inheritDoc} */
     public void onQueryComplete(int token, Object cookie, Cursor cursor) {
         mAdapter.changeCursor(cursor);
 
@@ -180,6 +181,11 @@
         }
     }
 
+    /** {@inheritDoc} */
+    public void onQueryEntitiesComplete(int token, Object cookie, EntityIterator iterator) {
+        // No actions
+    }
+
     /**
      * Handle any clicks on header views added to our {@link #mAdapter}, which
      * are usually the global modifier checkboxes.
@@ -631,9 +637,4 @@
         public static final int COL_SUMMARY_WITH_PHONES = 6;
 
     }
-
-    public void onQueryEntitiesComplete(int token, Object cookie, EntityIterator iterator) {
-        // Emtpy
-    }
-
 }
diff --git a/src/com/android/contacts/ScrollingTabWidget.java b/src/com/android/contacts/ScrollingTabWidget.java
index 982d661..2f703aa 100644
--- a/src/com/android/contacts/ScrollingTabWidget.java
+++ b/src/com/android/contacts/ScrollingTabWidget.java
@@ -201,6 +201,13 @@
     }
 
     /**
+     * Return index of the currently selected tab.
+     */
+    public int getCurrentTab() {
+        return mSelectedTab;
+    }
+
+    /**
      * Sets the current tab and focuses the UI on it.
      * This method makes sure that the focused tab matches the selected
      * tab, normally at {@link #setCurrentTab}.  Normally this would not
@@ -215,6 +222,10 @@
      *  @see #setCurrentTab
      */
     public void focusCurrentTab(int index) {
+        if (index < 0 || index >= getTabCount()) {
+            return;
+        }
+
         setCurrentTab(index);
         getChildTabViewAt(index).requestFocus();
 
diff --git a/src/com/android/contacts/ShowOrCreateActivity.java b/src/com/android/contacts/ShowOrCreateActivity.java
index 6ba2315..27fce18 100755
--- a/src/com/android/contacts/ShowOrCreateActivity.java
+++ b/src/com/android/contacts/ShowOrCreateActivity.java
@@ -16,14 +16,13 @@
 
 package com.android.contacts;
 
-import com.android.contacts.NotifyingAsyncQueryHandler.AsyncQueryListener;
 import com.android.contacts.ui.FastTrackWindow;
+import com.android.contacts.util.NotifyingAsyncQueryHandler;
 
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.content.ComponentName;
 import android.content.ContentUris;
-import android.content.Context;
 import android.content.DialogInterface;
 import android.content.EntityIterator;
 import android.content.Intent;
@@ -31,12 +30,10 @@
 import android.graphics.Rect;
 import android.net.Uri;
 import android.os.Bundle;
-import android.os.IBinder;
-import android.provider.ContactsContract.Intents;
 import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.Intents;
 import android.provider.ContactsContract.PhoneLookup;
-import android.view.View;
+import android.provider.ContactsContract.RawContacts;
 
 /**
  * Handle several edge cases around showing or possibly creating contacts in
@@ -54,8 +51,8 @@
  * {@link Intent#ACTION_SEARCH}.
  * </ul>
  */
-public final class ShowOrCreateActivity extends Activity implements AsyncQueryListener,
-        FastTrackWindow.OnDismissListener {
+public final class ShowOrCreateActivity extends Activity implements
+        NotifyingAsyncQueryHandler.AsyncQueryListener, FastTrackWindow.OnDismissListener {
     static final String TAG = "ShowOrCreateActivity";
     static final boolean LOGD = false;
 
@@ -180,6 +177,7 @@
         finish();
     }
 
+    /** {@inheritDoc} */
     public void onQueryComplete(int token, Object cookie, Cursor cursor) {
         if (cursor == null) {
             return;
@@ -243,6 +241,11 @@
         }
     }
 
+    /** {@inheritDoc} */
+    public void onQueryEntitiesComplete(int token, Object cookie, EntityIterator iterator) {
+        // No actions
+    }
+
     /**
      * Listener for {@link DialogInterface} that launches a given {@link Intent}
      * when clicked. When clicked, this also closes the parent using
@@ -268,8 +271,4 @@
             mParent.finish();
         }
     }
-
-    public void onQueryEntitiesComplete(int token, Object cookie, EntityIterator iterator) {
-        // Empty
-    }
 }
diff --git a/src/com/android/contacts/model/ContactsSource.java b/src/com/android/contacts/model/ContactsSource.java
index e0e0c7c..8611bb3 100644
--- a/src/com/android/contacts/model/ContactsSource.java
+++ b/src/com/android/contacts/model/ContactsSource.java
@@ -16,9 +16,15 @@
 
 package com.android.contacts.model;
 
+import org.xmlpull.v1.XmlPullParser;
+
 import android.accounts.Account;
 import android.content.ContentValues;
 import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.XmlResourceParser;
 import android.database.Cursor;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
@@ -76,7 +82,7 @@
     /**
      * The {@link RawContacts#ACCOUNT_TYPE} these constraints apply to.
      */
-    public String accountType;
+    public String accountType = null;
 
     /**
      * Package that resources should be loaded from, either defined through an
@@ -87,11 +93,71 @@
     public int titleRes;
     public int iconRes;
 
+    public boolean readOnly;
+
     /**
      * Set of {@link DataKind} supported by this source.
      */
     private ArrayList<DataKind> mKinds = new ArrayList<DataKind>();
 
+    private static final String ACTION_SYNC_ADAPTER = "android.content.SyncAdapter";
+    private static final String METADATA_CONTACTS = "android.provider.CONTACTS_STRUCTURE";
+
+    public static final int LEVEL_SUMMARY = 1;
+    public static final int LEVEL_MIMETYPES = 2;
+    public static final int LEVEL_CONSTRAINTS = 3;
+
+    private int mInflatedLevel = -1;
+
+    public synchronized boolean isInflated(int inflateLevel) {
+        return mInflatedLevel >= inflateLevel;
+    }
+
+    /**
+     * Ensure that the constraint rules behind this {@link ContactsSource} have
+     * been inflated. Because this may involve parsing meta-data from
+     * {@link PackageManager}, it shouldn't be called from a UI thread.
+     */
+    public synchronized void ensureInflated(Context context, int inflateLevel) {
+        if (isInflated(inflateLevel)) return;
+        // TODO: handle inflating at multiple levels of parsing
+        mInflatedLevel = inflateLevel;
+        mKinds.clear();
+
+        // Handle some well-known sources with hard-coded constraints
+        // TODO: move these into adapter-specific XML once schema finalized
+        if (HardCodedSources.ACCOUNT_TYPE_GOOGLE.equals(accountType)) {
+            HardCodedSources.buildGoogle(this);
+            return;
+        } else if(HardCodedSources.ACCOUNT_TYPE_EXCHANGE.equals(accountType)) {
+            HardCodedSources.buildExchange(this);
+            return;
+        } else if(HardCodedSources.ACCOUNT_TYPE_FACEBOOK.equals(accountType)) {
+            HardCodedSources.buildFacebook(this);
+            return;
+        }
+
+        // Handle unknown sources by searching their package
+        final PackageManager pm = context.getPackageManager();
+        final Intent syncAdapter = new Intent(ACTION_SYNC_ADAPTER);
+        final List<ResolveInfo> matches = pm.queryIntentServices(syncAdapter,
+                PackageManager.GET_META_DATA);
+        for (ResolveInfo info : matches) {
+            final XmlResourceParser parser = info.activityInfo.loadXmlMetaData(pm,
+                    METADATA_CONTACTS);
+            inflate(parser);
+        }
+    }
+
+    /**
+     * Inflate this {@link ContactsSource} from the given parser. This may only
+     * load details matching the publicly-defined schema.
+     */
+    protected void inflate(XmlPullParser parser) {
+        // TODO: implement basic functionality for third-party integration
+        throw new UnsupportedOperationException("Custom constraint parser not implemented");
+    }
+
     /**
      * {@link Comparator} to sort by {@link DataKind#weight}.
      */
diff --git a/src/com/android/contacts/model/EntityDelta.java b/src/com/android/contacts/model/EntityDelta.java
index 2867662..116300f 100644
--- a/src/com/android/contacts/model/EntityDelta.java
+++ b/src/com/android/contacts/model/EntityDelta.java
@@ -228,6 +228,18 @@
         return false;
     }
 
+    /**
+     * Mark this entire object deleted, including any {@link ValuesDelta}.
+     */
+    public void markDeleted() {
+        this.mValues.markDeleted();
+        for (ArrayList<ValuesDelta> mimeEntries : mEntries.values()) {
+            for (ValuesDelta child : mimeEntries) {
+                child.markDeleted();
+            }
+        }
+    }
+
     @Override
     public String toString() {
         final StringBuilder builder = new StringBuilder();
@@ -288,7 +300,7 @@
                         // Inserting under existing, so fill with known _id
                         builder.withValue(Data.RAW_CONTACT_ID, beforeId);
                     }
-                } else if (isContactInsert) {
+                } else if (isContactInsert && builder != null) {
                     // Child must be insert when Contact insert
                     throw new IllegalArgumentException("When parent insert, child must be also");
                 }
diff --git a/src/com/android/contacts/model/EntityModifier.java b/src/com/android/contacts/model/EntityModifier.java
index ed37736..29f4d1c 100644
--- a/src/com/android/contacts/model/EntityModifier.java
+++ b/src/com/android/contacts/model/EntityModifier.java
@@ -297,11 +297,7 @@
      * Parse the given {@link Bundle} into the given {@link EntityDelta} state,
      * assuming the extras defined through {@link Intents}.
      */
-    public static void parseExtras(Context context, EntityDelta state, Bundle extras) {
-        final String accountType = state.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
-        final ContactsSource source = Sources.getPartialInstance(context).
-                getSourceForType(accountType);
-
+    public static void parseExtras(Context context, ContactsSource source, EntityDelta state, Bundle extras) {
         {
             // StructuredName
             final DataKind kind = source.getKindForMimetype(StructuredName.CONTENT_ITEM_TYPE);
diff --git a/src/com/android/contacts/model/HardCodedSources.java b/src/com/android/contacts/model/HardCodedSources.java
new file mode 100644
index 0000000..8e7aadc
--- /dev/null
+++ b/src/com/android/contacts/model/HardCodedSources.java
@@ -0,0 +1,542 @@
+/*
+ * 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.android.contacts.R;
+import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.model.ContactsSource.EditField;
+import com.android.contacts.model.ContactsSource.EditType;
+import com.android.contacts.model.ContactsSource.StringInflater;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.view.inputmethod.EditorInfo;
+
+import java.util.ArrayList;
+
+/**
+ * Hard-coded definition of some {@link ContactsSource} constraints, since the
+ * XML language hasn't been finalized.
+ */
+public class HardCodedSources {
+    // TODO: finish hard-coding all constraints
+
+    public static final String ACCOUNT_TYPE_GOOGLE = "com.google.GAIA";
+    public static final String ACCOUNT_TYPE_EXCHANGE = "com.android.exchange";
+    public static final String ACCOUNT_TYPE_FACEBOOK = "com.facebook.auth.login";
+
+    private static final int FLAGS_PHONE = EditorInfo.TYPE_CLASS_PHONE;
+    private static final int FLAGS_EMAIL = EditorInfo.TYPE_CLASS_TEXT
+            | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
+    private static final int FLAGS_PERSON_NAME = EditorInfo.TYPE_CLASS_TEXT
+            | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS | EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME;
+    private static final int FLAGS_PHONETIC = EditorInfo.TYPE_CLASS_TEXT
+            | EditorInfo.TYPE_TEXT_VARIATION_PHONETIC;
+    private static final int FLAGS_GENERIC_NAME = EditorInfo.TYPE_CLASS_TEXT
+            | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
+    private static final int FLAGS_NOTE = EditorInfo.TYPE_CLASS_TEXT
+            | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
+    private static final int FLAGS_WEBSITE = EditorInfo.TYPE_CLASS_TEXT
+            | EditorInfo.TYPE_TEXT_VARIATION_URI;
+    private static final int FLAGS_POSTAL = EditorInfo.TYPE_CLASS_TEXT
+            | EditorInfo.TYPE_TEXT_VARIATION_POSTAL_ADDRESS | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS
+            | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
+
+    private HardCodedSources() {
+        // Static utility class
+    }
+
+    /**
+     * Hard-coded instance of {@link ContactsSource} for Google Contacts.
+     */
+    static void buildGoogle(ContactsSource list) {
+        {
+            // GOOGLE: STRUCTUREDNAME
+            DataKind kind = new DataKind(StructuredName.CONTENT_ITEM_TYPE,
+                    R.string.nameLabelsGroup, -1, -1, true);
+            list.add(kind);
+        }
+
+        {
+            // GOOGLE: PHOTO
+            DataKind kind = new DataKind(Photo.CONTENT_ITEM_TYPE, -1, -1, -1, true);
+            list.add(kind);
+        }
+
+        {
+            // GOOGLE: PHONE
+            DataKind kind = new DataKind(Phone.CONTENT_ITEM_TYPE,
+                    R.string.phoneLabelsGroup, android.R.drawable.sym_action_call, 10, true);
+            kind.iconAltRes = R.drawable.sym_action_sms;
+
+            kind.actionHeader = new ActionInflater(list.resPackageName, kind);
+            kind.actionAltHeader = new ActionAltInflater(list.resPackageName, kind);
+            kind.actionBody = new SimpleInflater(Phone.NUMBER);
+
+            kind.typeColumn = Phone.TYPE;
+            kind.typeList = new ArrayList<EditType>();
+            kind.typeList.add(new EditType(Phone.TYPE_HOME, R.string.type_home, R.string.call_home,
+                    R.string.sms_home));
+            kind.typeList.add(new EditType(Phone.TYPE_MOBILE, R.string.type_mobile,
+                    R.string.call_mobile, R.string.sms_mobile));
+            kind.typeList.add(new EditType(Phone.TYPE_WORK, R.string.type_work, R.string.call_work,
+                    R.string.sms_work));
+            kind.typeList.add(new EditType(Phone.TYPE_FAX_WORK, R.string.type_fax_work,
+                    R.string.call_fax_work, R.string.sms_fax_work).setSecondary(true));
+            kind.typeList.add(new EditType(Phone.TYPE_FAX_HOME, R.string.type_fax_home,
+                    R.string.call_fax_home, R.string.sms_fax_home).setSecondary(true));
+            kind.typeList.add(new EditType(Phone.TYPE_PAGER, R.string.type_pager,
+                    R.string.call_pager, R.string.sms_pager).setSecondary(true));
+            kind.typeList.add(new EditType(Phone.TYPE_OTHER, R.string.type_other,
+                    R.string.call_other, R.string.sms_other));
+            kind.typeList.add(new EditType(Phone.TYPE_CUSTOM, R.string.type_custom,
+                    R.string.call_custom, R.string.sms_custom).setSecondary(true).setCustomColumn(
+                    Phone.LABEL));
+
+            kind.fieldList = new ArrayList<EditField>();
+            kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+
+            list.add(kind);
+        }
+
+        {
+            // GOOGLE: EMAIL
+            DataKind kind = new DataKind(Email.CONTENT_ITEM_TYPE,
+                    R.string.emailLabelsGroup, android.R.drawable.sym_action_email, 15, true);
+
+            kind.actionHeader = new ActionInflater(list.resPackageName, kind);
+            kind.actionBody = new SimpleInflater(Email.DATA);
+
+            kind.typeColumn = Email.TYPE;
+            kind.typeList = new ArrayList<EditType>();
+            kind.typeList
+                    .add(new EditType(Email.TYPE_HOME, R.string.type_home, R.string.email_home));
+            kind.typeList
+                    .add(new EditType(Email.TYPE_WORK, R.string.type_work, R.string.email_work));
+            kind.typeList.add(new EditType(Email.TYPE_OTHER, R.string.type_other,
+                    R.string.email_other));
+            kind.typeList.add(new EditType(Email.TYPE_CUSTOM, R.string.type_custom,
+                    R.string.email_home).setSecondary(true).setCustomColumn(Email.LABEL));
+
+            kind.fieldList = new ArrayList<EditField>();
+            kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+
+            list.add(kind);
+        }
+
+        {
+            // GOOGLE: IM
+            DataKind kind = new DataKind(Im.CONTENT_ITEM_TYPE, R.string.imLabelsGroup,
+                    android.R.drawable.sym_action_chat, 20, true);
+
+            kind.actionHeader = new ActionInflater(list.resPackageName, kind);
+            kind.actionBody = new SimpleInflater(Im.DATA);
+
+            // 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 = new ArrayList<EditType>();
+            kind.typeList.add(new EditType(Im.PROTOCOL_AIM, R.string.type_im_aim));
+            kind.typeList.add(new EditType(Im.PROTOCOL_MSN, R.string.type_im_msn));
+            kind.typeList.add(new EditType(Im.PROTOCOL_YAHOO, R.string.type_im_yahoo));
+            kind.typeList.add(new EditType(Im.PROTOCOL_SKYPE, R.string.type_im_skype));
+            kind.typeList.add(new EditType(Im.PROTOCOL_QQ, R.string.type_im_qq));
+            kind.typeList.add(new EditType(Im.PROTOCOL_GOOGLE_TALK, R.string.type_im_google_talk));
+            kind.typeList.add(new EditType(Im.PROTOCOL_ICQ, R.string.type_im_icq));
+            kind.typeList.add(new EditType(Im.PROTOCOL_JABBER, R.string.type_im_jabber));
+            kind.typeList.add(new EditType(Im.PROTOCOL_CUSTOM, R.string.type_custom).setSecondary(
+                    true).setCustomColumn(Im.CUSTOM_PROTOCOL));
+
+            kind.fieldList = new ArrayList<EditField>();
+            kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
+
+            list.add(kind);
+        }
+
+        {
+            // GOOGLE: POSTAL
+            DataKind kind = new DataKind(StructuredPostal.CONTENT_ITEM_TYPE,
+                    R.string.postalLabelsGroup, R.drawable.sym_action_map, 25, true);
+
+            kind.actionHeader = new ActionInflater(list.resPackageName, kind);
+            // TODO: build body from various structured fields
+            kind.actionBody = new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS);
+
+            kind.typeColumn = StructuredPostal.TYPE;
+            kind.typeList = new ArrayList<EditType>();
+            kind.typeList.add(new EditType(StructuredPostal.TYPE_HOME, R.string.type_home,
+                    R.string.map_home));
+            kind.typeList.add(new EditType(StructuredPostal.TYPE_WORK, R.string.type_work,
+                    R.string.map_work));
+            kind.typeList.add(new EditType(StructuredPostal.TYPE_OTHER, R.string.type_other,
+                    R.string.map_other));
+            kind.typeList
+                    .add(new EditType(StructuredPostal.TYPE_CUSTOM, R.string.type_custom,
+                            R.string.map_custom).setSecondary(true).setCustomColumn(
+                            StructuredPostal.LABEL));
+
+            kind.fieldList = new ArrayList<EditField>();
+            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, true));
+            kind.fieldList.add(new EditField(StructuredPostal.NEIGHBORHOOD,
+                    R.string.postal_neighborhood, FLAGS_POSTAL, 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, true));
+
+            list.add(kind);
+        }
+
+        {
+            // GOOGLE: ORGANIZATION
+            DataKind kind = new DataKind(Organization.CONTENT_ITEM_TYPE,
+                    R.string.organizationLabelsGroup, R.drawable.sym_action_organization, 30, true);
+
+            kind.actionHeader = new SimpleInflater(list.resPackageName, R.string.organizationLabelsGroup);
+            // TODO: build body from multiple fields
+            kind.actionBody = new SimpleInflater(Organization.TITLE);
+
+            kind.typeColumn = Organization.TYPE;
+            kind.typeList = new ArrayList<EditType>();
+            kind.typeList.add(new EditType(Organization.TYPE_WORK, R.string.type_work));
+            kind.typeList.add(new EditType(Organization.TYPE_OTHER, R.string.type_other));
+            kind.typeList.add(new EditType(Organization.TYPE_CUSTOM, R.string.type_custom)
+                    .setSecondary(true).setCustomColumn(Organization.LABEL));
+
+            kind.fieldList = new ArrayList<EditField>();
+            kind.fieldList.add(new EditField(Organization.COMPANY, R.string.ghostData_company,
+                    FLAGS_GENERIC_NAME));
+            kind.fieldList.add(new EditField(Organization.TITLE, R.string.ghostData_title,
+                    FLAGS_GENERIC_NAME));
+
+            list.add(kind);
+        }
+
+        {
+            // GOOGLE: NOTE
+            DataKind kind = new DataKind(Note.CONTENT_ITEM_TYPE,
+                    R.string.label_notes, R.drawable.sym_note, 110, true);
+            kind.secondary = true;
+
+            kind.actionHeader = new SimpleInflater(list.resPackageName, R.string.label_notes);
+            kind.actionBody = new SimpleInflater(Note.NOTE);
+
+            kind.fieldList = new ArrayList<EditField>();
+            kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE));
+
+            list.add(kind);
+        }
+
+        {
+            // GOOGLE: NICKNAME
+            DataKind kind = new DataKind(Nickname.CONTENT_ITEM_TYPE,
+                    R.string.nicknameLabelsGroup, -1, 115, true);
+            kind.secondary = true;
+
+            kind.actionHeader = new SimpleInflater(list.resPackageName, R.string.nicknameLabelsGroup);
+            kind.actionBody = new SimpleInflater(Nickname.NAME);
+
+            kind.fieldList = new ArrayList<EditField>();
+            kind.fieldList.add(new EditField(Nickname.NAME, R.string.nicknameLabelsGroup,
+                    FLAGS_PERSON_NAME));
+
+            list.add(kind);
+        }
+
+        // TODO: GOOGLE: GROUPMEMBERSHIP
+        // TODO: GOOGLE: WEBSITE
+    }
+
+    /**
+     * 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;
+    private static final int TYPE_COMPANY_MAIN = 29;
+    private static final int TYPE_MMS = 30;
+    private static final int TYPE_RADIO = 31;
+
+    /**
+     * Hard-coded instance of {@link ContactsSource} for Exchange.
+     */
+    static void buildExchange(ContactsSource list) {
+        {
+            // EXCHANGE: STRUCTUREDNAME
+            DataKind kind = new DataKind(StructuredName.CONTENT_ITEM_TYPE,
+                    R.string.nameLabelsGroup, -1, -1, true);
+            kind.typeOverallMax = 1;
+            list.add(kind);
+        }
+
+        {
+            // EXCHANGE: PHOTO
+            DataKind kind = new DataKind(Photo.CONTENT_ITEM_TYPE, -1, -1, -1, true);
+            kind.typeOverallMax = 1;
+            list.add(kind);
+        }
+
+        {
+            // EXCHANGE: PHONE
+            DataKind kind = new DataKind(Phone.CONTENT_ITEM_TYPE,
+                    R.string.phoneLabelsGroup, android.R.drawable.sym_action_call, 10, true);
+            kind.iconAltRes = R.drawable.sym_action_sms;
+
+            kind.actionHeader = new ActionInflater(list.resPackageName, kind);
+            kind.actionAltHeader = new ActionAltInflater(list.resPackageName, kind);
+            kind.actionBody = new SimpleInflater(Phone.NUMBER);
+
+            kind.typeColumn = Phone.TYPE;
+            kind.typeList = new ArrayList<EditType>();
+            kind.typeList.add(new EditType(Phone.TYPE_HOME, R.string.type_home, R.string.call_home,
+                    R.string.sms_home).setSpecificMax(1));
+            kind.typeList.add(new EditType(TYPE_HOME2, R.string.type_home_2, R.string.call_home_2,
+                    R.string.sms_home_2).setSecondary(true).setSpecificMax(1));
+            kind.typeList.add(new EditType(Phone.TYPE_MOBILE, R.string.type_mobile,
+                    R.string.call_mobile, R.string.sms_mobile).setSpecificMax(1));
+            kind.typeList.add(new EditType(Phone.TYPE_WORK, R.string.type_work, R.string.call_work,
+                    R.string.sms_work).setSpecificMax(1));
+            kind.typeList.add(new EditType(TYPE_WORK2, R.string.type_work_2, R.string.call_work_2,
+                    R.string.sms_work_2).setSecondary(true).setSpecificMax(1));
+            kind.typeList.add(new EditType(Phone.TYPE_FAX_WORK, R.string.type_fax_work,
+                    R.string.call_fax_work, R.string.sms_fax_work).setSecondary(true)
+                    .setSpecificMax(1));
+            kind.typeList.add(new EditType(Phone.TYPE_FAX_HOME, R.string.type_fax_home,
+                    R.string.call_fax_home, R.string.sms_fax_home).setSecondary(true)
+                    .setSpecificMax(1));
+            kind.typeList.add(new EditType(Phone.TYPE_PAGER, R.string.type_pager,
+                    R.string.call_pager, R.string.sms_pager).setSecondary(true).setSpecificMax(1));
+            kind.typeList.add(new EditType(TYPE_CAR, R.string.type_car, R.string.call_car,
+                    R.string.sms_car).setSecondary(true).setSpecificMax(1));
+            kind.typeList.add(new EditType(TYPE_COMPANY_MAIN, R.string.type_company_main,
+                    R.string.call_company_main, R.string.sms_company_main).setSecondary(true)
+                    .setSpecificMax(1));
+            kind.typeList.add(new EditType(TYPE_MMS, R.string.type_mms, R.string.call_mms,
+                    R.string.sms_mms).setSecondary(true).setSpecificMax(1));
+            kind.typeList.add(new EditType(TYPE_RADIO, R.string.type_radio, R.string.call_radio,
+                    R.string.sms_radio).setSecondary(true).setSpecificMax(1));
+            kind.typeList.add(new EditType(Phone.TYPE_CUSTOM, R.string.type_assistant,
+                    R.string.call_custom, R.string.sms_custom).setSecondary(true).setSpecificMax(1)
+                    .setCustomColumn(Phone.LABEL));
+
+            kind.fieldList = new ArrayList<EditField>();
+            kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+
+            list.add(kind);
+        }
+
+        {
+            // EXCHANGE: EMAIL
+            DataKind kind = new DataKind(Email.CONTENT_ITEM_TYPE,
+                    R.string.emailLabelsGroup, android.R.drawable.sym_action_email, 15, true);
+
+            kind.actionHeader = new ActionInflater(list.resPackageName, kind);
+            kind.actionBody = new SimpleInflater(Email.DATA);
+
+            kind.typeColumn = Email.TYPE;
+            kind.typeList = new ArrayList<EditType>();
+            kind.typeList.add(new EditType(TYPE_EMAIL1, R.string.type_email_1, R.string.email_1)
+                    .setSpecificMax(1));
+            kind.typeList.add(new EditType(TYPE_EMAIL2, R.string.type_email_2, R.string.email_2)
+                    .setSpecificMax(1));
+            kind.typeList.add(new EditType(TYPE_EMAIL3, R.string.type_email_3, R.string.email_3)
+                    .setSpecificMax(1));
+
+            kind.fieldList = new ArrayList<EditField>();
+            kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+
+            list.add(kind);
+        }
+
+        {
+            // EXCHANGE: IM
+            DataKind kind = new DataKind(Im.CONTENT_ITEM_TYPE, R.string.imLabelsGroup,
+                    android.R.drawable.sym_action_chat, 20, true);
+
+            kind.actionHeader = new ActionInflater(list.resPackageName, kind);
+            kind.actionBody = new SimpleInflater(Im.DATA);
+
+            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));
+
+            kind.fieldList = new ArrayList<EditField>();
+            kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
+
+            list.add(kind);
+        }
+
+        {
+            // EXCHANGE: NICKNAME
+            DataKind kind = new DataKind(Nickname.CONTENT_ITEM_TYPE,
+                    R.string.nicknameLabelsGroup, -1, 115, true);
+            kind.secondary = true;
+            kind.typeOverallMax = 1;
+
+            kind.actionHeader = new SimpleInflater(list.resPackageName, R.string.nicknameLabelsGroup);
+            kind.actionBody = new SimpleInflater(Nickname.NAME);
+
+            kind.fieldList = new ArrayList<EditField>();
+            kind.fieldList.add(new EditField(Nickname.NAME, R.string.nicknameLabelsGroup,
+                    FLAGS_PERSON_NAME));
+
+            list.add(kind);
+        }
+
+        {
+            // EXCHANGE: WEBSITE
+            DataKind kind = new DataKind(Website.CONTENT_ITEM_TYPE,
+                    R.string.websiteLabelsGroup, -1, 120, true);
+            kind.secondary = true;
+            kind.typeOverallMax = 1;
+
+            kind.actionHeader = new SimpleInflater(list.resPackageName, R.string.websiteLabelsGroup);
+            kind.actionBody = new SimpleInflater(Website.URL);
+
+            kind.fieldList = new ArrayList<EditField>();
+            kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE));
+
+            list.add(kind);
+        }
+    }
+
+    /**
+     * Hard-coded instance of {@link ContactsSource} for Facebook.
+     */
+    static void buildFacebook(ContactsSource list) {
+        list.accountType = ACCOUNT_TYPE_FACEBOOK;
+        list.readOnly = true;
+
+        // TODO: fill in read-only values that should be visible?
+    }
+
+    /**
+     * Simple inflater that assumes a string resource has a "%s" that will be
+     * filled from the given column.
+     */
+    public static class SimpleInflater implements StringInflater {
+        private final String mPackageName;
+        private final int mStringRes;
+        private final String mColumnName;
+
+        public SimpleInflater(String packageName, int stringRes) {
+            this(packageName, stringRes, null);
+        }
+
+        public SimpleInflater(String columnName) {
+            this(null, -1, columnName);
+        }
+
+        public SimpleInflater(String packageName, int stringRes, String columnName) {
+            mPackageName = packageName;
+            mStringRes = stringRes;
+            mColumnName = columnName;
+        }
+
+        public CharSequence inflateUsing(Context context, Cursor cursor) {
+            final int index = mColumnName != null ? cursor.getColumnIndex(mColumnName) : -1;
+            final boolean validString = mStringRes > 0;
+            final boolean validColumn = index != -1;
+
+            final CharSequence stringValue = validString ? context.getPackageManager().getText(
+                    mPackageName, mStringRes, null) : null;
+            final CharSequence columnValue = validColumn ? cursor.getString(index) : null;
+
+            if (validString && validColumn) {
+                return String.format(stringValue.toString(), columnValue);
+            } else if (validString) {
+                return stringValue;
+            } else if (validColumn) {
+                return columnValue;
+            } else {
+                return null;
+            }
+        }
+    }
+
+    /**
+     * Simple inflater that will combine two string resources, usually to
+     * provide an action string like "Call home", where "home" is provided from
+     * {@link EditType#labelRes}.
+     */
+    public static class ActionInflater implements StringInflater {
+        private String mPackageName;
+        private DataKind mKind;
+
+        public ActionInflater(String packageName, DataKind labelProvider) {
+            mPackageName = packageName;
+            mKind = labelProvider;
+        }
+
+        public CharSequence inflateUsing(Context context, Cursor cursor) {
+            final EditType type = EntityModifier.getCurrentType(cursor, mKind);
+            final boolean validString = (type != null && type.actionRes > 0);
+            return validString ? context.getPackageManager().getText(mPackageName, type.actionRes,
+                    null) : null;
+        }
+    }
+
+    public static class ActionAltInflater implements StringInflater {
+        private String mPackageName;
+        private DataKind mKind;
+
+        public ActionAltInflater(String packageName, DataKind labelProvider) {
+            mPackageName = packageName;
+            mKind = labelProvider;
+        }
+
+        public CharSequence inflateUsing(Context context, Cursor cursor) {
+            final EditType type = EntityModifier.getCurrentType(cursor, mKind);
+            final boolean validString = (type != null && type.actionAltRes > 0);
+            return validString ? context.getPackageManager().getText(mPackageName,
+                    type.actionAltRes, null) : null;
+        }
+    }
+}
diff --git a/src/com/android/contacts/model/Sources.java b/src/com/android/contacts/model/Sources.java
index 1eebf16..3258f13 100644
--- a/src/com/android/contacts/model/Sources.java
+++ b/src/com/android/contacts/model/Sources.java
@@ -16,30 +16,17 @@
 
 package com.android.contacts.model;
 
-import com.android.contacts.R;
-
+import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.accounts.AuthenticatorDescription;
-import android.content.ContentValues;
+import android.content.ContentResolver;
 import android.content.Context;
+import android.content.IContentService;
+import android.content.SyncAdapterType;
 import android.content.pm.PackageManager;
-import android.database.Cursor;
-import android.provider.ContactsContract.CommonDataKinds.Im;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.Nickname;
-import android.provider.ContactsContract.CommonDataKinds.Note;
-import android.provider.ContactsContract.CommonDataKinds.Organization;
-import android.provider.ContactsContract.CommonDataKinds.Photo;
-import android.provider.ContactsContract.CommonDataKinds.StructuredName;
-import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
-import android.provider.ContactsContract.CommonDataKinds.Website;
-import android.view.inputmethod.EditorInfo;
-
-import com.android.contacts.model.ContactsSource.DataKind;
-import com.android.contacts.model.ContactsSource.EditType;
-import com.android.contacts.model.ContactsSource.EditField;
-import com.android.contacts.model.ContactsSource.StringInflater;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.util.Log;
 
 import java.lang.ref.SoftReference;
 import java.util.ArrayList;
@@ -48,599 +35,135 @@
 /**
  * Singleton holder for all parsed {@link ContactsSource} available on the
  * system, typically filled through {@link PackageManager} queries.
- * <p>
- * Some {@link ContactsSource} may be hard-coded here, as the constraint
- * language hasn't been finalized.
  */
 public class Sources {
-    // TODO: finish hard-coding all constraints
+    private static final String TAG = "Sources";
 
-    private static SoftReference<Sources> sInstance;
+    public static final String ACCOUNT_TYPE_FALLBACK = HardCodedSources.ACCOUNT_TYPE_GOOGLE;
 
-    private ArrayList<SourcesCompleteListener> mSourcesCompleteListeners;
-    private boolean mComplete = false;
-
-    /**
-     * Returns the singleton {@link Sources} without binding data from
-     * the available authenticators. All clients of this class should move
-     * to requestInstance().
-     */
-    @Deprecated
-    public static synchronized Sources getPartialInstance(Context context) {
-        if (sInstance == null || sInstance.get() == null) {
-            sInstance = new SoftReference<Sources>(new Sources(context, false));
-        }
-        return sInstance.get();
-    }
-
-    /**
-     * Requests the singleton instance of {@link Sources} with data bound
-     * from the available authenticators. The result will be returned to
-     * the {@link SourcesCompleteListener} callback interface.
-     * @param context
-     * @param listener An implementation of {@link SourcesCompleteListener} to
-     * pass the {@link Sources} object to.
-     */
-    public static synchronized void requestInstance(Context context,
-            SourcesCompleteListener listener) {
-        Sources sources = sInstance == null ? null : sInstance.get();
-        if (sources == null) {
-            sources = new Sources(context, true);
-            sources.mSourcesCompleteListeners.add(listener);
-            sInstance = new SoftReference<Sources>(sources);
-        } else {
-            if (sources.mComplete) {
-                // We're already complete, so we can call the callback right now.
-                listener.onSourcesComplete(sources);
-            } else {
-                sources.mSourcesCompleteListeners.add(listener);
-            }
-        }
-
-    }
-
-    public static final String ACCOUNT_TYPE_GOOGLE = "com.google.GAIA";
-    public static final String ACCOUNT_TYPE_EXCHANGE = "com.android.exchange";
-    public static final String ACCOUNT_TYPE_FACEBOOK = "com.facebook.auth.login";
+    private Context mContext;
 
     private HashMap<String, ContactsSource> mSources = new HashMap<String, ContactsSource>();
 
-    private Sources(Context context, boolean fetchAuthenticatorData) {
-        mSources.put(ACCOUNT_TYPE_GOOGLE, buildGoogle(context));
-        mSources.put(ACCOUNT_TYPE_EXCHANGE, buildExchange(context));
-        mSources.put(ACCOUNT_TYPE_FACEBOOK, buildFacebook(context));
+    private static SoftReference<Sources> sInstance = null;
 
-        if (fetchAuthenticatorData) {
-            mSourcesCompleteListeners = new ArrayList<SourcesCompleteListener>();
-            AuthenticatorDescription[] authenticatorDescs =
-                    AccountManager.get(context).getAuthenticatorTypes();
-
-            for (int i = 0; i < authenticatorDescs.length; i++) {
-                String accountType = authenticatorDescs[i].type;
-                ContactsSource contactSource = mSources.get(accountType);
-                if (contactSource != null) {
-                    contactSource.iconRes = authenticatorDescs[i].iconId;
-                    contactSource.titleRes = authenticatorDescs[i].labelId;
-                    contactSource.resPackageName = authenticatorDescs[i].packageName;;
-                }
-            }
-
-            mComplete = true;
-            for (SourcesCompleteListener listener : mSourcesCompleteListeners) {
-                listener.onSourcesComplete(Sources.this);
-            }
+    /**
+     * Requests the singleton instance of {@link Sources} with data bound from
+     * the available authenticators. This method blocks until its interaction
+     * with {@link AccountManager} is finished, so don't call from a UI thread.
+     */
+    public static synchronized Sources getInstance(Context context) {
+        Sources sources = sInstance == null ? null : sInstance.get();
+        if (sources == null) {
+            sources = new Sources(context);
+            sInstance = new SoftReference<Sources>(sources);
         }
+        return sources;
     }
 
     /**
-     * Find the {@link ContactsSource} for the given
+     * Internal constructor that only performs initial parsing. Obtain a
      * {@link android.provider.ContactsContract.RawContacts#ACCOUNT_TYPE}.
      */
-    public ContactsSource getSourceForType(String accountType) {
-        return mSources.get(accountType);
-    }
-
-    private static final int FLAGS_PHONE = EditorInfo.TYPE_CLASS_PHONE;
-    private static final int FLAGS_EMAIL = EditorInfo.TYPE_CLASS_TEXT
-            | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
-    private static final int FLAGS_PERSON_NAME = EditorInfo.TYPE_CLASS_TEXT
-            | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS | EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME;
-    private static final int FLAGS_PHONETIC = EditorInfo.TYPE_CLASS_TEXT
-            | EditorInfo.TYPE_TEXT_VARIATION_PHONETIC;
-    private static final int FLAGS_GENERIC_NAME = EditorInfo.TYPE_CLASS_TEXT
-            | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
-    private static final int FLAGS_NOTE = EditorInfo.TYPE_CLASS_TEXT
-            | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
-    private static final int FLAGS_WEBSITE = EditorInfo.TYPE_CLASS_TEXT
-            | EditorInfo.TYPE_TEXT_VARIATION_URI;
-    private static final int FLAGS_POSTAL = EditorInfo.TYPE_CLASS_TEXT
-            | EditorInfo.TYPE_TEXT_VARIATION_POSTAL_ADDRESS | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS
-            | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
-
-    /**
-     * Hard-coded instance of {@link ContactsSource} for Google Contacts.
-     */
-    private ContactsSource buildGoogle(Context context) {
-        final ContactsSource list = new ContactsSource();
-        list.accountType = ACCOUNT_TYPE_GOOGLE;
-        list.resPackageName = context.getPackageName();
-
-        {
-            // GOOGLE: STRUCTUREDNAME
-            DataKind kind = new DataKind(StructuredName.CONTENT_ITEM_TYPE,
-                    R.string.nameLabelsGroup, -1, -1, true);
-            list.add(kind);
-        }
-
-        {
-            // GOOGLE: PHOTO
-            DataKind kind = new DataKind(Photo.CONTENT_ITEM_TYPE, -1, -1, -1, true);
-            list.add(kind);
-        }
-
-        {
-            // GOOGLE: PHONE
-            DataKind kind = new DataKind(Phone.CONTENT_ITEM_TYPE,
-                    R.string.phoneLabelsGroup, android.R.drawable.sym_action_call, 10, true);
-            kind.iconAltRes = R.drawable.sym_action_sms;
-
-            kind.actionHeader = new ActionInflater(list.resPackageName, kind);
-            kind.actionAltHeader = new ActionAltInflater(list.resPackageName, kind);
-            kind.actionBody = new SimpleInflater(Phone.NUMBER);
-
-            kind.typeColumn = Phone.TYPE;
-            kind.typeList = new ArrayList<EditType>();
-            kind.typeList.add(new EditType(Phone.TYPE_HOME, R.string.type_home, R.string.call_home,
-                    R.string.sms_home));
-            kind.typeList.add(new EditType(Phone.TYPE_MOBILE, R.string.type_mobile,
-                    R.string.call_mobile, R.string.sms_mobile));
-            kind.typeList.add(new EditType(Phone.TYPE_WORK, R.string.type_work, R.string.call_work,
-                    R.string.sms_work));
-            kind.typeList.add(new EditType(Phone.TYPE_FAX_WORK, R.string.type_fax_work,
-                    R.string.call_fax_work, R.string.sms_fax_work).setSecondary(true));
-            kind.typeList.add(new EditType(Phone.TYPE_FAX_HOME, R.string.type_fax_home,
-                    R.string.call_fax_home, R.string.sms_fax_home).setSecondary(true));
-            kind.typeList.add(new EditType(Phone.TYPE_PAGER, R.string.type_pager,
-                    R.string.call_pager, R.string.sms_pager).setSecondary(true));
-            kind.typeList.add(new EditType(Phone.TYPE_OTHER, R.string.type_other,
-                    R.string.call_other, R.string.sms_other));
-            kind.typeList.add(new EditType(Phone.TYPE_CUSTOM, R.string.type_custom,
-                    R.string.call_custom, R.string.sms_custom).setSecondary(true).setCustomColumn(
-                    Phone.LABEL));
-
-            kind.fieldList = new ArrayList<EditField>();
-            kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
-
-            list.add(kind);
-        }
-
-        {
-            // GOOGLE: EMAIL
-            DataKind kind = new DataKind(Email.CONTENT_ITEM_TYPE,
-                    R.string.emailLabelsGroup, android.R.drawable.sym_action_email, 15, true);
-
-            kind.actionHeader = new ActionInflater(list.resPackageName, kind);
-            kind.actionBody = new SimpleInflater(Email.DATA);
-
-            kind.typeColumn = Email.TYPE;
-            kind.typeList = new ArrayList<EditType>();
-            kind.typeList
-                    .add(new EditType(Email.TYPE_HOME, R.string.type_home, R.string.email_home));
-            kind.typeList
-                    .add(new EditType(Email.TYPE_WORK, R.string.type_work, R.string.email_work));
-            kind.typeList.add(new EditType(Email.TYPE_OTHER, R.string.type_other,
-                    R.string.email_other));
-            kind.typeList.add(new EditType(Email.TYPE_CUSTOM, R.string.type_custom,
-                    R.string.email_home).setSecondary(true).setCustomColumn(Email.LABEL));
-
-            kind.fieldList = new ArrayList<EditField>();
-            kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
-
-            list.add(kind);
-        }
-
-        {
-            // GOOGLE: IM
-            DataKind kind = new DataKind(Im.CONTENT_ITEM_TYPE, R.string.imLabelsGroup,
-                    android.R.drawable.sym_action_chat, 20, true);
-
-            kind.actionHeader = new ActionInflater(list.resPackageName, kind);
-            kind.actionBody = new SimpleInflater(Im.DATA);
-
-            // 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 = new ArrayList<EditType>();
-            kind.typeList.add(new EditType(Im.PROTOCOL_AIM, R.string.type_im_aim));
-            kind.typeList.add(new EditType(Im.PROTOCOL_MSN, R.string.type_im_msn));
-            kind.typeList.add(new EditType(Im.PROTOCOL_YAHOO, R.string.type_im_yahoo));
-            kind.typeList.add(new EditType(Im.PROTOCOL_SKYPE, R.string.type_im_skype));
-            kind.typeList.add(new EditType(Im.PROTOCOL_QQ, R.string.type_im_qq));
-            kind.typeList.add(new EditType(Im.PROTOCOL_GOOGLE_TALK, R.string.type_im_google_talk));
-            kind.typeList.add(new EditType(Im.PROTOCOL_ICQ, R.string.type_im_icq));
-            kind.typeList.add(new EditType(Im.PROTOCOL_JABBER, R.string.type_im_jabber));
-            kind.typeList.add(new EditType(Im.PROTOCOL_CUSTOM, R.string.type_custom).setSecondary(
-                    true).setCustomColumn(Im.CUSTOM_PROTOCOL));
-
-            kind.fieldList = new ArrayList<EditField>();
-            kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
-
-            list.add(kind);
-        }
-
-        {
-            // GOOGLE: POSTAL
-            DataKind kind = new DataKind(StructuredPostal.CONTENT_ITEM_TYPE,
-                    R.string.postalLabelsGroup, R.drawable.sym_action_map, 25, true);
-
-            kind.actionHeader = new ActionInflater(list.resPackageName, kind);
-            // TODO: build body from various structured fields
-            kind.actionBody = new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS);
-
-            kind.typeColumn = StructuredPostal.TYPE;
-            kind.typeList = new ArrayList<EditType>();
-            kind.typeList.add(new EditType(StructuredPostal.TYPE_HOME, R.string.type_home,
-                    R.string.map_home));
-            kind.typeList.add(new EditType(StructuredPostal.TYPE_WORK, R.string.type_work,
-                    R.string.map_work));
-            kind.typeList.add(new EditType(StructuredPostal.TYPE_OTHER, R.string.type_other,
-                    R.string.map_other));
-            kind.typeList
-                    .add(new EditType(StructuredPostal.TYPE_CUSTOM, R.string.type_custom,
-                            R.string.map_custom).setSecondary(true).setCustomColumn(
-                            StructuredPostal.LABEL));
-
-            kind.fieldList = new ArrayList<EditField>();
-            kind.fieldList.add(new EditField(StructuredPostal.AGENT, -1, FLAGS_POSTAL, true));
-            kind.fieldList.add(new EditField(StructuredPostal.HOUSENAME, -1, FLAGS_POSTAL, true));
-            kind.fieldList.add(new EditField(StructuredPostal.STREET, -1, FLAGS_POSTAL));
-            kind.fieldList.add(new EditField(StructuredPostal.POBOX, -1, FLAGS_POSTAL, true));
-            kind.fieldList.add(new EditField(StructuredPostal.NEIGHBORHOOD, -1, FLAGS_POSTAL, true));
-            kind.fieldList.add(new EditField(StructuredPostal.CITY, -1, FLAGS_POSTAL));
-            kind.fieldList.add(new EditField(StructuredPostal.SUBREGION, -1, FLAGS_POSTAL, true));
-            kind.fieldList.add(new EditField(StructuredPostal.REGION, -1, FLAGS_POSTAL));
-            kind.fieldList.add(new EditField(StructuredPostal.POSTCODE, -1, FLAGS_POSTAL));
-            kind.fieldList.add(new EditField(StructuredPostal.COUNTRY, -1, FLAGS_POSTAL, true));
-
-            list.add(kind);
-        }
-
-        {
-            // GOOGLE: ORGANIZATION
-            DataKind kind = new DataKind(Organization.CONTENT_ITEM_TYPE,
-                    R.string.organizationLabelsGroup, R.drawable.sym_action_organization, 30, true);
-
-            kind.actionHeader = new SimpleInflater(list.resPackageName, R.string.organizationLabelsGroup);
-            // TODO: build body from multiple fields
-            kind.actionBody = new SimpleInflater(Organization.TITLE);
-
-            kind.typeColumn = Organization.TYPE;
-            kind.typeList = new ArrayList<EditType>();
-            kind.typeList.add(new EditType(Organization.TYPE_WORK, R.string.type_work));
-            kind.typeList.add(new EditType(Organization.TYPE_OTHER, R.string.type_other));
-            kind.typeList.add(new EditType(Organization.TYPE_CUSTOM, R.string.type_custom)
-                    .setSecondary(true).setCustomColumn(Organization.LABEL));
-
-            kind.fieldList = new ArrayList<EditField>();
-            kind.fieldList.add(new EditField(Organization.COMPANY, R.string.ghostData_company,
-                    FLAGS_GENERIC_NAME));
-            kind.fieldList.add(new EditField(Organization.TITLE, R.string.ghostData_title,
-                    FLAGS_GENERIC_NAME));
-
-            list.add(kind);
-        }
-
-        {
-            // GOOGLE: NOTE
-            DataKind kind = new DataKind(Note.CONTENT_ITEM_TYPE,
-                    R.string.label_notes, R.drawable.sym_note, 110, true);
-            kind.secondary = true;
-
-            kind.actionHeader = new SimpleInflater(list.resPackageName, R.string.label_notes);
-            kind.actionBody = new SimpleInflater(Note.NOTE);
-
-            kind.fieldList = new ArrayList<EditField>();
-            kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE));
-
-            list.add(kind);
-        }
-
-        {
-            // GOOGLE: NICKNAME
-            DataKind kind = new DataKind(Nickname.CONTENT_ITEM_TYPE,
-                    R.string.nicknameLabelsGroup, -1, 115, true);
-            kind.secondary = true;
-
-            kind.actionHeader = new SimpleInflater(list.resPackageName, R.string.nicknameLabelsGroup);
-            kind.actionBody = new SimpleInflater(Nickname.NAME);
-
-            kind.fieldList = new ArrayList<EditField>();
-            kind.fieldList.add(new EditField(Nickname.NAME, R.string.nicknameLabelsGroup,
-                    FLAGS_PERSON_NAME));
-
-            list.add(kind);
-        }
-
-        // TODO: GOOGLE: GROUPMEMBERSHIP
-        // TODO: GOOGLE: WEBSITE
-
-        return list;
+    private Sources(Context context) {
+        mContext = context;
+        loadAccounts();
     }
 
     /**
-     * The constants below are shared with the Exchange sync adapter, and are
-     * currently static. These values should be maintained in parallel.
+     * Blocking call to load all {@link AuthenticatorDescription} known by the
+     * {@link AccountManager} on the system.
      */
-    private static final int TYPE_EMAIL1 = 20;
-    private static final int TYPE_EMAIL2 = 21;
-    private static final int TYPE_EMAIL3 = 22;
+    protected void loadAccounts() {
+        mSources.clear();
 
-    private static final int TYPE_IM1 = 23;
-    private static final int TYPE_IM2 = 24;
-    private static final int TYPE_IM3 = 25;
+        final AccountManager am = AccountManager.get(mContext);
+        final IContentService cs = ContentResolver.getContentService();
 
-    private static final int TYPE_WORK2 = 26;
-    private static final int TYPE_HOME2 = 27;
-    private static final int TYPE_CAR = 28;
-    private static final int TYPE_COMPANY_MAIN = 29;
-    private static final int TYPE_MMS = 30;
-    private static final int TYPE_RADIO = 31;
+        try {
+            final SyncAdapterType[] syncs = cs.getSyncAdapterTypes();
+            final AuthenticatorDescription[] auths = am.getAuthenticatorTypes();
 
-    /**
-     * Hard-coded instance of {@link ContactsSource} for Exchange.
-     */
-    private ContactsSource buildExchange(Context context) {
-        final ContactsSource list = new ContactsSource();
-        list.accountType = ACCOUNT_TYPE_EXCHANGE;
-        list.resPackageName = context.getPackageName();
+            for (SyncAdapterType sync : syncs) {
+                if (ContactsContract.AUTHORITY.equals(sync.authority)) {
+                    // Skip sync adapters that don't provide contact data.
+                    continue;
+                }
 
-        {
-            // EXCHANGE: STRUCTUREDNAME
-            DataKind kind = new DataKind(StructuredName.CONTENT_ITEM_TYPE,
-                    R.string.nameLabelsGroup, -1, -1, true);
-            kind.typeOverallMax = 1;
-            list.add(kind);
+                // Look for the formatting details provided by each sync
+                // adapter, using the authenticator to find general resources.
+                final String accountType = sync.accountType;
+                final AuthenticatorDescription auth = findAuthenticator(auths, accountType);
+
+                final ContactsSource source = new ContactsSource();
+                source.accountType = auth.type;
+                // TODO: use syncadapter package instead, since it provides resources
+                source.resPackageName = auth.packageName;
+                source.titleRes = auth.labelId;
+                source.iconRes = auth.iconId;
+
+                mSources.put(accountType, source);
+            }
+        } catch (RemoteException e) {
+            Log.w(TAG, "Problem loading accounts: " + e.toString());
         }
-
-        {
-            // EXCHANGE: PHOTO
-            DataKind kind = new DataKind(Photo.CONTENT_ITEM_TYPE, -1, -1, -1, true);
-            kind.typeOverallMax = 1;
-            list.add(kind);
-        }
-
-        {
-            // EXCHANGE: PHONE
-            DataKind kind = new DataKind(Phone.CONTENT_ITEM_TYPE,
-                    R.string.phoneLabelsGroup, android.R.drawable.sym_action_call, 10, true);
-            kind.iconAltRes = R.drawable.sym_action_sms;
-
-            kind.actionHeader = new ActionInflater(list.resPackageName, kind);
-            kind.actionAltHeader = new ActionAltInflater(list.resPackageName, kind);
-            kind.actionBody = new SimpleInflater(Phone.NUMBER);
-
-            kind.typeColumn = Phone.TYPE;
-            kind.typeList = new ArrayList<EditType>();
-            kind.typeList.add(new EditType(Phone.TYPE_HOME, R.string.type_home, R.string.call_home,
-                    R.string.sms_home).setSpecificMax(1));
-            kind.typeList.add(new EditType(TYPE_HOME2, R.string.type_home_2, R.string.call_home_2,
-                    R.string.sms_home_2).setSecondary(true).setSpecificMax(1));
-            kind.typeList.add(new EditType(Phone.TYPE_MOBILE, R.string.type_mobile,
-                    R.string.call_mobile, R.string.sms_mobile).setSpecificMax(1));
-            kind.typeList.add(new EditType(Phone.TYPE_WORK, R.string.type_work, R.string.call_work,
-                    R.string.sms_work).setSpecificMax(1));
-            kind.typeList.add(new EditType(TYPE_WORK2, R.string.type_work_2, R.string.call_work_2,
-                    R.string.sms_work_2).setSecondary(true).setSpecificMax(1));
-            kind.typeList.add(new EditType(Phone.TYPE_FAX_WORK, R.string.type_fax_work,
-                    R.string.call_fax_work, R.string.sms_fax_work).setSecondary(true)
-                    .setSpecificMax(1));
-            kind.typeList.add(new EditType(Phone.TYPE_FAX_HOME, R.string.type_fax_home,
-                    R.string.call_fax_home, R.string.sms_fax_home).setSecondary(true)
-                    .setSpecificMax(1));
-            kind.typeList.add(new EditType(Phone.TYPE_PAGER, R.string.type_pager,
-                    R.string.call_pager, R.string.sms_pager).setSecondary(true).setSpecificMax(1));
-            kind.typeList.add(new EditType(TYPE_CAR, R.string.type_car, R.string.call_car,
-                    R.string.sms_car).setSecondary(true).setSpecificMax(1));
-            kind.typeList.add(new EditType(TYPE_COMPANY_MAIN, R.string.type_company_main,
-                    R.string.call_company_main, R.string.sms_company_main).setSecondary(true)
-                    .setSpecificMax(1));
-            kind.typeList.add(new EditType(TYPE_MMS, R.string.type_mms, R.string.call_mms,
-                    R.string.sms_mms).setSecondary(true).setSpecificMax(1));
-            kind.typeList.add(new EditType(TYPE_RADIO, R.string.type_radio, R.string.call_radio,
-                    R.string.sms_radio).setSecondary(true).setSpecificMax(1));
-            kind.typeList.add(new EditType(Phone.TYPE_CUSTOM, R.string.type_assistant,
-                    R.string.call_custom, R.string.sms_custom).setSecondary(true).setSpecificMax(1)
-                    .setCustomColumn(Phone.LABEL));
-
-            kind.fieldList = new ArrayList<EditField>();
-            kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
-
-            list.add(kind);
-        }
-
-        {
-            // EXCHANGE: EMAIL
-            DataKind kind = new DataKind(Email.CONTENT_ITEM_TYPE,
-                    R.string.emailLabelsGroup, android.R.drawable.sym_action_email, 15, true);
-
-            kind.actionHeader = new ActionInflater(list.resPackageName, kind);
-            kind.actionBody = new SimpleInflater(Email.DATA);
-
-            kind.typeColumn = Email.TYPE;
-            kind.typeList = new ArrayList<EditType>();
-            kind.typeList.add(new EditType(TYPE_EMAIL1, R.string.type_email_1, R.string.email_1)
-                    .setSpecificMax(1));
-            kind.typeList.add(new EditType(TYPE_EMAIL2, R.string.type_email_2, R.string.email_2)
-                    .setSpecificMax(1));
-            kind.typeList.add(new EditType(TYPE_EMAIL3, R.string.type_email_3, R.string.email_3)
-                    .setSpecificMax(1));
-
-            kind.fieldList = new ArrayList<EditField>();
-            kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
-
-            list.add(kind);
-        }
-
-        {
-            // EXCHANGE: IM
-            DataKind kind = new DataKind(Im.CONTENT_ITEM_TYPE, R.string.imLabelsGroup,
-                    android.R.drawable.sym_action_chat, 20, true);
-
-            kind.actionHeader = new ActionInflater(list.resPackageName, kind);
-            kind.actionBody = new SimpleInflater(Im.DATA);
-
-            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));
-
-            kind.fieldList = new ArrayList<EditField>();
-            kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
-
-            list.add(kind);
-        }
-
-        {
-            // EXCHANGE: NICKNAME
-            DataKind kind = new DataKind(Nickname.CONTENT_ITEM_TYPE,
-                    R.string.nicknameLabelsGroup, -1, 115, true);
-            kind.secondary = true;
-            kind.typeOverallMax = 1;
-
-            kind.actionHeader = new SimpleInflater(list.resPackageName, R.string.nicknameLabelsGroup);
-            kind.actionBody = new SimpleInflater(Nickname.NAME);
-
-            kind.fieldList = new ArrayList<EditField>();
-            kind.fieldList.add(new EditField(Nickname.NAME, R.string.nicknameLabelsGroup,
-                    FLAGS_PERSON_NAME));
-
-            list.add(kind);
-        }
-
-        {
-            // EXCHANGE: WEBSITE
-            DataKind kind = new DataKind(Website.CONTENT_ITEM_TYPE,
-                    R.string.websiteLabelsGroup, -1, 120, true);
-            kind.secondary = true;
-            kind.typeOverallMax = 1;
-
-            kind.actionHeader = new SimpleInflater(list.resPackageName, R.string.websiteLabelsGroup);
-            kind.actionBody = new SimpleInflater(Website.URL);
-
-            kind.fieldList = new ArrayList<EditField>();
-            kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE));
-
-            list.add(kind);
-        }
-
-        return list;
     }
 
     /**
-     * Hard-coded instance of {@link ContactsSource} for Facebook.
+     * Find a specific {@link AuthenticatorDescription} in the provided list
+     * that matches the given account type.
      */
-    private ContactsSource buildFacebook(Context context) {
-        final ContactsSource list = new ContactsSource();
-        list.accountType = ACCOUNT_TYPE_FACEBOOK;
-        list.resPackageName = context.getPackageName();
-
-        return list;
-
-    }
-
-    /**
-     * Simple inflater that assumes a string resource has a "%s" that will be
-     * filled from the given column.
-     */
-    public static class SimpleInflater implements StringInflater {
-        private final String mPackageName;
-        private final int mStringRes;
-        private final String mColumnName;
-
-        public SimpleInflater(String packageName, int stringRes) {
-            this(packageName, stringRes, null);
-        }
-
-        public SimpleInflater(String columnName) {
-            this(null, -1, columnName);
-        }
-
-        public SimpleInflater(String packageName, int stringRes, String columnName) {
-            mPackageName = packageName;
-            mStringRes = stringRes;
-            mColumnName = columnName;
-        }
-
-        public CharSequence inflateUsing(Context context, Cursor cursor) {
-            final int index = mColumnName != null ? cursor.getColumnIndex(mColumnName) : -1;
-            final boolean validString = mStringRes > 0;
-            final boolean validColumn = index != -1;
-
-            final CharSequence stringValue = validString ? context.getPackageManager().getText(
-                    mPackageName, mStringRes, null) : null;
-            final CharSequence columnValue = validColumn ? cursor.getString(index) : null;
-
-            if (validString && validColumn) {
-                return String.format(stringValue.toString(), columnValue);
-            } else if (validString) {
-                return stringValue;
-            } else if (validColumn) {
-                return columnValue;
-            } else {
-                return null;
+    protected static AuthenticatorDescription findAuthenticator(AuthenticatorDescription[] auths,
+            String accountType) {
+        for (AuthenticatorDescription auth : auths) {
+            if (accountType.equals(auth.type)) {
+                return auth;
             }
         }
+        throw new IllegalStateException("Couldn't find authenticator for specific account type");
     }
 
     /**
-     * Simple inflater that will combine two string resources, usually to
-     * provide an action string like "Call home", where "home" is provided from
-     * {@link EditType#labelRes}.
+     * Return list of all known, writable {@link ContactsSource}. Sources
+     * returned may require inflation before they can be used.
      */
-    public static class ActionInflater implements StringInflater {
-        private String mPackageName;
-        private DataKind mKind;
+    public ArrayList<Account> getWritableAccounts() {
+        final AccountManager am = AccountManager.get(mContext);
+        final Account[] accounts = am.getAccounts();
+        final ArrayList<Account> writable = new ArrayList<Account>();
 
-        public ActionInflater(String packageName, DataKind labelProvider) {
-            mPackageName = packageName;
-            mKind = labelProvider;
+        for (Account account : accounts) {
+            // Ensure we have details loaded for each account
+            final ContactsSource source = getInflatedSource(account.type,
+                    ContactsSource.LEVEL_SUMMARY);
+            if (!source.readOnly) {
+                writable.add(account);
+            }
         }
-
-        public CharSequence inflateUsing(Context context, Cursor cursor) {
-            final EditType type = EntityModifier.getCurrentType(cursor, mKind);
-            final boolean validString = (type != null && type.actionRes > 0);
-            return validString ? context.getPackageManager().getText(mPackageName, type.actionRes,
-                    null) : null;
-        }
+        return writable;
     }
 
-    public static class ActionAltInflater implements StringInflater {
-        private String mPackageName;
-        private DataKind mKind;
-
-        public ActionAltInflater(String packageName, DataKind labelProvider) {
-            mPackageName = packageName;
-            mKind = labelProvider;
+    protected ContactsSource getSourceForType(String accountType) {
+        ContactsSource source = mSources.get(accountType);
+        if (source == null) {
+            Log.w(TAG, "Unknown account type '" + accountType + "', falling back to default");
+            source = mSources.get(ACCOUNT_TYPE_FALLBACK);
         }
-
-        public CharSequence inflateUsing(Context context, Cursor cursor) {
-            final EditType type = EntityModifier.getCurrentType(cursor, mKind);
-            final boolean validString = (type != null && type.actionAltRes > 0);
-            return validString ? context.getPackageManager().getText(mPackageName,
-                    type.actionAltRes, null) : null;
-        }
+        return source;
     }
 
     /**
-     * Callback interface used for being notified when the Sources object
-     * has finished binding with data from the Authenticators.
+     * Return {@link ContactsSource} for the given account type.
      */
-    public interface SourcesCompleteListener {
-        public void onSourcesComplete(Sources sources);
+    public ContactsSource getInflatedSource(String accountType, int inflateLevel) {
+        final ContactsSource source = getSourceForType(accountType);
+        if (source.isInflated(inflateLevel)) {
+            // Found inflated, so return directly
+            return source;
+        } else {
+            // Not inflated, but requested that we force-inflate
+            source.ensureInflated(mContext, inflateLevel);
+            return source;
+        }
     }
 }
diff --git a/src/com/android/contacts/ui/EditContactActivity.java b/src/com/android/contacts/ui/EditContactActivity.java
index 0241ffb..b87b0c9 100644
--- a/src/com/android/contacts/ui/EditContactActivity.java
+++ b/src/com/android/contacts/ui/EditContactActivity.java
@@ -16,16 +16,23 @@
 
 package com.android.contacts.ui;
 
+import com.android.contacts.BaseContactCardActivity;
+import com.android.contacts.ContactsUtils;
 import com.android.contacts.R;
 import com.android.contacts.ScrollingTabWidget;
 import com.android.contacts.model.ContactsSource;
 import com.android.contacts.model.EntityDelta;
+import com.android.contacts.model.HardCodedSources;
 import com.android.contacts.model.Sources;
-import com.android.contacts.model.ContactsSource.EditType;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
 import com.android.contacts.ui.widget.ContactEditorView;
+import com.android.contacts.util.EmptyService;
+import com.android.contacts.util.NotifyingAsyncQueryHandler;
+import com.android.contacts.util.WeakAsyncTask;
 import com.android.internal.widget.ContactHeaderWidget;
 
+import android.accounts.Account;
+import android.accounts.AccountManager;
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.Dialog;
@@ -40,14 +47,20 @@
 import android.content.EntityIterator;
 import android.content.Intent;
 import android.content.OperationApplicationException;
+import android.database.Cursor;
 import android.net.Uri;
+import android.os.AsyncTask;
 import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.os.RemoteException;
+import android.provider.Contacts;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.RawContacts;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.ContactsContract.Contacts.Data;
 import android.util.Log;
+import android.util.SparseArray;
 import android.view.ContextThemeWrapper;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
@@ -62,7 +75,12 @@
 import android.widget.TextView;
 import android.widget.Toast;
 
+import java.lang.ref.WeakReference;
 import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 /**
  * Activity for editing or inserting a contact.
@@ -74,7 +92,11 @@
     /** The launch code when picking a photo and the raw data is returned */
     private static final int PHOTO_PICKED_WITH_DATA = 3021;
 
-    private static final String KEY_DELTAS = "deltas";
+    private static final int TOKEN_ENTITY = 41;
+
+    private static final String KEY_EDIT_STATE = "state";
+    private static final String KEY_EDITOR_STATE = "editor";
+    private static final String KEY_SELECTED_TAB = "tab";
 
     private ScrollingTabWidget mTabWidget;
     private ContactHeaderWidget mHeader;
@@ -82,10 +104,54 @@
     private View mTabContent;
     private ContactEditorView mEditor;
 
-    private Uri mUri;
-    private Sources mSources;
+    private EditState mState = new EditState();
 
-    private ArrayList<EntityDelta> mEntities = new ArrayList<EntityDelta>();
+    private static class EditState extends ArrayList<EntityDelta> implements Parcelable {
+        public long getAggregateId() {
+            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;
+            }
+        }
+
+        /** {@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];
+            }
+        };
+    }
 
     @Override
     protected void onCreate(Bundle icicle) {
@@ -98,11 +164,9 @@
         final String action = intent.getAction();
         final Bundle extras = intent.getExtras();
 
-        mUri = intent.getData();
-        mSources = Sources.getPartialInstance(this);
-
         setContentView(R.layout.act_edit);
 
+        // Header bar is filled later after queries finish
         mHeader = (ContactHeaderWidget)this.findViewById(R.id.contact_header_widget);
         mHeader.setContactHeaderListener(this);
         mHeader.showStar(true);
@@ -120,131 +184,181 @@
 
         if (Intent.ACTION_EDIT.equals(action) && icicle == null) {
             // Read initial state from database
-            readEntities();
-            rebuildTabs();
+            new QueryEntitiesTask(this).execute(intent);
 
-            final long contactId = ContentUris.parseId(mUri);
-            mHeader.bindFromContactId(contactId);
         } else if (Intent.ACTION_INSERT.equals(action)) {
-            // TODO: handle insert case for header
+            // Trigger dialog to pick account type
+            doAddAction();
 
         }
     }
 
+    private static class QueryEntitiesTask extends
+            WeakAsyncTask<Intent, Void, Void, EditContactActivity> {
+        public QueryEntitiesTask(EditContactActivity target) {
+            super(target);
+        }
+
+        @Override
+        protected Void doInBackground(EditContactActivity target, Intent... params) {
+            // Load edit details in background
+            final Context context = target;
+            final Sources sources = Sources.getInstance(context);
+            final Intent intent = params[0];
+
+            final ContentResolver resolver = context.getContentResolver();
+
+            // Handle both legacy and new authorities
+            String selection = "0";
+            final Uri data = intent.getData();
+            final String authority = data.getAuthority();
+            if (ContactsContract.AUTHORITY.equals(authority)) {
+                final long contactId = ContentUris.parseId(data);
+                selection = RawContacts.CONTACT_ID + "=" + contactId;
+
+            } else if (Contacts.AUTHORITY.equals(authority)) {
+                final long rawContactId = ContentUris.parseId(data);
+                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;
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(EditContactActivity target, Void result) {
+            // Bind UI to new background state
+            target.bindTabs();
+            target.bindHeader();
+        }
+    }
+
+
+//    /**
+//     * Instance state for {@link #mEditor} from a previous instance.
+//     */
+//    private SparseArray<Parcelable> mEditorState;
+//
+//    /**
+//     * Save state of the currently selected {@link #mEditor}, usually for
+//     * passing across instance boundaries to restore later.
+//     */
+//    private SparseArray<Parcelable> buildEditorState() {
+//        final SparseArray<Parcelable> state = new SparseArray<Parcelable>();
+//        if (mEditor != null) {
+//            mEditor.getView().saveHierarchyState(state);
+//        }
+//        return state;
+//    }
+//
     @Override
     protected void onSaveInstanceState(Bundle outState) {
         // Store entities with modifications
-        outState.putParcelableArrayList(KEY_DELTAS, mEntities);
+        outState.putParcelable(KEY_EDIT_STATE, mState);
+//        outState.putSparseParcelableArray(KEY_EDITOR_STATE, buildEditorState());
+//        outState.putInt(KEY_SELECTED_TAB, mTabWidget.getCurrentTab());
 
         super.onSaveInstanceState(outState);
     }
 
     @Override
     protected void onRestoreInstanceState(Bundle savedInstanceState) {
+        // Read modifications from instance
+        mState = savedInstanceState.<EditState> getParcelable(KEY_EDIT_STATE);
 
-        // Read and apply modifications from instance
-        mEntities = savedInstanceState.<EntityDelta> getParcelableArrayList(KEY_DELTAS);
-        rebuildTabs();
+        Log.d(TAG, "onrestoreinstancestate");
+
+//        mEditorState = savedInstanceState.getSparseParcelableArray(KEY_EDITOR_STATE);
+//
+//        final int selectedTab = savedInstanceState.getInt(KEY_SELECTED_TAB);
+        bindTabs();
+        bindHeader();
 
         // Restore selected tab and any focus
         super.onRestoreInstanceState(savedInstanceState);
     }
 
-    @Override
-    protected void onPostCreate(Bundle savedInstanceState) {
-        super.onPostCreate(savedInstanceState);
 
-        final String action = getIntent().getAction();
-        if (Intent.ACTION_INSERT.equals(action)) {
-            // TODO: show account disambig dialog before creating
+    /**
+     * Rebuild tabs to match our underlying {@link #mEntities} object, usually
+     * called once we've parsed {@link Entity} data or have inserted a new
+     * {@link RawContacts}.
+     */
+    protected void bindTabs() {
+        final Sources sources = Sources.getInstance(this);
 
-            final ContentValues values = new ContentValues();
-            values.put(RawContacts.ACCOUNT_TYPE, Sources.ACCOUNT_TYPE_GOOGLE);
-
-            final EntityDelta insert = new EntityDelta(ValuesDelta.fromAfter(values));
-            mEntities.add(insert);
-
-        }
-
-        // TODO: if insert, handle account disambig if not already done
-    }
-
-    protected void readEntities() {
-        // TODO: handle saving the previous values before replacing, in some cases
-        try {
-            final ContentResolver resolver = this.getContentResolver();
-            final long aggId = ContentUris.parseId(mUri);
-            final EntityIterator iterator = resolver.queryEntities(
-                    ContactsContract.RawContacts.CONTENT_URI,
-                    ContactsContract.RawContacts.CONTACT_ID + "=" + aggId, null, null);
-            while (iterator.hasNext()) {
-                final Entity before = iterator.next();
-                final EntityDelta entity = EntityDelta.fromBefore(before);
-                mEntities.add(entity);
-            }
-            iterator.close();
-        } catch (RemoteException e) {
-            Log.d(TAG, "Problem reading aggregate", e);
-        }
-    }
-
-    protected void rebuildTabs() {
         mTabWidget.removeAllTabs();
-        for (EntityDelta entity : mEntities) {
+        for (EntityDelta entity : mState) {
             final String accountType = entity.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
-            final ContactsSource source = getSourceForEntity(entity);
+            final ContactsSource source = sources.getInflatedSource(accountType,
+                    ContactsSource.LEVEL_CONSTRAINTS);
 
-            final View tabView = createTabView(mTabWidget, source);
+            final View tabView = BaseContactCardActivity.createTabIndicatorView(mTabWidget, source);
             mTabWidget.addTab(tabView);
         }
-        if (mEntities.size() > 0) {
+        if (mState.size() > 0) {
             mTabWidget.setCurrentTab(0);
             this.onTabSelectionChanged(0, false);
         }
     }
 
     /**
-     * Create the {@link View} to represent the given {@link ContactsSource}.
+     * Bind our header based on {@link #mEntities}, which include any edits.
+     * Usually called once {@link Entity} data has been loaded, or after a
+     * primary {@link Data} change.
      */
-    public static View createTabView(ViewGroup parent, ContactsSource source) {
-        final Context context = parent.getContext();
-        final LayoutInflater inflater = (LayoutInflater)context
-                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+    protected void bindHeader() {
+        // TODO: rebuild header widget based on internal entities
 
-        final View tabIndicator = inflater.inflate(R.layout.tab_indicator, parent, false);
-        final TextView titleView = (TextView)tabIndicator.findViewById(R.id.tab_title);
-        final ImageView iconView = (ImageView) tabIndicator.findViewById(R.id.tab_icon);
+        // TODO: fill header bar with newly parsed data for speed
+        // TODO: handle legacy case correctly instead of assuming _id
 
-        if (source.titleRes > 0) {
-            titleView.setText(source.titleRes);
-        }
-        if (source.iconRes > 0) {
-            iconView.setImageResource(source.iconRes);
+        final Uri uri = this.getIntent().getData();
+
+        try {
+            final long contactId = ContentUris.parseId(uri);
+            mHeader.bindFromContactId(contactId);
+        } catch (NumberFormatException e) {
         }
 
-        return tabIndicator;
+//        mHeader.setDisplayName(displayName, phoneticName);
+//        mHeader.setPhoto(bitmap);
     }
 
-    /**
-     * Find the {@link ContactsSource} that describes the structure for the
-     * given {@link EntityDelta}, or null if no matching source found.
-     */
-    private ContactsSource getSourceForEntity(EntityDelta entity) {
-        final String accountType = entity.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
-        ContactsSource source = mSources.getSourceForType(accountType);
-        if (source == null) {
-            // TODO: remove and place "read only" placard when missing
-            source = mSources.getSourceForType(Sources.ACCOUNT_TYPE_GOOGLE);
-        }
-        return source;
-    }
 
 
     /** {@inheritDoc} */
     public void onTabSelectionChanged(int tabIndex, boolean clicked) {
+        boolean validTab = mState != null && tabIndex >= 0 && tabIndex < mState.size();
+        if (!validTab) return;
+
         // Find entity and source for selected tab
-        final EntityDelta entity = mEntities.get(tabIndex);
-        final ContactsSource source = getSourceForEntity(entity);
+        final EntityDelta entity = mState.get(tabIndex);
+        final String accountType = entity.getValues().getAsString(RawContacts.ACCOUNT_TYPE);
+
+        final Sources sources = Sources.getInstance(this);
+        final ContactsSource source = sources.getInflatedSource(accountType,
+                ContactsSource.LEVEL_CONSTRAINTS);
 
         // Assign editor state based on entity and source
         mEditor.setState(entity, source);
@@ -276,6 +390,7 @@
     }
 
 
+    /** {@inheritDoc} */
     @Override
     public boolean onKeyDown(int keyCode, KeyEvent event) {
         switch (keyCode) {
@@ -285,9 +400,7 @@
         return super.onKeyDown(keyCode, event);
     }
 
-
-
-
+    /** {@inheritDoc} */
     @Override
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
         // Ignore failed requests
@@ -295,6 +408,7 @@
 
         switch (requestCode) {
             case PHOTO_PICKED_WITH_DATA: {
+                // TODO: pass back to requesting tab
 //                final Bundle extras = data.getExtras();
 //                if (extras != null) {
 //                    Bitmap photo = extras.getParcelable("data");
@@ -321,7 +435,7 @@
 
     @Override
     public boolean onPrepareOptionsMenu(Menu menu) {
-        // show or hide photo item based on current tab
+        // TODO: show or hide photo items based on current tab
         // hide photo stuff entirely if on read-only source
 
         return true;
@@ -334,6 +448,8 @@
                 return doSaveAction();
             case R.id.menu_discard:
                 return doRevertAction();
+            case R.id.menu_add:
+                return doAddAction();
             case R.id.menu_delete:
                 return doDeleteAction();
             case R.id.menu_photo_add:
@@ -344,37 +460,107 @@
         return false;
     }
 
+
+
+
+
+
+
+
+
+
+
+
+
+
+    /**
+     * Background task for persisting edited contact data, using the changes
+     * defined by a set of {@link EntityDelta}. This task starts
+     * {@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);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected void onPreExecute(Context context) {
+            // Before starting this task, start an empty service to protect our
+            // process from being reclaimed by the system.
+            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
+
+                try {
+                    resolver.applyBatch(ContactsContract.AUTHORITY, diff);
+                } catch (RemoteException e) {
+                    Log.w(TAG, "problem writing rawcontact diff", e);
+                } catch (OperationApplicationException e) {
+                    Log.w(TAG, "problem writing rawcontact diff", e);
+                }
+            }
+
+            return savedChanges;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected void onPostExecute(Context context, Boolean result) {
+            if (result) {
+                Toast.makeText(context, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
+            }
+
+            // Stop the service that was protecting us
+            context.stopService(new Intent(context, EmptyService.class));
+        }
+    }
+
+    /**
+     * Timeout for a {@link PersistTask} running on a background thread. This is
+     * just shorter than the ANR timeout, so that we hold off user interaction
+     * as long as possible.
+     */
+    private static final long TIMEOUT_PERSIST = 4000;
+
     /**
      * Saves or creates the contact based on the mode, and if successful
      * finishes the activity.
      */
     private boolean doSaveAction() {
-        final ContentResolver resolver = this.getContentResolver();
-        boolean savedChanges = false;
-        for (EntityDelta entity : mEntities) {
-            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
-
-            try {
-                Log.d(TAG, "about to persist " + entity.toString());
-                resolver.applyBatch(ContactsContract.AUTHORITY, diff);
-            } catch (RemoteException e) {
-                Log.w(TAG, "problem writing rawcontact diff", e);
-            } catch (OperationApplicationException e) {
-                Log.w(TAG, "problem writing rawcontact diff", e);
-            }
+        try {
+            final PersistTask task = new PersistTask(this);
+            task.execute(mState);
+            task.get(TIMEOUT_PERSIST, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            // Ignore when someone cancels the operation
+        } catch (TimeoutException e) {
+            // Ignore when task is taking too long
+        } catch (ExecutionException e) {
+            // Important exceptions are handled on remote thread
         }
 
-        if (savedChanges) {
-            Toast.makeText(this, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
-        }
-
+        // Persisting finished, or we timed out waiting on it. Either way,
+        // finish this activity, the background task will keep running.
         this.finish();
         return true;
     }
@@ -388,6 +574,15 @@
     }
 
     /**
+     * Create a new {@link RawContacts} which will exist as another
+     * {@link EntityDelta} under the currently edited {@link Contacts}.
+     */
+    private boolean doAddAction() {
+        new AddContactTask(this).execute();
+        return true;
+    }
+
+    /**
      * Delete the entire contact currently being edited, which usually asks for
      * user confirmation before continuing.
      */
@@ -396,20 +591,13 @@
         return true;
     }
 
-    /**
-     * Delete the entire contact currently being edited.
-     */
-    private void onDeleteActionConfirmed() {
-        // TODO: delete entire contact
-    }
-
 
     /**
      * Pick a specific photo to be added under this contact.
      */
     private boolean doPickPhotoAction() {
         try {
-            final Intent intent = getPhotoPickIntent();
+            final Intent intent = ContactsUtils.getPhotoPickIntent();
             startActivityForResult(intent, PHOTO_PICKED_WITH_DATA);
         } catch (ActivityNotFoundException e) {
             new AlertDialog.Builder(EditContactActivity.this).setTitle(R.string.errorDialogTitle)
@@ -419,18 +607,6 @@
         return true;
     }
 
-    public static Intent getPhotoPickIntent() {
-        Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
-        intent.setType("image/*");
-        intent.putExtra("crop", "true");
-        intent.putExtra("aspectX", 1);
-        intent.putExtra("aspectY", 1);
-        intent.putExtra("outputX", 96);
-        intent.putExtra("outputY", 96);
-        intent.putExtra("return-data", true);
-        return intent;
-    }
-
     public boolean doRemovePhotoAction() {
         // TODO: remove photo from current contact
         return true;
@@ -439,6 +615,104 @@
 
 
 
+
+
+    /**
+     * Build dialog that handles adding a new {@link RawContacts} after the user
+     * picks a specific {@link ContactsSource}.
+     */
+    private static class AddContactTask extends
+            WeakAsyncTask<Void, Void, AlertDialog.Builder, EditContactActivity> {
+        public AddContactTask(EditContactActivity target) {
+            super(target);
+        }
+
+        @Override
+        protected AlertDialog.Builder doInBackground(final EditContactActivity target,
+                Void... params) {
+            final Sources sources = Sources.getInstance(target);
+
+            // Wrap our context to inflate list items using correct theme
+            final Context dialogContext = new ContextThemeWrapper(target, android.R.style.Theme_Light);
+            final LayoutInflater dialogInflater = (LayoutInflater)dialogContext
+                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+            final ArrayList<Account> writable = sources.getWritableAccounts();
+            final ArrayAdapter<Account> accountAdapter = new ArrayAdapter<Account>(target,
+                    android.R.layout.simple_list_item_2, writable) {
+                @Override
+                public View getView(int position, View convertView, ViewGroup parent) {
+                    if (convertView == null) {
+                        convertView = dialogInflater.inflate(android.R.layout.simple_list_item_2,
+                                parent, false);
+                    }
+
+                    // TODO: show icon along with title
+                    final TextView text1 = (TextView)convertView.findViewById(android.R.id.text1);
+                    final TextView text2 = (TextView)convertView.findViewById(android.R.id.text2);
+
+                    final Account account = this.getItem(position);
+                    final ContactsSource source = sources.getInflatedSource(account.type,
+                            ContactsSource.LEVEL_SUMMARY);
+                    if (source.titleRes > 0) {
+                        text1.setText(source.titleRes);
+                    }
+                    text2.setText(account.name);
+
+                    return convertView;
+                }
+            };
+
+            final DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() {
+                public void onClick(DialogInterface dialog, int which) {
+                    dialog.dismiss();
+
+                    // Create new contact based on selected source
+                    final Account source = accountAdapter.getItem(which);
+                    final ContentValues values = new ContentValues();
+                    values.put(RawContacts.ACCOUNT_NAME, source.name);
+                    values.put(RawContacts.ACCOUNT_TYPE, source.type);
+
+                    // Tie this directly to existing aggregate
+                    // TODO: this may need to use aggregation exception rules
+                    final long aggregateId = target.mState.getAggregateId();
+                    if (aggregateId >= 0) {
+                        values.put(RawContacts.CONTACT_ID, aggregateId);
+                    }
+
+                    final EntityDelta insert = new EntityDelta(ValuesDelta.fromAfter(values));
+                    target.mState.add(insert);
+
+                    target.bindTabs();
+                    target.bindHeader();
+                }
+            };
+
+            final DialogInterface.OnCancelListener cancelListener = new DialogInterface.OnCancelListener() {
+                public void onCancel(DialogInterface dialog) {
+                    // If nothing remains, close activity
+                    if (target.mState.size() == 0) {
+                        target.finish();
+                    }
+                }
+            };
+
+            // TODO: when canceled and single add, finish()
+            final AlertDialog.Builder builder = new AlertDialog.Builder(target);
+            builder.setTitle(R.string.dialog_new_contact_account);
+            builder.setSingleChoiceItems(accountAdapter, 0, clickListener);
+            builder.setOnCancelListener(cancelListener);
+            return builder;
+        }
+
+        @Override
+        protected void onPostExecute(EditContactActivity target, AlertDialog.Builder result) {
+            result.create().show();
+        }
+    }
+
+
+
     private Dialog createDeleteDialog() {
         final AlertDialog.Builder builder = new AlertDialog.Builder(this);
         builder.setTitle(R.string.deleteConfirmation_title);
@@ -446,7 +720,12 @@
         builder.setMessage(R.string.deleteConfirmation);
         builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
             public void onClick(DialogInterface dialog, int which) {
-                onDeleteActionConfirmed();
+                // Mark the currently selected contact for deletion
+                final int index = mTabWidget.getCurrentTab();
+                final EntityDelta delta = mState.get(index);
+                delta.markDeleted();
+
+                // TODO: trigger task to update tabs (doesnt need to be background)
             }
         });
         builder.setNegativeButton(android.R.string.cancel, null);
@@ -465,7 +744,7 @@
     private Dialog createNameDialog() {
         // Build set of all available display names
         final ArrayList<ValuesDelta> allNames = new ArrayList<ValuesDelta>();
-        for (EntityDelta entity : this.mEntities) {
+        for (EntityDelta entity : mState) {
             final ArrayList<ValuesDelta> displayNames = entity
                     .getMimeEntries(StructuredName.CONTENT_ITEM_TYPE);
             allNames.addAll(displayNames);
@@ -473,15 +752,16 @@
 
         // Wrap our context to inflate list items using correct theme
         final Context dialogContext = new ContextThemeWrapper(this, android.R.style.Theme_Light);
-        final LayoutInflater dialogInflater = this.getLayoutInflater().cloneInContext(dialogContext);
+        final LayoutInflater dialogInflater = this.getLayoutInflater()
+                .cloneInContext(dialogContext);
 
         final ListAdapter nameAdapter = new ArrayAdapter<ValuesDelta>(this,
                 android.R.layout.simple_list_item_1, allNames) {
             @Override
             public View getView(int position, View convertView, ViewGroup parent) {
                 if (convertView == null) {
-                    convertView = dialogInflater.inflate(
-                            android.R.layout.simple_expandable_list_item_1, parent, false);
+                    convertView = dialogInflater.inflate(android.R.layout.simple_list_item_1,
+                            parent, false);
                 }
 
                 final ValuesDelta structuredName = this.getItem(position);
@@ -502,8 +782,8 @@
                 structuredName.put(Data.IS_PRIMARY, 1);
                 structuredName.put(Data.IS_SUPER_PRIMARY, 1);
 
-                // TODO: include last social snippet after update
-                final String displayName = structuredName.getAsString(StructuredName.DISPLAY_NAME);
+                // Update header based on edited values
+                bindHeader();
             }
         };
 
diff --git a/src/com/android/contacts/ui/FastTrackWindow.java b/src/com/android/contacts/ui/FastTrackWindow.java
index 9f4347e..1463983 100644
--- a/src/com/android/contacts/ui/FastTrackWindow.java
+++ b/src/com/android/contacts/ui/FastTrackWindow.java
@@ -15,12 +15,11 @@
 
 package com.android.contacts.ui;
 
-import com.android.contacts.NotifyingAsyncQueryHandler;
 import com.android.contacts.R;
-import com.android.contacts.NotifyingAsyncQueryHandler.AsyncQueryListener;
 import com.android.contacts.model.ContactsSource;
 import com.android.contacts.model.Sources;
 import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.util.NotifyingAsyncQueryHandler;
 import com.android.internal.policy.PolicyManager;
 
 import android.content.ActivityNotFoundException;
@@ -61,7 +60,6 @@
 import android.view.ViewStub;
 import android.view.Window;
 import android.view.WindowManager;
-import android.view.View.OnClickListener;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.animation.Animation;
 import android.view.animation.AnimationUtils;
@@ -84,7 +82,8 @@
  * Window that shows fast-track contact details for a specific
  * {@link Contacts#_ID}.
  */
-public class FastTrackWindow implements Window.Callback, AsyncQueryListener, OnClickListener,
+public class FastTrackWindow implements Window.Callback,
+        NotifyingAsyncQueryHandler.AsyncQueryListener, View.OnClickListener,
         AbsListView.OnItemClickListener {
     private static final String TAG = "FastTrackWindow";
 
@@ -710,8 +709,8 @@
     private void handleData(Cursor cursor) {
         if (cursor == null) return;
 
-        final ContactsSource defaultSource = Sources.getPartialInstance(mContext).getSourceForType(
-                Sources.ACCOUNT_TYPE_GOOGLE);
+        // TODO: turn this into background async instead of blocking ui
+        final Sources sources = Sources.getInstance(mContext);
 
         {
             // Add the profile shortcut action
@@ -740,7 +739,10 @@
 
             // TODO: find the ContactsSource for this, either from accountType,
             // or through lazy-loading when resPackage is set, or default.
-            final ContactsSource source = defaultSource;
+
+            // TODO: move source inflation to background thread so we don't block UI
+            final ContactsSource source = sources.getInflatedSource(accountType,
+                    ContactsSource.LEVEL_MIMETYPES);
             final DataKind kind = source.getKindForMimetype(mimeType);
 
             if (kind != null) {
@@ -999,7 +1001,23 @@
     public void onWindowFocusChanged(boolean hasFocus) {
     }
 
+    /** {@inheritDoc} */
+    public void onDeleteComplete(int token, Object cookie, int result) {
+        // No actions
+    }
+
+    /** {@inheritDoc} */
+    public void onInsertComplete(int token, Object cookie, Uri uri) {
+        // No actions
+    }
+
+    /** {@inheritDoc} */
     public void onQueryEntitiesComplete(int token, Object cookie, EntityIterator iterator) {
-        //Empty
+        // No actions
+    }
+
+    /** {@inheritDoc} */
+    public void onUpdateComplete(int token, Object cookie, int result) {
+        // No actions
     }
 }
diff --git a/src/com/android/contacts/ui/widget/ContactEditorView.java b/src/com/android/contacts/ui/widget/ContactEditorView.java
index 7925bb8..8cbe298 100644
--- a/src/com/android/contacts/ui/widget/ContactEditorView.java
+++ b/src/com/android/contacts/ui/widget/ContactEditorView.java
@@ -17,13 +17,13 @@
 package com.android.contacts.ui.widget;
 
 import com.android.contacts.R;
+import com.android.contacts.model.ContactsSource;
 import com.android.contacts.model.EntityDelta;
 import com.android.contacts.model.EntityModifier;
-import com.android.contacts.model.ContactsSource;
-import com.android.contacts.model.EntityDelta.ValuesDelta;
 import com.android.contacts.model.ContactsSource.DataKind;
 import com.android.contacts.model.ContactsSource.EditField;
 import com.android.contacts.model.ContactsSource.EditType;
+import com.android.contacts.model.EntityDelta.ValuesDelta;
 
 import android.app.AlertDialog;
 import android.app.Dialog;
@@ -38,9 +38,10 @@
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.CommonDataKinds.Photo;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.text.Editable;
+import android.text.TextUtils;
 import android.text.TextWatcher;
-import android.util.Log;
 import android.view.ContextThemeWrapper;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -70,7 +71,7 @@
     private static final int RES_CONTENT = R.layout.act_edit_contact;
 
     private PhotoEditor mPhoto;
-    private DisplayNameEditor mDisplayName;
+    private StructuredNameEditor mDisplayName;
 
     private ViewGroup mGeneral;
     private ViewGroup mSecondary;
@@ -98,7 +99,7 @@
         mPhoto = new PhotoEditor(context);
         mPhoto.swapInto((ViewGroup)mContent.findViewById(R.id.hook_photo));
 
-        mDisplayName = new DisplayNameEditor(context);
+        mDisplayName = new StructuredNameEditor(context);
         mDisplayName.swapInto((ViewGroup)mContent.findViewById(R.id.hook_displayname));
     }
 
@@ -163,7 +164,8 @@
      * {@link DataKind} around a {@link Data#MIMETYPE}. This view shows a
      * section header and a trigger for adding new {@link Data} rows.
      */
-    protected static class KindSection extends ViewHolder implements OnClickListener, EditorListener {
+    protected static class KindSection extends ViewHolder implements OnClickListener,
+            EditorListener {
         private static final int RES_SECTION = R.layout.item_edit_kind;
 
         private ViewGroup mEditors;
@@ -258,6 +260,12 @@
          * Add a specific {@link EditorListener} to this {@link Editor}.
          */
         public void setEditorListener(EditorListener listener);
+
+        /**
+         * Called internally when the contents of a specific field have changed,
+         * allowing advanced editors to persist data in a specific way.
+         */
+        public void onFieldChanged(String column, String value);
     }
 
     /**
@@ -276,22 +284,22 @@
      * {@link Entity} values, and to correctly write any changes values.
      */
     protected static class GenericEditor extends ViewHolder implements Editor, View.OnClickListener {
-        private static final int RES_EDITOR = R.layout.item_editor;
-        private static final int RES_FIELD = R.layout.item_editor_field;
-        private static final int RES_LABEL_ITEM = android.R.layout.simple_list_item_1;
+        protected static final int RES_EDITOR = R.layout.item_editor;
+        protected static final int RES_FIELD = R.layout.item_editor_field;
+        protected static final int RES_LABEL_ITEM = android.R.layout.simple_list_item_1;
 
-        private static final int INPUT_TYPE_CUSTOM = EditorInfo.TYPE_CLASS_TEXT
+        protected static final int INPUT_TYPE_CUSTOM = EditorInfo.TYPE_CLASS_TEXT
                 | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
 
-        private TextView mLabel;
-        private ViewGroup mFields;
-        private View mDelete;
+        protected TextView mLabel;
+        protected ViewGroup mFields;
+        protected View mDelete;
 
-        private DataKind mKind;
-        private ValuesDelta mEntry;
-        private EntityDelta mState;
+        protected DataKind mKind;
+        protected ValuesDelta mEntry;
+        protected EntityDelta mState;
 
-        private EditType mType;
+        protected EditType mType;
 
         public GenericEditor(Context context) {
             super(context, RES_EDITOR);
@@ -305,7 +313,7 @@
             mDelete.setOnClickListener(this);
         }
 
-        private EditorListener mListener;
+        protected EditorListener mListener;
 
         public void setEditorListener(EditorListener listener) {
             mListener = listener;
@@ -335,6 +343,16 @@
             mLabel.setText(mType.labelRes);
         }
 
+        /** {@inheritDoc} */
+        public void onFieldChanged(String column, String value) {
+            // Field changes are saved directly
+            mEntry.put(column, value);
+        }
+
+        /**
+         * Prepare this editor using the given {@link DataKind} for defining
+         * structure and {@link ValuesDelta} describing the content to edit.
+         */
         public void setValues(DataKind kind, ValuesDelta entry, EntityDelta state) {
             mKind = kind;
             mEntry = entry;
@@ -361,7 +379,7 @@
             for (EditField field : kind.fieldList) {
                 // Inflate field from definition
                 EditText fieldView = (EditText)mInflater.inflate(RES_FIELD, mFields, false);
-                if (field.titleRes != -1) {
+                if (field.titleRes > 0) {
                     fieldView.setHint(field.titleRes);
                 }
                 fieldView.setInputType(field.inputType);
@@ -375,8 +393,8 @@
                 // Prepare listener for writing changes
                 fieldView.addTextChangedListener(new TextWatcher() {
                     public void afterTextChanged(Editable s) {
-                        // Write the newly changed value
-                        mEntry.put(column, s.toString());
+                        // Trigger event for newly changed value
+                        onFieldChanged(column, s.toString());
                     }
 
                     public void beforeTextChanged(CharSequence s, int start, int count, int after) {
@@ -387,7 +405,7 @@
                 });
 
                 // Hide field when empty and optional value
-                boolean shouldHide = (value == null && field.optional);
+                boolean shouldHide = (TextUtils.isEmpty(value) && field.optional);
                 fieldView.setVisibility(shouldHide ? View.GONE : View.VISIBLE);
 
                 mFields.addView(fieldView);
@@ -498,6 +516,26 @@
     }
 
     /**
+     * Specific editor for {@link StructuredPostal} addresses that flattens any
+     * user changes into {@link StructuredPostal#FORMATTED_ADDRESS} so data
+     * consistency is maintained.
+     */
+    protected static class PostalEditor extends GenericEditor {
+        public PostalEditor(Context context) {
+            super(context);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void onFieldChanged(String column, String value) {
+            super.onFieldChanged(column, value);
+
+            // TODO: flatten the structured values into unstructured
+            mEntry.put(StructuredPostal.FORMATTED_ADDRESS, null);
+        }
+    }
+
+    /**
      * Simple editor for {@link Photo}.
      */
     protected static class PhotoEditor extends ViewHolder implements Editor {
@@ -512,6 +550,11 @@
             mPhoto = (ImageView)mContent;
         }
 
+        /** {@inheritDoc} */
+        public void onFieldChanged(String column, String value) {
+            throw new UnsupportedOperationException("Photos don't support direct field changes");
+        }
+
         public void setValues(DataKind kind, ValuesDelta values, EntityDelta state) {
             mEntry = values;
             if (values != null) {
@@ -544,40 +587,59 @@
     /**
      * Simple editor for {@link StructuredName}.
      */
-    protected static class DisplayNameEditor extends ViewHolder implements Editor {
+    protected static class StructuredNameEditor extends ViewHolder implements Editor {
         private static final int RES_DISPLAY_NAME = R.layout.item_editor_displayname;
 
         private EditText mName;
         private ValuesDelta mEntry;
 
-        public DisplayNameEditor(Context context) {
+        public StructuredNameEditor(Context context) {
             super(context, RES_DISPLAY_NAME);
 
             mName = (EditText)mContent.findViewById(R.id.name);
         }
 
+        /** {@inheritDoc} */
+        public void onFieldChanged(String column, String value) {
+            if (!StructuredName.DISPLAY_NAME.equals(column)) {
+                throw new IllegalArgumentException("StructuredName editor only "
+                        + "supports updating through DISPLAY_NAME field");
+
+            }
+
+            // Field changes are saved directly
+            mEntry.put(column, value);
+
+            // TODO: split into structured fields using NameSplitter
+        }
+
+        private TextWatcher mWatcher = new TextWatcher() {
+            public void afterTextChanged(Editable s) {
+                // Trigger event for newly changed value
+                onFieldChanged(StructuredName.DISPLAY_NAME, s.toString());
+            }
+
+            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+            }
+
+            public void onTextChanged(CharSequence s, int start, int before, int count) {
+            }
+        };
+
         public void setValues(DataKind kind, ValuesDelta values, EntityDelta state) {
             mEntry = values;
             if (values == null) {
                 // Invalid display name, so reset and skip
+                // TODO: should always have a display name?
+                mName.removeTextChangedListener(mWatcher);
                 mName.setText(null);
                 return;
             }
 
             final String displayName = values.getAsString(StructuredName.DISPLAY_NAME);
+            mName.removeTextChangedListener(mWatcher);
             mName.setText(displayName);
-            mName.addTextChangedListener(new TextWatcher() {
-                public void afterTextChanged(Editable s) {
-                    // Write the newly changed value
-                    mEntry.put(StructuredName.DISPLAY_NAME, s.toString());
-                }
-
-                public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-                }
-
-                public void onTextChanged(CharSequence s, int start, int before, int count) {
-                }
-            });
+            mName.addTextChangedListener(mWatcher);
         }
 
         public void setEditorListener(EditorListener listener) {
diff --git a/src/com/android/contacts/util/EmptyService.java b/src/com/android/contacts/util/EmptyService.java
new file mode 100644
index 0000000..2e6a159
--- /dev/null
+++ b/src/com/android/contacts/util/EmptyService.java
@@ -0,0 +1,33 @@
+/*
+ * 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.content.Intent;
+import android.os.IBinder;
+
+/**
+ * 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 EmptyService extends Service {
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null;
+    }
+}
diff --git a/src/com/android/contacts/NotifyingAsyncQueryHandler.java b/src/com/android/contacts/util/NotifyingAsyncQueryHandler.java
similarity index 64%
rename from src/com/android/contacts/NotifyingAsyncQueryHandler.java
rename to src/com/android/contacts/util/NotifyingAsyncQueryHandler.java
index 0eff1ac..795ac79 100644
--- a/src/com/android/contacts/NotifyingAsyncQueryHandler.java
+++ b/src/com/android/contacts/util/NotifyingAsyncQueryHandler.java
@@ -1,4 +1,20 @@
-package com.android.contacts;
+/*
+ * 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.content.AsyncQueryHandler;
 import android.content.Context;
@@ -8,11 +24,14 @@
 import java.lang.ref.WeakReference;
 
 /**
- * Slightly more abstract {@link android.content.AsyncQueryHandler} that helps
- * keep a {@link WeakReference} back to a callback interface. Will properly
- * close the completed query if the listener ceases to exist.
+ * Slightly more abstract {@link AsyncQueryHandler} that helps keep a
+ * {@link WeakReference} back to a listener. Will properly close any
+ * {@link Cursor} or {@link EntityIterator} if the listener ceases to exist.
  * <p>
- * Using this pattern will help keep you from leaking a {@link Context}.
+ * This pattern can be used to perform background queries without leaking
+ * {@link Context} objects.
+ *
+ * @hide pending API council review
  */
 public class NotifyingAsyncQueryHandler extends AsyncQueryHandler {
     private WeakReference<AsyncQueryListener> mListener;
diff --git a/src/com/android/contacts/util/WeakAsyncTask.java b/src/com/android/contacts/util/WeakAsyncTask.java
new file mode 100644
index 0000000..f60cfd7
--- /dev/null
+++ b/src/com/android/contacts/util/WeakAsyncTask.java
@@ -0,0 +1,69 @@
+/*
+ * 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.os.AsyncTask;
+
+import java.lang.ref.WeakReference;
+
+public abstract class WeakAsyncTask<Params, Progress, Result, WeakTarget> extends
+        AsyncTask<Params, Progress, Result> {
+    protected WeakReference<WeakTarget> mTarget;
+
+    public WeakAsyncTask(WeakTarget target) {
+        mTarget = new WeakReference<WeakTarget>(target);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected final void onPreExecute() {
+        final WeakTarget target = mTarget.get();
+        if (target != null) {
+            this.onPreExecute(target);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected final Result doInBackground(Params... params) {
+        final WeakTarget target = mTarget.get();
+        if (target != null) {
+            return this.doInBackground(target, params);
+        } else {
+            return null;
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected final void onPostExecute(Result result) {
+        final WeakTarget target = mTarget.get();
+        if (target != null) {
+            this.onPostExecute(target, result);
+        }
+    }
+
+    protected void onPreExecute(WeakTarget target) {
+        // No default action
+    }
+
+    protected abstract Result doInBackground(WeakTarget target, Params... params);
+
+    protected void onPostExecute(WeakTarget target, Result result) {
+        // No default action
+    }
+}