Split sources, partial inflation, third-party support.

This change splits HardCodedSources into separate classes
so they could borrow helpers from fallback.  This also
finishes up FallbackSource so it handles all new types
supported by ContactsContract.  The view inflaters now
handle showing all types, even if not supported as a choice
during edit.  This approach also allows us to partially
inflate sources, speeding up view and FastTrack.  Fixes
http://b/2116999 and http://b/2126675 and makes progress
towards fixing http://b/2134623

This change also fixes on-phone contacts, meaning we
always have fallback sources, fixing http://b/2119637 and
http://b/2123401

Repurpose code from StyleManager for Sources inflation of
third-party data sources, fixing http://b/2126691

Fix FastTrack chicklet bug so we uncheck when switching
between tabs.  Since all types are in framework, we borrow
those strings here, and also clean up our descriptions for
translation.
diff --git a/res/layout-finger/fasttrack_item.xml b/res/layout-finger/fasttrack_item.xml
index e820a03..58d85aa 100644
--- a/res/layout-finger/fasttrack_item.xml
+++ b/res/layout-finger/fasttrack_item.xml
@@ -14,7 +14,7 @@
      limitations under the License.
 -->
 
-<com.android.contacts.ui.CheckableImageView 
+<com.android.contacts.ui.widget.CheckableImageView
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="59dip"
     android:layout_height="52dip"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 4de02ba..f358592 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -887,47 +887,25 @@
     <!-- Toast indicating that sharing a contact has failed. -->
     <string name="share_error">This contact cannot be shared.</string>
 
+    <!-- Header that expands to list all name types when editing a structured name of a contact -->
+    <string name="nameLabelsGroup">Name</string>
+    <!-- Header that expands to list all nickname types when editing a nickname of a contact -->
+    <string name="nicknameLabelsGroup">Nickname</string>
+    <!-- Header that expands to list all organization types when editing an organization of a contact -->
+    <string name="organizationLabelsGroup">Organization</string>
+    <!-- Header that expands to list all website types when editing a website of a contact -->
+    <string name="websiteLabelsGroup">Website</string>
 
-
-<!-- TODO: add comments to each of these strings to prepare for translation -->
-<string name="nameLabelsGroup">Name</string>
-<string name="nicknameLabelsGroup">Nickname</string>
-<string name="organizationLabelsGroup">Organization</string>
-<string name="websiteLabelsGroup">Website</string>
-
-<string name="type_home">Home</string>
-<string name="type_mobile">Mobile</string>
-<string name="type_work">Work</string>
-<string name="type_fax_work">Work Fax</string>
-<string name="type_fax_home">Home Fax</string>
-<string name="type_pager">Pager</string>
-<string name="type_other">Other</string>
-<string name="type_custom">Custom</string>
-
-<!-- Single-character overlays on shortcut icons -->
-<string name="type_short_home">H</string>
-<string name="type_short_mobile">M</string>
-<string name="type_short_work">W</string>
-<string name="type_short_pager">P</string>
-<string name="type_short_other">O</string>
-
-<!-- exchange specific -->
-<string name="type_home_2">Home 2</string>
-<string name="type_work_2">Work 2</string>
-<string name="type_car">Car</string>
-<string name="type_company_main">Company Main</string>
-<string name="type_mms">MMS</string>
-<string name="type_radio">Radio</string>
-<string name="type_assistant">Assistant</string>
-
-<string name="type_im_aim">AIM</string>
-<string name="type_im_msn">Windows Live</string>
-<string name="type_im_yahoo">Yahoo</string>
-<string name="type_im_skype">Skype</string>
-<string name="type_im_qq">QQ</string>
-<string name="type_im_google_talk">Google Talk</string>
-<string name="type_im_icq">ICQ</string>
-<string name="type_im_jabber">Jabber</string>
+    <!-- Single-character overlay for home phone numbers when creating desktop shortcuts -->
+    <string name="type_short_home">H</string>
+    <!-- Single-character overlay for mobile phone numbers when creating desktop shortcuts -->
+    <string name="type_short_mobile">M</string>
+    <!-- Single-character overlay for work phone numbers when creating desktop shortcuts -->
+    <string name="type_short_work">W</string>
+    <!-- Single-character overlay for pager phone numbers when creating desktop shortcuts -->
+    <string name="type_short_pager">P</string>
+    <!-- Single-character overlay for other phone numbers when creating desktop shortcuts -->
+    <string name="type_short_other">O</string>
 
     <!-- Shown as the header title over a collapsible section that, by default, hides
          secondary contact detail edit fields, such as birthday. -->
@@ -957,89 +935,165 @@
          user to give a specific name to describe this address. -->
     <string name="label_email_display_name">Display name</string>
 
-<string name="call_home">Call home</string>
-<string name="call_mobile">Call mobile</string>
-<string name="call_work">Call work</string>
-<string name="call_fax_work">Call work fax</string>
-<string name="call_fax_home">Call home fax</string>
-<string name="call_pager">Call pager</string>
-<string name="call_other">Call other</string>
-<string name="call_custom">Call <xliff:g id="custom">%s</xliff:g></string>
+    <!-- Action string for calling a custom phone number -->
+    <string name="call_custom">Call <xliff:g id="custom">%s</xliff:g></string>
+    <!-- Action string for calling a home phone number -->
+    <string name="call_home">Call home</string>
+    <!-- Action string for calling a mobile phone number -->
+    <string name="call_mobile">Call mobile</string>
+    <!-- Action string for calling a work phone number -->
+    <string name="call_work">Call work</string>
+    <!-- Action string for calling a work fax phone number -->
+    <string name="call_fax_work">Call work fax</string>
+    <!-- Action string for calling a home fax phone number -->
+    <string name="call_fax_home">Call home fax</string>
+    <!-- Action string for calling a pager phone number -->
+    <string name="call_pager">Call pager</string>
+    <!-- Action string for calling an other phone number -->
+    <string name="call_other">Call other</string>
+    <!-- Action string for calling a callback number -->
+    <string name="call_callback">Call callback</string>
+    <!-- Action string for calling a car phone number -->
+    <string name="call_car">Call car</string>
+    <!-- Action string for calling a company main phone number -->
+    <string name="call_company_main">Call company main</string>
+    <!-- Action string for calling a ISDN phone number -->
+    <string name="call_isdn">Call ISDN</string>
+    <!-- Action string for calling a main phone number -->
+    <string name="call_main">Call main</string>
+    <!-- Action string for calling an other fax phone number -->
+    <string name="call_other_fax">Call other fax</string>
+    <!-- Action string for calling a radio phone number -->
+    <string name="call_radio">Call radio</string>
+    <!-- Action string for calling a Telex phone number -->
+    <string name="call_telex">Call telex</string>
+    <!-- Action string for calling a TTY/TDD phone number -->
+    <string name="call_tty_tdd">Call TTY/TDD</string>
+    <!-- Action string for calling a work mobile phone number -->
+    <string name="call_work_mobile">Call work mobile</string>
+    <!-- Action string for calling a work pager phone number -->
+    <string name="call_work_pager">Call work pager</string>
+    <!-- Action string for calling an assistant phone number -->
+    <string name="call_assistant">Call <xliff:g id="assistant">%s</xliff:g></string>
+    <!-- Action string for calling a MMS phone number -->
+    <string name="call_mms">Call MMS</string>
 
-<!-- exchange specific -->
-<string name="call_home_2">Call home 2</string>
-<string name="call_work_2">Call work 2</string>
-<string name="call_car">Call car</string>
-<string name="call_company_main">Call company main</string>
-<string name="call_mms">Call MMS</string>
-<string name="call_radio">Call radio</string>
+    <!-- Action string for sending an SMS to a custom phone number -->
+    <string name="sms_custom">Text <xliff:g id="custom">%s</xliff:g></string>
+    <!-- Action string for sending an SMS to a home phone number -->
+    <string name="sms_home">Text home</string>
+    <!-- Action string for sending an SMS to a mobile phone number -->
+    <string name="sms_mobile">Text mobile</string>
+    <!-- Action string for sending an SMS to a work phone number -->
+    <string name="sms_work">Text work</string>
+    <!-- Action string for sending an SMS to a work fax phone number -->
+    <string name="sms_fax_work">Text work fax</string>
+    <!-- Action string for sending an SMS to a home fax phone number -->
+    <string name="sms_fax_home">Text home fax</string>
+    <!-- Action string for sending an SMS to a pager phone number -->
+    <string name="sms_pager">Text pager</string>
+    <!-- Action string for sending an SMS to an other phone number -->
+    <string name="sms_other">Text other</string>
+    <!-- Action string for sending an SMS to a callback number -->
+    <string name="sms_callback">Text callback</string>
+    <!-- Action string for sending an SMS to a car phone number -->
+    <string name="sms_car">Text car</string>
+    <!-- Action string for sending an SMS to a company main phone number -->
+    <string name="sms_company_main">Text company main</string>
+    <!-- Action string for sending an SMS to a ISDN phone number -->
+    <string name="sms_isdn">Text ISDN</string>
+    <!-- Action string for sending an SMS to a main phone number -->
+    <string name="sms_main">Text main</string>
+    <!-- Action string for sending an SMS to an other fax phone number -->
+    <string name="sms_other_fax">Text other fax</string>
+    <!-- Action string for sending an SMS to a radio phone number -->
+    <string name="sms_radio">Text radio</string>
+    <!-- Action string for sending an SMS to a Telex phone number -->
+    <string name="sms_telex">Text telex</string>
+    <!-- Action string for sending an SMS to a TTY/TDD phone number -->
+    <string name="sms_tty_tdd">Text TTY/TDD</string>
+    <!-- Action string for sending an SMS to a work mobile phone number -->
+    <string name="sms_work_mobile">Text work mobile</string>
+    <!-- Action string for sending an SMS to a work pager phone number -->
+    <string name="sms_work_pager">Text work pager</string>
+    <!-- Action string for sending an SMS to an assistant phone number -->
+    <string name="sms_assistant">Text <xliff:g id="assistant">%s</xliff:g></string>
+    <!-- Action string for sending an SMS to a MMS phone number -->
+    <string name="sms_mms">Text MMS</string>
 
+    <!-- Action string for sending an email to a home email address -->
+    <string name="email_home">Email home</string>
+    <!-- Action string for sending an email to a mobile email address -->
+    <string name="email_mobile">Email mobile</string>
+    <!-- Action string for sending an email to a work email address -->
+    <string name="email_work">Email work</string>
+    <!-- Action string for sending an email to an other email address -->
+    <string name="email_other">Email other</string>
+    <!-- Action string for sending an email to a custom email address -->
+    <string name="email_custom">Email <xliff:g id="custom">%s</xliff:g></string>
 
+    <!-- Generic action string for sending an email -->
+    <string name="email">Email</string>
 
+    <!-- Action string for viewing a home postal address -->
+    <string name="map_home">View home address</string>
+    <!-- Action string for viewing a work postal address -->
+    <string name="map_work">View work address</string>
+    <!-- Action string for viewing an other postal address -->
+    <string name="map_other">View other address</string>
+    <!-- Action string for viewing a custom postal address -->
+    <string name="map_custom">View <xliff:g id="custom">%s</xliff:g> address</string>
 
+    <!-- Action string for starting an IM chat with the AIM protocol -->
+    <string name="chat_aim">Chat using AIM</string>
+    <!-- Action string for starting an IM chat with the MSN or Windows Live protocol -->
+    <string name="chat_msn">Chat using Windows Live</string>
+    <!-- Action string for starting an IM chat with the Yahoo protocol -->
+    <string name="chat_yahoo">Chat using Yahoo</string>
+    <!-- Action string for starting an IM chat with the Skype protocol -->
+    <string name="chat_skype">Chat using Skype</string>
+    <!-- Action string for starting an IM chat with the QQ protocol -->
+    <string name="chat_qq">Chat using QQ</string>
+    <!-- Action string for starting an IM chat with the Google Talk protocol -->
+    <string name="chat_gtalk">Chat using Google Talk</string>
+    <!-- Action string for starting an IM chat with the ICQ protocol -->
+    <string name="chat_icq">Chat using ICQ</string>
+    <!-- Action string for starting an IM chat with the Jabber protocol -->
+    <string name="chat_jabber">Chat using Jabber</string>
 
-<string name="sms_home">Text home</string>
-<string name="sms_mobile">Text mobile</string>
-<string name="sms_work">Text work</string>
-<string name="sms_fax_work">Text work fax</string>
-<string name="sms_fax_home">Text home fax</string>
-<string name="sms_pager">Text pager</string>
-<string name="sms_other">Text other</string>
-<string name="sms_custom">Text <xliff:g id="custom">%s</xliff:g></string>
+    <!-- Generic action string for starting an IM chat -->
+    <string name="chat">Chat</string>
 
-<!-- exchange specific -->
-<string name="sms_home_2">Text home 2</string>
-<string name="sms_work_2">Text work 2</string>
-<string name="sms_car">Text car</string>
-<string name="sms_company_main">Text company main</string>
-<string name="sms_mms">Text MMS</string>
-<string name="sms_radio">Text radio</string>
+    <!-- Field title for the street of a structured postal address of a contact -->
+    <string name="postal_street">Street</string>
+    <!-- Field title for the PO box of a structured postal address of a contact -->
+    <string name="postal_pobox">PO box</string>
+    <!-- Field title for the neighborhood of a structured postal address of a contact -->
+    <string name="postal_neighborhood">Neighborhood</string>
+    <!-- Field title for the city of a structured postal address of a contact -->
+    <string name="postal_city">City</string>
+    <!-- Field title for the region, or state, of a structured postal address of a contact -->
+    <string name="postal_region">State</string>
+    <!-- Field title for the postal code of a structured postal address of a contact -->
+    <string name="postal_postcode">ZIP code</string>
+    <!-- Field title for the country of a structured postal address of a contact -->
+    <string name="postal_country">Country</string>
 
-
-
-<string name="email_home">Email home</string>
-<string name="email_mobile">Email mobile</string>
-<string name="email_work">Email work</string>
-<string name="email_other">Email other</string>
-<string name="email_custom">Email <xliff:g id="custom">%s</xliff:g></string>
-
-<string name="email">Email</string>
-
-
-
-<string name="map_home">View home address</string>
-<string name="map_work">View work address</string>
-<string name="map_other">View other address</string>
-<string name="map_custom">View <xliff:g id="custom">%s</xliff:g> address</string>
-
-<string name="chat_aim">Chat using AIM</string>
-<string name="chat_msn">Chat using Windows Live</string>
-<string name="chat_yahoo">Chat using Yahoo</string>
-<string name="chat_skype">Chat using Skype</string>
-<string name="chat_qq">Chat using QQ</string>
-<string name="chat_gtalk">Chat using Google Talk</string>
-<string name="chat_icq">Chat using ICQ</string>
-<string name="chat_jabber">Chat using Jabber</string>
-<string name="chat_other">Chat</string>
-
-<string name="postal_street">Street</string>
-<string name="postal_pobox">PO box</string>
-<string name="postal_neighborhood">Neighborhood</string>
-<string name="postal_city">City</string>
-<string name="postal_region">State</string>
-<string name="postal_postcode">ZIP code</string>
-<string name="postal_country">Country</string>
-
-
-<string name="name_given">Given name</string>
-<string name="name_family">Family name</string>
-<string name="name_prefix">Name prefix</string>
-<string name="name_middle">Middle name</string>
-<string name="name_suffix">Name suffix</string>
-<string name="name_phonetic_given">Phonetic given name</string>
-<string name="name_phonetic_middle">Phonetic middle name</string>
-<string name="name_phonetic_family">Phonetic family name</string>
-
-
+    <!-- Field title for the given name of a contact -->
+    <string name="name_given">Given name</string>
+    <!-- Field title for the family name of a contact -->
+    <string name="name_family">Family name</string>
+    <!-- Field title for the prefix name of a contact -->
+    <string name="name_prefix">Name prefix</string>
+    <!-- Field title for the middle name of a contact -->
+    <string name="name_middle">Middle name</string>
+    <!-- Field title for the suffix name of a contact -->
+    <string name="name_suffix">Name suffix</string>
+    <!-- Field title for the phonetic given name of a contact -->
+    <string name="name_phonetic_given">Phonetic given name</string>
+    <!-- Field title for the phonetic middle name of a contact -->
+    <string name="name_phonetic_middle">Phonetic middle name</string>
+    <!-- Field title for the phonetic family name of a contact -->
+    <string name="name_phonetic_family">Phonetic family name</string>
 
 </resources>
diff --git a/src/com/android/contacts/ContactsListActivity.java b/src/com/android/contacts/ContactsListActivity.java
index 1b14aad..5d47ee4 100644
--- a/src/com/android/contacts/ContactsListActivity.java
+++ b/src/com/android/contacts/ContactsListActivity.java
@@ -23,6 +23,7 @@
 import com.android.contacts.ui.DisplayGroupsActivity;
 import com.android.contacts.ui.DisplayGroupsActivity.Prefs;
 import com.android.contacts.util.Constants;
+import com.google.android.collect.Lists;
 
 import android.accounts.Account;
 import android.app.Activity;
@@ -1945,7 +1946,6 @@
         private String mAlphabet;
         private boolean mLoading = true;
         private CharSequence mUnknownNameText;
-        private SparseArray<Integer> mLocalizedLabels;
         private boolean mDisplayPhotos = false;
         private boolean mDisplayCallButton = false;
         private boolean mDisplayAdditionalData = true;
@@ -1969,19 +1969,13 @@
             switch (mMode) {
                 case MODE_LEGACY_PICK_POSTAL:
                 case MODE_PICK_POSTAL:
-                    mLocalizedLabels = inflateLocalizedLabels(
-                            CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE);
                     mDisplaySectionHeaders = false;
                     break;
                 case MODE_LEGACY_PICK_PHONE:
                 case MODE_PICK_PHONE:
                     mDisplaySectionHeaders = false;
-                    mLocalizedLabels = inflateLocalizedLabels(
-                            CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
                     break;
                 default:
-                    mLocalizedLabels = inflateLocalizedLabels(
-                            CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
                     break;
             }
 
@@ -2013,29 +2007,6 @@
             }
         }
 
-        private SparseArray<Integer> inflateLocalizedLabels(String mimetype) {
-            SparseArray<Integer> localizedLabels = new SparseArray<Integer>();
-
-            Sources sources = Sources.getInstance(ContactsListActivity.this);
-
-            ContactsSource contactsSource = sources.getInflatedSource(null /*get fallback type*/,
-                    ContactsSource.LEVEL_MIMETYPES);
-            if (contactsSource == null) {
-                return localizedLabels;
-            }
-
-            DataKind kind = contactsSource.getKindForMimetype(mimetype);
-            if (kind == null) {
-                return localizedLabels;
-            }
-
-            for (EditType type : kind.typeList) {
-                localizedLabels.put(type.rawValue, type.labelRes);
-            }
-
-            return localizedLabels;
-        }
-
         private class ImageFetchHandler extends Handler {
 
             @Override
@@ -2371,18 +2342,15 @@
             // Set the label.
             if (!cursor.isNull(typeColumnIndex)) {
                 labelView.setVisibility(View.VISIBLE);
-                int type = cursor.getInt(typeColumnIndex);
 
-                if (type != CommonDataKinds.BaseTypes.TYPE_CUSTOM) {
-                    try {
-                        labelView.setText(mLocalizedLabels.get(type));
-                    } catch (ArrayIndexOutOfBoundsException e) {
-                        labelView.setText(mLocalizedLabels.get(defaultType));
-                    }
+                final int type = cursor.getInt(typeColumnIndex);
+                final String label = cursor.getString(labelColumnIndex);
+
+                if (mMode == MODE_LEGACY_PICK_POSTAL || mMode == MODE_PICK_POSTAL) {
+                    labelView.setText(StructuredPostal.getTypeLabel(context.getResources(), type,
+                            label));
                 } else {
-                    cursor.copyStringToBuffer(labelColumnIndex, cache.labelBuffer);
-                    // Don't check size, if it's zero just don't show anything
-                    labelView.setText(cache.labelBuffer.data, 0, cache.labelBuffer.sizeCopied);
+                    labelView.setText(Phone.getTypeLabel(context.getResources(), type, label));
                 }
             } else {
                 // There is no label, hide the the view
diff --git a/src/com/android/contacts/ContactsUtils.java b/src/com/android/contacts/ContactsUtils.java
index dea0bad..13a17c1 100644
--- a/src/com/android/contacts/ContactsUtils.java
+++ b/src/com/android/contacts/ContactsUtils.java
@@ -339,17 +339,7 @@
     public static View createTabIndicatorView(ViewGroup parent, ContactsSource source) {
         Drawable icon = null;
         if (source != null) {
-            final String packageName = source.resPackageName;
-            if (source.iconRes > 0) {
-                try {
-                    final Context authContext = parent.getContext().
-                            createPackageContext(packageName, 0);
-                    icon = authContext.getResources().getDrawable(source.iconRes);
-
-                } catch (PackageManager.NameNotFoundException e) {
-                    Log.d(TAG, "error getting the Package Context for " + packageName, e);
-                }
-            }
+            icon = source.getDisplayIcon(parent.getContext());
         }
         return createTabIndicatorView(parent, null, icon);
     }
diff --git a/src/com/android/contacts/SplitAggregateView.java b/src/com/android/contacts/SplitAggregateView.java
index 09cf5ec..ed78fd9 100644
--- a/src/com/android/contacts/SplitAggregateView.java
+++ b/src/com/android/contacts/SplitAggregateView.java
@@ -251,16 +251,7 @@
             ContactsSource source = mSources.getInflatedSource(info.accountType,
                     ContactsSource.LEVEL_SUMMARY);
             if (source != null) {
-                final String packageName = source.resPackageName;
-                if (source.iconRes > 0) {
-                    try {
-                        final Context context = getContext().createPackageContext(packageName, 0);
-                        icon = context.getResources().getDrawable(source.iconRes);
-
-                    } catch (PackageManager.NameNotFoundException e) {
-                        Log.d(TAG, "error getting the Package Context for " + packageName, e);
-                    }
-                }
+                icon = source.getDisplayIcon(getContext());
             }
             if (icon != null) {
                 cache.sourceIcon.setImageDrawable(icon);
diff --git a/src/com/android/contacts/StyleManager.java b/src/com/android/contacts/StyleManager.java
deleted file mode 100644
index 2d24551..0000000
--- a/src/com/android/contacts/StyleManager.java
+++ /dev/null
@@ -1,315 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.contacts;
-
-import com.android.contacts.model.Sources;
-
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.WeakHashMap;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.content.res.TypedArray;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.util.Xml;
-
-
-/**
- * @deprecated Use {@link Sources} instead.
- */
-@Deprecated
-public final class StyleManager extends BroadcastReceiver {
-
-    public static final String TAG = "StyleManager";
-
-    private static StyleManager sInstance = null;
-
-    private WeakHashMap<String, Bitmap> mIconCache;
-    private HashMap<String, StyleSet> mStyleSetCache;
-
-    /*package*/ static final String DEFAULT_MIMETYPE = "default-icon";
-    private static final String ICON_SET_META_DATA = "com.android.contacts.iconset";
-    private static final String TAG_ICON_SET = "icon-set";
-    private static final String TAG_ICON = "icon";
-    private static final String TAG_ICON_DEFAULT = "icon-default";
-    private static final String KEY_JOIN_CHAR = "|";
-
-    private StyleManager(Context context) {
-        mIconCache = new WeakHashMap<String, Bitmap>();
-        mStyleSetCache = new HashMap<String, StyleSet>();
-        registerIntentReceivers(context);
-    }
-
-    /**
-     * Returns an instance of StyleManager. This method enforces that only a single instance of this
-     * class exists at any one time in a process.
-     *
-     * @param context A context object
-     * @return StyleManager object
-     */
-    public static StyleManager getInstance(Context context) {
-        if (sInstance == null) {
-            sInstance = new StyleManager(context);
-        }
-        return sInstance;
-    }
-
-    private void registerIntentReceivers(Context context) {
-        IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
-        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
-        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
-        filter.addDataScheme("package");
-
-        // We use getApplicationContext() so that the broadcast reciever can stay registered for
-        // the length of the application lifetime (instead of the calling activity's lifetime).
-        // This is so that we can notified of package changes, and purge the cache accordingly,
-        // but not be woken up if the application process isn't already running, since we will
-        // have no cache to clear at that point.
-        context.getApplicationContext().registerReceiver(this, filter);
-    }
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-        final String action = intent.getAction();
-        final String packageName = intent.getData().getSchemeSpecificPart();
-
-        if (Intent.ACTION_PACKAGE_REMOVED.equals(action)
-                || Intent.ACTION_PACKAGE_ADDED.equals(action)
-                || Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
-            onPackageChange(packageName);
-        }
-    }
-
-    public void onPackageChange(String packageName) {
-        Iterator<String> itr;
-
-        // Remove cached icons for this package
-        for (itr = mIconCache.keySet().iterator(); itr.hasNext(); ) {
-            if (itr.next().startsWith(packageName + KEY_JOIN_CHAR)) {
-                itr.remove();
-            }
-        }
-
-        // Remove the cached style set for this package
-        mStyleSetCache.remove(packageName);
-    }
-
-    /**
-     * Get the default icon for a given package. If no icon is specified for that package
-     * null is returned.
-     *
-     * @param packageName
-     * @return Bitmap holding the default icon.
-     */
-    public Bitmap getDefaultIcon(Context context, String packageName) {
-        return getMimetypeIcon(context, packageName, DEFAULT_MIMETYPE);
-    }
-
-    /**
-     * Get the icon associated with a mimetype for a given package. If no icon is specified for that
-     * package null is returned.
-     *
-     * @param packageName
-     * @return Bitmap holding the default icon.
-     */
-    public Bitmap getMimetypeIcon(Context context, String packageName, String mimetype) {
-        String key = getKey(packageName, mimetype);
-
-        synchronized(mIconCache) {
-            if (!mIconCache.containsKey(key)) {
-                // Cache miss
-
-                // loadIcon() may return null, which is fine since, if no icon was found we want to
-                // store a null value so we know not to look next time.
-                mIconCache.put(key, loadIcon(context, packageName, mimetype));
-            }
-            return mIconCache.get(key);
-        }
-    }
-
-    private Bitmap loadIcon(Context context, String packageName, String mimetype) {
-        StyleSet ss = null;
-
-        synchronized(mStyleSetCache) {
-            if (!mStyleSetCache.containsKey(packageName)) {
-                // Cache miss
-                try {
-                    StyleSet inflated = inflateStyleSet(context, packageName);
-                    mStyleSetCache.put(packageName, inflated);
-                } catch (InflateException e) {
-                    // If inflation failed keep a null entry so we know not to try again.
-                    Log.w(TAG, "Inflation failed: " + e);
-                    mStyleSetCache.put(packageName, null);
-                }
-            }
-        }
-
-        ss = mStyleSetCache.get(packageName);
-        if (ss == null) {
-            return null;
-        }
-
-        int iconRes;
-        if ((iconRes = ss.getIconRes(mimetype)) == -1) {
-            return null;
-        }
-
-        return BitmapFactory.decodeResource(context.getResources(),
-                iconRes, null);
-    }
-
-    private StyleSet inflateStyleSet(Context context, String packageName) throws InflateException {
-        final PackageManager pm = context.getPackageManager();
-        final ApplicationInfo ai;
-
-        try {
-            ai = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
-        } catch (NameNotFoundException e) {
-            return null;
-        }
-
-        XmlPullParser parser = ai.loadXmlMetaData(pm, ICON_SET_META_DATA);
-        final AttributeSet attrs = Xml.asAttributeSet(parser);
-
-        if (parser == null) {
-            return null;
-        }
-
-        try {
-            int type;
-            while ((type = parser.next()) != XmlPullParser.START_TAG
-                    && type != XmlPullParser.END_DOCUMENT) {
-                // Drain comments and whitespace
-            }
-
-            if (type != XmlPullParser.START_TAG) {
-                throw new InflateException("No start tag found");
-            }
-
-            if (!TAG_ICON_SET.equals(parser.getName())) {
-                throw new InflateException("Top level element must be StyleSet");
-            }
-
-            // Parse all children actions
-            StyleSet styleSet = new StyleSet();
-            final int depth = parser.getDepth();
-            while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
-                    && type != XmlPullParser.END_DOCUMENT) {
-                if (type == XmlPullParser.END_TAG) {
-                    continue;
-                }
-
-                TypedArray a;
-
-                String mimetype;
-                if (TAG_ICON.equals(parser.getName())) {
-                    a = context.obtainStyledAttributes(attrs, android.R.styleable.Icon);
-                    mimetype = a.getString(com.android.internal.R.styleable.Icon_mimeType);
-                    if (mimetype != null) {
-                        styleSet.addIcon(mimetype,
-                                a.getResourceId(com.android.internal.R.styleable.Icon_icon, -1));
-                    }
-                } else if (TAG_ICON_DEFAULT.equals(parser.getName())) {
-                    a = context.obtainStyledAttributes(attrs, android.R.styleable.IconDefault);
-                    styleSet.addIcon(DEFAULT_MIMETYPE,
-                            a.getResourceId(
-                                    com.android.internal.R.styleable.IconDefault_icon, -1));
-                } else {
-                    throw new InflateException("Expected " + TAG_ICON + " or "
-                            + TAG_ICON_DEFAULT + " tag");
-                }
-            }
-            return styleSet;
-
-        } catch (XmlPullParserException e) {
-            throw new InflateException("Problem reading XML", e);
-        } catch (IOException e) {
-            throw new InflateException("Problem reading XML", e);
-        }
-    }
-
-    private String getKey(String packageName, String mimetype) {
-        return packageName + KEY_JOIN_CHAR + mimetype;
-    }
-
-    public static class InflateException extends Exception {
-        public InflateException(String message) {
-            super(message);
-        }
-
-        public InflateException(String message, Throwable throwable) {
-            super(message, throwable);
-        }
-    }
-
-    private static class StyleSet {
-        private HashMap<String, Integer> mMimetypeIconResMap;
-
-        public StyleSet() {
-            mMimetypeIconResMap = new HashMap<String, Integer>();
-        }
-
-        public int getIconRes(String mimetype) {
-            if (!mMimetypeIconResMap.containsKey(mimetype)) {
-                return -1;
-            }
-            return mMimetypeIconResMap.get(mimetype);
-        }
-
-        public void addIcon(String mimetype, int res) {
-            if (mimetype == null) {
-                return;
-            }
-            mMimetypeIconResMap.put(mimetype, res);
-        }
-    }
-
-    //-------------------------------------------//
-    //-- Methods strictly for testing purposes --//
-    //-------------------------------------------//
-
-    /*package*/ int getIconCacheSize() {
-        return mIconCache.size();
-    }
-
-    /*package*/ int getStyleSetCacheSize() {
-        return mStyleSetCache.size();
-    }
-
-    /*package*/ boolean isStyleSetCacheHit(String packageName) {
-        return mStyleSetCache.containsKey(packageName);
-    }
-
-    /*package*/ boolean isIconCacheHit(String packageName, String mimetype) {
-        return mIconCache.containsKey(getKey(packageName, mimetype));
-    }
-
-    //-------------------------------------------//
-}
diff --git a/src/com/android/contacts/ViewContactActivity.java b/src/com/android/contacts/ViewContactActivity.java
index be78bea..0b07a57 100644
--- a/src/com/android/contacts/ViewContactActivity.java
+++ b/src/com/android/contacts/ViewContactActivity.java
@@ -302,7 +302,8 @@
             // 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, ContactsUtils.createTabIndicatorView(mTabWidget.getTabParent(), source));
+            addTab(rawContactId, ContactsUtils.createTabIndicatorView(mTabWidget.getTabParent(),
+                    source));
         }
     }
 
@@ -846,12 +847,11 @@
             for (Entity entity: mEntities) {
                 final ContentValues entValues = entity.getEntityValues();
                 final String accountType = entValues.getAsString(RawContacts.ACCOUNT_TYPE);
-                // TODO: entry.contactId should be renamed to entry.rawContactId
-                long contactId = entValues.getAsLong(RawContacts._ID);
+                final long rawContactId = entValues.getAsLong(RawContacts._ID);
 
                 // This performs the tab filtering
                 if (mSelectedRawContactId != null
-                        && mSelectedRawContactId != contactId
+                        && mSelectedRawContactId != rawContactId
                         && mSelectedRawContactId != ALL_CONTACTS_ID) {
                     continue;
                 }
@@ -859,26 +859,19 @@
                 for (NamedContentValues subValue : entity.getSubValues()) {
                     ViewEntry entry = new ViewEntry();
 
-                    ContentValues entryValues = subValue.values;
+                    final ContentValues entryValues = subValue.values;
+                    entryValues.put(Data.RAW_CONTACT_ID, rawContactId);
+
                     final String mimetype = entryValues.getAsString(Data.MIMETYPE);
-                    if (mimetype == null || accountType == null) {
-                        continue;
-                    }
+                    if (mimetype == null) continue;
 
-                    ContactsSource contactsSource = sources.getInflatedSource(accountType,
+                    final DataKind kind = sources.getKindOrFallback(accountType, mimetype, this,
                             ContactsSource.LEVEL_MIMETYPES);
-                    if (contactsSource == null) {
-                        continue;
-                    }
-
-                    DataKind kind = contactsSource.getKindForMimetype(mimetype);
-                    if (kind == null) {
-                        continue;
-                    }
+                    if (kind == null) continue;
 
                     final long id = entryValues.getAsLong(Data._ID);
                     final Uri uri = ContentUris.withAppendedId(Data.CONTENT_URI, id);
-                    entry.contactId = contactId;
+                    entry.contactId = rawContactId;
                     entry.id = id;
                     entry.uri = uri;
                     entry.mimetype = mimetype;
@@ -888,12 +881,12 @@
                         entry.type = entryValues.getAsInteger(kind.typeColumn);
                     }
                     if (kind.iconRes > 0) {
+                        entry.resPackageName = kind.resPackageName;
                         entry.actionIcon = kind.iconRes;
                     }
 
                     // Don't crash if the data is bogus
                     if (TextUtils.isEmpty(entry.data)) {
-                        Log.w(TAG, "empty data for contact method " + id);
                         continue;
                     }
 
@@ -938,10 +931,6 @@
                             // Build email entries
                             entry.intent = new Intent(Intent.ACTION_SENDTO,
                                     Uri.fromParts("mailto", entry.data, null));
-                            // Temporary hack until we get real label resources for exchange.
-                            if (TextUtils.isEmpty(entry.label)) {
-                                entry.label = getString(R.string.email).toLowerCase();
-                            }
                             entry.isPrimary = isSuperPrimary;
                             mEmailEntries.add(entry);
                         } else if (CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE.
@@ -995,9 +984,12 @@
                         entry.intent = null;
                         entry.maxLines = 10;
                         mOtherEntries.add(entry);
+                    } else {
+                        // Handle showing custom
+                        entry.intent = new Intent(Intent.ACTION_VIEW, uri);
+                        mOtherEntries.add(entry);
                     }
 
-
                     // TODO(emillar) Add group entries
                     //              // Build the group entries
                     //              final Uri groupsUri = Uri.withAppendedPath(mUri, GroupMembership.CONTENT_DIRECTORY);
@@ -1069,6 +1061,7 @@
      * A basic structure with the data for a contact entry in the list.
      */
     static class ViewEntry extends ContactEntryAdapter.Entry implements Collapsible<ViewEntry> {
+        public String resPackageName = null;
         public int actionIcon = -1;
         public boolean isPrimary = false;
         public int presenceIcon = -1;
@@ -1225,7 +1218,15 @@
             // Set the action icon
             ImageView action = views.actionIcon;
             if (entry.actionIcon != -1) {
-                action.setImageDrawable(resources.getDrawable(entry.actionIcon));
+                Drawable actionIcon;
+                if (entry.resPackageName != null) {
+                    // Load external resources through PackageManager
+                    actionIcon = mContext.getPackageManager().getDrawable(entry.resPackageName,
+                            entry.actionIcon, null);
+                } else {
+                    actionIcon = resources.getDrawable(entry.actionIcon);
+                }
+                action.setImageDrawable(actionIcon);
                 action.setVisibility(View.VISIBLE);
             } else {
                 // Things should still line up as if there was an icon, so make it invisible
diff --git a/src/com/android/contacts/model/ContactsSource.java b/src/com/android/contacts/model/ContactsSource.java
index 1e797d4..f0c21e3 100644
--- a/src/com/android/contacts/model/ContactsSource.java
+++ b/src/com/android/contacts/model/ContactsSource.java
@@ -17,17 +17,14 @@
 package com.android.contacts.model;
 
 import com.google.android.collect.Lists;
-
-import org.xmlpull.v1.XmlPullParser;
+import com.google.android.collect.Maps;
 
 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.graphics.drawable.Drawable;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.RawContacts;
@@ -38,41 +35,9 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.List;
 
-/*
-
-<!-- example of what SourceConstraints would look like in XML -->
-<!-- NOTE: may not directly match the current structure version -->
-
-<DataKind
-    mimeType="vnd.android.cursor.item/email"
-    title="@string/title_postal"
-    icon="@drawable/icon_postal"
-    weight="12"
-    editable="true">
-
-    <!-- these are defined using string-builder-ish -->
-    <ActionHeader></ActionHeader>
-    <ActionBody socialSummary="true" />  <!-- can pull together various columns -->
-
-    <!-- ordering handles precedence the "insert/add" case -->
-    <!-- assume uniform type when missing "column", use title in place -->
-    <EditTypes column="data5" overallMax="-1">
-        <EditType rawValue="0" label="@string/type_home" specificMax="-1" />
-        <EditType rawValue="1" label="@string/type_work" specificMax="-1" secondary="true" />
-        <EditType rawValue="4" label="@string/type_custom" customColumn="data6" specificMax="-1" secondary="true" />
-    </EditTypes>
-
-    <!-- when single edit field, simplifies edit case -->
-    <EditField column="data1" title="@string/field_family_name" android:inputType="textCapWords|textPhonetic" />
-    <EditField column="data2" title="@string/field_given_name" android:minLines="2" />
-    <EditField column="data3" title="@string/field_suffix" />
-
-</DataKind>
-
-*/
-
 /**
  * Internal structure that represents constraints and styles for a specific data
  * source, such as the various data types they support, including details on how
@@ -80,7 +45,7 @@
  * <p>
  * In the future this may be inflated from XML defined by a data source.
  */
-public class ContactsSource {
+public abstract class ContactsSource {
     /**
      * The {@link RawContacts#ACCOUNT_TYPE} these constraints apply to.
      */
@@ -91,6 +56,7 @@
      * {@link Account} or for matching against {@link Data#RES_PACKAGE}.
      */
     public String resPackageName;
+    public String summaryResPackageName;
 
     public int titleRes;
     public int iconRes;
@@ -102,14 +68,17 @@
      */
     private ArrayList<DataKind> mKinds = Lists.newArrayList();
 
-    private static final String ACTION_SYNC_ADAPTER = "android.content.SyncAdapter";
-    private static final String METADATA_CONTACTS = "android.provider.CONTACTS_STRUCTURE";
+    /**
+     * Lookup map of {@link #mKinds} on {@link DataKind#mimeType}.
+     */
+    private HashMap<String, DataKind> mMimeKinds = Maps.newHashMap();
 
+    public static final int LEVEL_NONE = 0;
     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;
+    private int mInflatedLevel = LEVEL_NONE;
 
     public synchronized boolean isInflated(int inflateLevel) {
         return mInflatedLevel >= inflateLevel;
@@ -121,62 +90,54 @@
     }
 
     /**
-     * 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.
+     * Ensure that this {@link ContactsSource} has been inflated to the
+     * requested level.
      */
     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_FALLBACK.equals(accountType)) {
-            HardCodedSources.buildFallback(context, this);
-            return;
-        } else if (HardCodedSources.ACCOUNT_TYPE_GOOGLE.equals(accountType)) {
-            HardCodedSources.buildGoogle(context, this);
-            return;
-        } else if(HardCodedSources.ACCOUNT_TYPE_EXCHANGE.equals(accountType)) {
-            HardCodedSources.buildExchange(context, this);
-            return;
-        } else if(HardCodedSources.ACCOUNT_TYPE_FACEBOOK.equals(accountType)) {
-            HardCodedSources.buildFacebook(context, 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);
+        if (!isInflated(inflateLevel)) {
+            inflate(context, inflateLevel);
         }
     }
 
     /**
-     * Inflate this {@link ContactsSource} from the given parser. This may only
-     * load details matching the publicly-defined schema.
+     * Perform the actual inflation to the requested level. Called by
+     * {@link #ensureInflated(Context, int)} when inflation is needed.
      */
-    protected void inflate(XmlPullParser parser) {
-        // TODO: implement basic functionality for third-party integration
-        throw new UnsupportedOperationException("Custom constraint parser not implemented");
+    protected abstract void inflate(Context context, int inflateLevel);
+
+    /**
+     * Invalidate any cache for this {@link ContactsSource}, removing all
+     * inflated data. Calling {@link #ensureInflated(Context, int)} will
+     * populate again from scratch.
+     */
+    public synchronized void invalidateCache() {
+        this.mKinds.clear();
+        this.mMimeKinds.clear();
+        setInflatedLevel(LEVEL_NONE);
     }
 
     public CharSequence getDisplayLabel(Context context) {
-        if (this.titleRes > 0) {
+        if (this.titleRes != -1 && this.summaryResPackageName != null) {
             final PackageManager pm = context.getPackageManager();
-            return pm.getText(this.resPackageName, this.titleRes, null);
+            return pm.getText(this.summaryResPackageName, this.titleRes, null);
+        } else if (this.titleRes != -1) {
+            return context.getText(this.titleRes);
         } else {
             return this.accountType;
         }
     }
 
+    public Drawable getDisplayIcon(Context context) {
+        if (this.titleRes != -1 && this.summaryResPackageName != null) {
+            final PackageManager pm = context.getPackageManager();
+            return pm.getDrawable(this.summaryResPackageName, this.iconRes, null);
+        } else if (this.titleRes != -1) {
+            return context.getResources().getDrawable(this.iconRes);
+        } else {
+            return null;
+        }
+    }
+
     /**
      * {@link Comparator} to sort by {@link DataKind#weight}.
      */
@@ -197,20 +158,22 @@
     }
 
     /**
-     * Find the {@link DataKind} for a specifc MIME-type, if it's handled by
-     * this data source.
+     * Find the {@link DataKind} for a specific MIME-type, if it's handled by
+     * this data source. If you may need a fallback {@link DataKind}, use
+     * {@link Sources#getKindOrFallback(String, String, Context, int)}.
      */
     public DataKind getKindForMimetype(String mimeType) {
-        for (DataKind kind : mKinds) {
-            if (mimeType.equals(kind.mimeType)) {
-                return kind;
-            }
-        }
-        return null;
+        return this.mMimeKinds.get(mimeType);
     }
 
-    public void add(DataKind kind) {
+    /**
+     * Add given {@link DataKind} to list of those provided by this source.
+     */
+    public DataKind addKind(DataKind kind) {
+        kind.resPackageName = this.resPackageName;
         this.mKinds.add(kind);
+        this.mMimeKinds.put(kind.mimeType, kind);
+        return kind;
     }
 
     /**
@@ -220,6 +183,7 @@
      * labels and editable {@link EditField}.
      */
     public static class DataKind {
+        public String resPackageName;
         public String mimeType;
         public int titleRes;
         public int iconRes;
@@ -231,6 +195,7 @@
         public StringInflater actionHeader;
         public StringInflater actionAltHeader;
         public StringInflater actionBody;
+        public StringInflater actionFooter;
         public boolean actionBodySocial;
         public boolean actionBodyCombine;
 
@@ -242,6 +207,9 @@
 
         public ContentValues defaultValues;
 
+        public DataKind() {
+        }
+
         public DataKind(String mimeType, int titleRes, int iconRes, int weight, boolean editable) {
             this.mimeType = mimeType;
             this.titleRes = titleRes;
@@ -261,8 +229,8 @@
     public static class EditType {
         public int rawValue;
         public int labelRes;
-        public int actionRes;
-        public int actionAltRes;
+//        public int actionRes;
+//        public int actionAltRes;
         public boolean secondary;
         public int specificMax;
         public String customColumn;
@@ -273,16 +241,6 @@
             this.specificMax = -1;
         }
 
-        public EditType(int rawValue, int labelRes, int actionRes) {
-            this(rawValue, labelRes);
-            this.actionRes = actionRes;
-        }
-
-        public EditType(int rawValue, int labelRes, int actionRes, int actionAltRes) {
-            this(rawValue, labelRes, actionRes);
-            this.actionAltRes = actionAltRes;
-        }
-
         public EditType setSecondary(boolean secondary) {
             this.secondary = secondary;
             return this;
@@ -335,9 +293,9 @@
             this.inputType = inputType;
         }
 
-        public EditField(String column, int titleRes, int inputType, boolean optional) {
-            this(column, titleRes, inputType);
+        public EditField setOptional(boolean optional) {
             this.optional = optional;
+            return this;
         }
     }
 
diff --git a/src/com/android/contacts/model/ExchangeSource.java b/src/com/android/contacts/model/ExchangeSource.java
new file mode 100644
index 0000000..c017037
--- /dev/null
+++ b/src/com/android/contacts/model/ExchangeSource.java
@@ -0,0 +1,273 @@
+/*
+ * 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.google.android.collect.Lists;
+
+import android.content.ContentValues;
+import android.content.Context;
+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;
+
+public class ExchangeSource extends FallbackSource {
+
+    public static final String ACCOUNT_TYPE = "com.android.exchange";
+
+    public ExchangeSource(String resPackageName) {
+        this.accountType = ACCOUNT_TYPE;
+        this.resPackageName = null;
+        this.summaryResPackageName = resPackageName;
+    }
+
+    @Override
+    protected void inflate(Context context, int inflateLevel) {
+
+        inflateStructuredName(inflateLevel);
+        inflateNickname(inflateLevel);
+        inflatePhone(inflateLevel);
+        inflateEmail(inflateLevel);
+        inflateStructuredPostal(inflateLevel);
+        inflateIm(inflateLevel);
+        inflateOrganization(inflateLevel);
+        inflatePhoto(inflateLevel);
+        inflateNote(inflateLevel);
+        inflateWebsite(inflateLevel);
+
+        setInflatedLevel(inflateLevel);
+    }
+
+    @Override
+    protected DataKind inflateStructuredName(int inflateLevel) {
+        final DataKind kind = super.inflateStructuredName(ContactsSource.LEVEL_MIMETYPES);
+
+        if (inflateLevel >= ContactsSource.LEVEL_CONSTRAINTS) {
+            kind.typeOverallMax = 1;
+
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
+                    FLAGS_PERSON_NAME).setOptional(true));
+            kind.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
+                    FLAGS_PERSON_NAME));
+            kind.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
+                    FLAGS_PERSON_NAME).setOptional(true));
+            kind.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
+                    FLAGS_PERSON_NAME));
+            kind.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
+                    FLAGS_PERSON_NAME).setOptional(true));
+            kind.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME,
+                    R.string.name_phonetic_given, FLAGS_PHONETIC).setOptional(true));
+            kind.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME,
+                    R.string.name_phonetic_family, FLAGS_PHONETIC).setOptional(true));
+        }
+
+        return kind;
+    }
+
+    @Override
+    protected DataKind inflateNickname(int inflateLevel) {
+        final DataKind kind = super.inflateNickname(ContactsSource.LEVEL_MIMETYPES);
+
+        if (inflateLevel >= ContactsSource.LEVEL_CONSTRAINTS) {
+            kind.typeOverallMax = 1;
+
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(Nickname.NAME, R.string.nicknameLabelsGroup,
+                    FLAGS_PERSON_NAME));
+        }
+
+        return kind;
+    }
+
+    @Override
+    protected DataKind inflatePhone(int inflateLevel) {
+        final DataKind kind = super.inflatePhone(ContactsSource.LEVEL_MIMETYPES);
+
+        if (inflateLevel >= ContactsSource.LEVEL_CONSTRAINTS) {
+            kind.typeColumn = Phone.TYPE;
+            kind.typeList = Lists.newArrayList();
+            kind.typeList.add(buildPhoneType(Phone.TYPE_HOME).setSpecificMax(2));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE).setSpecificMax(1));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_WORK).setSpecificMax(2));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true)
+                    .setSpecificMax(1));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true)
+                    .setSpecificMax(1));
+            kind.typeList
+                    .add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true).setSpecificMax(1));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_CAR).setSecondary(true).setSpecificMax(1));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_COMPANY_MAIN).setSecondary(true)
+                    .setSpecificMax(1));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_MMS).setSecondary(true).setSpecificMax(1));
+            kind.typeList
+                    .add(buildPhoneType(Phone.TYPE_RADIO).setSecondary(true).setSpecificMax(1));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_CUSTOM).setSecondary(true)
+                    .setSpecificMax(1).setCustomColumn(Phone.LABEL));
+
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+        }
+
+        return kind;
+    }
+
+    @Override
+    protected DataKind inflateEmail(int inflateLevel) {
+        final DataKind kind = super.inflateEmail(ContactsSource.LEVEL_MIMETYPES);
+
+        if (inflateLevel >= ContactsSource.LEVEL_CONSTRAINTS) {
+            kind.typeOverallMax = 3;
+
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+        }
+
+        return kind;
+    }
+
+    @Override
+    protected DataKind inflateStructuredPostal(int inflateLevel) {
+        final DataKind kind = super.inflateStructuredPostal(ContactsSource.LEVEL_MIMETYPES);
+
+        if (inflateLevel >= ContactsSource.LEVEL_CONSTRAINTS) {
+            kind.typeColumn = StructuredPostal.TYPE;
+            kind.typeList = Lists.newArrayList();
+            kind.typeList.add(buildPostalType(StructuredPostal.TYPE_WORK).setSpecificMax(1));
+            kind.typeList.add(buildPostalType(StructuredPostal.TYPE_HOME).setSpecificMax(1));
+            kind.typeList.add(buildPostalType(StructuredPostal.TYPE_OTHER).setSpecificMax(1));
+
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(StructuredPostal.STREET, R.string.postal_street,
+                    FLAGS_POSTAL));
+            kind.fieldList.add(new EditField(StructuredPostal.CITY, R.string.postal_city,
+                    FLAGS_POSTAL));
+            kind.fieldList.add(new EditField(StructuredPostal.REGION, R.string.postal_region,
+                    FLAGS_POSTAL));
+            kind.fieldList.add(new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode,
+                    FLAGS_POSTAL));
+            kind.fieldList.add(new EditField(StructuredPostal.COUNTRY, R.string.postal_country,
+                    FLAGS_POSTAL).setOptional(true));
+        }
+
+        return kind;
+    }
+
+    @Override
+    protected DataKind inflateIm(int inflateLevel) {
+        final DataKind kind = super.inflateIm(ContactsSource.LEVEL_MIMETYPES);
+
+        if (inflateLevel >= ContactsSource.LEVEL_CONSTRAINTS) {
+            kind.typeOverallMax = 3;
+
+            // NOTE: even though a traditional "type" exists, for editing
+            // purposes we're using the protocol to pick labels
+
+            kind.defaultValues = new ContentValues();
+            kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER);
+
+            kind.typeColumn = Im.PROTOCOL;
+            kind.typeList = Lists.newArrayList();
+            kind.typeList.add(buildImType(Im.PROTOCOL_AIM));
+            kind.typeList.add(buildImType(Im.PROTOCOL_MSN));
+            kind.typeList.add(buildImType(Im.PROTOCOL_YAHOO));
+            kind.typeList.add(buildImType(Im.PROTOCOL_SKYPE));
+            kind.typeList.add(buildImType(Im.PROTOCOL_QQ));
+            kind.typeList.add(buildImType(Im.PROTOCOL_GOOGLE_TALK));
+            kind.typeList.add(buildImType(Im.PROTOCOL_ICQ));
+            kind.typeList.add(buildImType(Im.PROTOCOL_JABBER));
+            kind.typeList.add(buildImType(Im.PROTOCOL_CUSTOM).setSecondary(true).setCustomColumn(
+                    Im.CUSTOM_PROTOCOL));
+
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
+        }
+
+        return kind;
+    }
+
+    @Override
+    protected DataKind inflateOrganization(int inflateLevel) {
+        final DataKind kind = super.inflateOrganization(ContactsSource.LEVEL_MIMETYPES);
+
+        if (inflateLevel >= ContactsSource.LEVEL_CONSTRAINTS) {
+            kind.typeColumn = Organization.TYPE;
+            kind.typeList = Lists.newArrayList();
+            kind.typeList.add(buildOrgType(Organization.TYPE_WORK).setSpecificMax(1));
+            kind.typeList.add(buildOrgType(Organization.TYPE_OTHER).setSpecificMax(1));
+            kind.typeList.add(buildOrgType(Organization.TYPE_CUSTOM).setSecondary(true)
+                    .setSpecificMax(1));
+
+            kind.fieldList = Lists.newArrayList();
+            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));
+        }
+
+        return kind;
+    }
+
+    @Override
+    protected DataKind inflatePhoto(int inflateLevel) {
+        final DataKind kind = super.inflatePhoto(ContactsSource.LEVEL_MIMETYPES);
+
+        if (inflateLevel >= ContactsSource.LEVEL_CONSTRAINTS) {
+            kind.typeOverallMax = 1;
+
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1));
+        }
+
+        return kind;
+    }
+
+    @Override
+    protected DataKind inflateNote(int inflateLevel) {
+        final DataKind kind = super.inflateNote(ContactsSource.LEVEL_MIMETYPES);
+
+        if (inflateLevel >= ContactsSource.LEVEL_CONSTRAINTS) {
+            kind.typeOverallMax = 1;
+
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE));
+        }
+
+        return kind;
+    }
+
+    @Override
+    protected DataKind inflateWebsite(int inflateLevel) {
+        final DataKind kind = super.inflateWebsite(ContactsSource.LEVEL_MIMETYPES);
+
+        if (inflateLevel >= ContactsSource.LEVEL_CONSTRAINTS) {
+            kind.typeOverallMax = 1;
+
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE));
+        }
+
+        return kind;
+    }
+}
diff --git a/src/com/android/contacts/model/ExternalSource.java b/src/com/android/contacts/model/ExternalSource.java
new file mode 100644
index 0000000..f22aba4
--- /dev/null
+++ b/src/com/android/contacts/model/ExternalSource.java
@@ -0,0 +1,261 @@
+/*
+ * 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 org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.content.ContentResolver;
+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.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.database.Cursor;
+import android.provider.ContactsContract.Data;
+import android.provider.SocialContract.Activities;
+import android.text.format.DateUtils;
+import android.util.AttributeSet;
+import android.util.Xml;
+
+import java.io.IOException;
+import java.util.List;
+
+/*
+
+<!-- example of what SourceConstraints would look like in XML -->
+<!-- NOTE: may not directly match the current structure version -->
+
+<DataKind
+    mimeType="vnd.android.cursor.item/email"
+    title="@string/title_postal"
+    icon="@drawable/icon_postal"
+    weight="12"
+    editable="true">
+
+    <!-- these are defined using string-builder-ish -->
+    <ActionHeader></ActionHeader>
+    <ActionBody socialSummary="true" />  <!-- can pull together various columns -->
+
+    <!-- ordering handles precedence the "insert/add" case -->
+    <!-- assume uniform type when missing "column", use title in place -->
+    <EditTypes column="data5" overallMax="-1">
+        <EditType rawValue="0" label="@string/type_home" specificMax="-1" />
+        <EditType rawValue="1" label="@string/type_work" specificMax="-1" secondary="true" />
+        <EditType rawValue="4" label="@string/type_custom" customColumn="data6" specificMax="-1" secondary="true" />
+    </EditTypes>
+
+    <!-- when single edit field, simplifies edit case -->
+    <EditField column="data1" title="@string/field_family_name" android:inputType="textCapWords|textPhonetic" />
+    <EditField column="data2" title="@string/field_given_name" android:minLines="2" />
+    <EditField column="data3" title="@string/field_suffix" />
+
+</DataKind>
+
+*/
+
+/**
+ * Internal structure that represents constraints and styles for a specific data
+ * source, such as the various data types they support, including details on how
+ * those types should be rendered and edited.
+ * <p>
+ * In the future this may be inflated from XML defined by a data source.
+ */
+public class ExternalSource extends ContactsSource {
+    private static final String ACTION_SYNC_ADAPTER = "android.content.SyncAdapter";
+    private static final String METADATA_CONTACTS = "android.provider.CONTACTS_STRUCTURE";
+
+    private interface InflateTags {
+        final String CONTACTS_SOURCE = "ContactsSource";
+        final String CONTACTS_DATA_KIND = "ContactsDataKind";
+    }
+
+    public ExternalSource(String resPackageName) {
+        this.resPackageName = resPackageName;
+        this.summaryResPackageName = resPackageName;
+    }
+
+    /**
+     * 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.
+     */
+    @Override
+    public void inflate(Context context, int inflateLevel) {
+        // 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.serviceInfo.loadXmlMetaData(pm,
+                    METADATA_CONTACTS);
+            if (parser == null) continue;
+            inflate(context, parser);
+        }
+    }
+
+    /**
+     * Inflate this {@link ContactsSource} from the given parser. This may only
+     * load details matching the publicly-defined schema.
+     */
+    protected void inflate(Context context, XmlPullParser parser) {
+        final AttributeSet attrs = Xml.asAttributeSet(parser);
+
+        try {
+            int type;
+            while ((type = parser.next()) != XmlPullParser.START_TAG
+                    && type != XmlPullParser.END_DOCUMENT) {
+                // Drain comments and whitespace
+            }
+
+            if (type != XmlPullParser.START_TAG) {
+                throw new IllegalStateException("No start tag found");
+            }
+
+            if (!InflateTags.CONTACTS_SOURCE.equals(parser.getName())) {
+                throw new IllegalStateException("Top level element must be "
+                        + InflateTags.CONTACTS_SOURCE);
+            }
+
+            // Parse all children kinds
+            final int depth = parser.getDepth();
+            while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
+                    && type != XmlPullParser.END_DOCUMENT) {
+                if (type == XmlPullParser.END_TAG
+                        || !InflateTags.CONTACTS_DATA_KIND.equals(parser.getName())) {
+                    continue;
+                }
+
+                final TypedArray a = context.obtainStyledAttributes(attrs,
+                        android.R.styleable.ContactsDataKind);
+                final DataKind kind = new DataKind();
+
+                kind.mimeType = a
+                        .getString(com.android.internal.R.styleable.ContactsDataKind_mimeType);
+                kind.iconRes = a.getResourceId(
+                        com.android.internal.R.styleable.ContactsDataKind_icon, -1);
+
+                final String summaryColumn = a
+                        .getString(com.android.internal.R.styleable.ContactsDataKind_summaryColumn);
+                if (summaryColumn != null) {
+                    // Inflate a specific column as summary when requested
+                    kind.actionHeader = new FallbackSource.SimpleInflater(summaryColumn);
+                }
+
+                final String detailColumn = a
+                        .getString(com.android.internal.R.styleable.ContactsDataKind_detailColumn);
+                final boolean detailSocialSummary = a.getBoolean(
+                        com.android.internal.R.styleable.ContactsDataKind_detailSocialSummary,
+                        false);
+                if (detailSocialSummary) {
+                    // Inflate social summary when requested
+                    kind.actionBody = new SocialInflater(false);
+                    kind.actionFooter = new SocialInflater(true);
+                } else {
+                    // Otherwise inflate specific column as summary
+                    kind.actionBody = new FallbackSource.SimpleInflater(detailColumn);
+                }
+
+                addKind(kind);
+            }
+        } catch (XmlPullParserException e) {
+            throw new IllegalStateException("Problem reading XML", e);
+        } catch (IOException e) {
+            throw new IllegalStateException("Problem reading XML", e);
+        }
+    }
+
+    /**
+     * Temporary cache to hold recent social data.
+     */
+    private static class SocialCache {
+        private static Status sLastStatus = null;
+
+        public static class Status {
+            public long rawContactId;
+            public CharSequence title;
+            public long published;
+        }
+
+        public static synchronized Status getLatestStatus(Context context, long rawContactId) {
+            if (sLastStatus == null || sLastStatus.rawContactId != rawContactId) {
+                // Cache missing, or miss, so query directly
+                sLastStatus = queryLatestStatus(context, rawContactId);
+            }
+            return sLastStatus;
+        }
+
+        private static Status queryLatestStatus(Context context, long rawContactId) {
+            // Find latest social update by this person, filtering to show only
+            // original content and avoid replies.
+            final ContentResolver resolver = context.getContentResolver();
+            final Cursor cursor = resolver.query(Activities.CONTENT_URI, new String[] {
+                Activities.TITLE, Activities.PUBLISHED
+            }, Activities.AUTHOR_CONTACT_ID + "=" + rawContactId + " AND "
+                    + Activities.IN_REPLY_TO + " IS NULL", null, Activities.PUBLISHED + " DESC");
+
+            final Status status = new Status();
+            try {
+                if (cursor != null && cursor.moveToFirst()) {
+                    status.title = cursor.getString(0);
+                    status.published = cursor.getLong(1);
+                }
+            } finally {
+                if (cursor != null) cursor.close();
+            }
+            return status;
+        }
+    }
+
+    /**
+     * Inflater that will return the latest {@link Activities#TITLE} and
+     * {@link Activities#PUBLISHED} for the given {@link Data#RAW_CONTACT_ID}.
+     */
+    protected static class SocialInflater implements StringInflater {
+        private final boolean mPublishedMode;
+
+        public SocialInflater(boolean publishedMode) {
+            mPublishedMode = publishedMode;
+        }
+
+        protected CharSequence inflatePublished(long published) {
+            return DateUtils.getRelativeTimeSpanString(published, System.currentTimeMillis(),
+                    DateUtils.MINUTE_IN_MILLIS);
+        }
+
+        /** {@inheritDoc} */
+        public CharSequence inflateUsing(Context context, Cursor cursor) {
+            final Long rawContactId = cursor.getLong(cursor.getColumnIndex(Data.RAW_CONTACT_ID));
+            if (rawContactId == null) return null;
+
+            final SocialCache.Status status = SocialCache.getLatestStatus(context, rawContactId);
+            return mPublishedMode ? inflatePublished(status.published) : status.title;
+        }
+
+        /** {@inheritDoc} */
+        public CharSequence inflateUsing(Context context, ContentValues values) {
+            final Long rawContactId = values.getAsLong(Data.RAW_CONTACT_ID);
+            if (rawContactId == null) return null;
+
+            final SocialCache.Status status = SocialCache.getLatestStatus(context, rawContactId);
+            return mPublishedMode ? inflatePublished(status.published) : status.title;
+        }
+    }
+}
diff --git a/src/com/android/contacts/model/FallbackSource.java b/src/com/android/contacts/model/FallbackSource.java
new file mode 100644
index 0000000..85aedd7
--- /dev/null
+++ b/src/com/android/contacts/model/FallbackSource.java
@@ -0,0 +1,579 @@
+/*
+ * 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.google.android.collect.Lists;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
+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.text.TextUtils;
+import android.view.inputmethod.EditorInfo;
+
+public class FallbackSource extends ContactsSource {
+    protected static final int FLAGS_PHONE = EditorInfo.TYPE_CLASS_PHONE;
+    protected static final int FLAGS_EMAIL = EditorInfo.TYPE_CLASS_TEXT
+            | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
+    protected static final int FLAGS_PERSON_NAME = EditorInfo.TYPE_CLASS_TEXT
+            | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS | EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME;
+    protected static final int FLAGS_PHONETIC = EditorInfo.TYPE_CLASS_TEXT
+            | EditorInfo.TYPE_TEXT_VARIATION_PHONETIC;
+    protected static final int FLAGS_GENERIC_NAME = EditorInfo.TYPE_CLASS_TEXT
+            | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
+    protected static final int FLAGS_NOTE = EditorInfo.TYPE_CLASS_TEXT
+            | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
+    protected static final int FLAGS_WEBSITE = EditorInfo.TYPE_CLASS_TEXT
+            | EditorInfo.TYPE_TEXT_VARIATION_URI;
+    protected 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;
+
+    public FallbackSource() {
+        this.accountType = null;
+        this.titleRes = R.string.account_phone;
+        this.iconRes = R.drawable.ic_launcher_contacts;
+    }
+
+    @Override
+    protected void inflate(Context context, int inflateLevel) {
+
+        inflateStructuredName(inflateLevel);
+        inflateNickname(inflateLevel);
+        inflatePhone(inflateLevel);
+        inflateEmail(inflateLevel);
+        inflateStructuredPostal(inflateLevel);
+        inflateIm(inflateLevel);
+        inflateOrganization(inflateLevel);
+        inflatePhoto(inflateLevel);
+        inflateNote(inflateLevel);
+        inflateWebsite(inflateLevel);
+
+        setInflatedLevel(inflateLevel);
+
+    }
+
+    protected EditType buildPhoneType(int type) {
+        return new EditType(type, Phone.getTypeLabelResource(type));
+    }
+
+    protected EditType buildEmailType(int type) {
+        return new EditType(type, Email.getTypeLabelResource(type));
+    }
+
+    protected EditType buildPostalType(int type) {
+        return new EditType(type, StructuredPostal.getTypeLabelResource(type));
+    }
+
+    protected EditType buildImType(int type) {
+        return new EditType(type, Im.getProtocolLabelResource(type));
+    }
+
+    protected EditType buildOrgType(int type) {
+        return new EditType(type, Organization.getTypeLabelResource(type));
+    }
+
+    protected DataKind inflateStructuredName(int inflateLevel) {
+        DataKind kind = getKindForMimetype(StructuredName.CONTENT_ITEM_TYPE);
+        if (kind == null) {
+            kind = addKind(new DataKind(StructuredName.CONTENT_ITEM_TYPE,
+                    R.string.nameLabelsGroup, -1, -1, true));
+        }
+
+        if (inflateLevel >= ContactsSource.LEVEL_CONSTRAINTS) {
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
+                    FLAGS_PERSON_NAME).setOptional(true));
+            kind.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
+                    FLAGS_PERSON_NAME));
+            kind.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
+                    FLAGS_PERSON_NAME).setOptional(true));
+            kind.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
+                    FLAGS_PERSON_NAME));
+            kind.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
+                    FLAGS_PERSON_NAME).setOptional(true));
+            kind.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME,
+                    R.string.name_phonetic_given, FLAGS_PHONETIC).setOptional(true));
+            kind.fieldList.add(new EditField(StructuredName.PHONETIC_MIDDLE_NAME,
+                    R.string.name_phonetic_middle, FLAGS_PHONETIC).setOptional(true));
+            kind.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME,
+                    R.string.name_phonetic_family, FLAGS_PHONETIC).setOptional(true));
+        }
+
+        return kind;
+    }
+
+    protected DataKind inflateNickname(int inflateLevel) {
+        DataKind kind = getKindForMimetype(Nickname.CONTENT_ITEM_TYPE);
+        if (kind == null) {
+            kind = addKind(new DataKind(Nickname.CONTENT_ITEM_TYPE,
+                    R.string.nicknameLabelsGroup, -1, 115, true));
+            kind.secondary = true;
+            kind.actionHeader = new SimpleInflater(R.string.nicknameLabelsGroup);
+            kind.actionBody = new SimpleInflater(Nickname.NAME);
+        }
+
+        if (inflateLevel >= ContactsSource.LEVEL_CONSTRAINTS) {
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(Nickname.NAME, R.string.nicknameLabelsGroup,
+                    FLAGS_PERSON_NAME));
+        }
+
+        return kind;
+    }
+
+    protected DataKind inflatePhone(int inflateLevel) {
+        DataKind kind = getKindForMimetype(Phone.CONTENT_ITEM_TYPE);
+        if (kind == null) {
+            kind = addKind(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 PhoneActionInflater();
+            kind.actionAltHeader = new PhoneActionAltInflater();
+            kind.actionBody = new SimpleInflater(Phone.NUMBER);
+        }
+
+        if (inflateLevel >= ContactsSource.LEVEL_CONSTRAINTS) {
+            kind.typeColumn = Phone.TYPE;
+            kind.typeList = Lists.newArrayList();
+            kind.typeList.add(buildPhoneType(Phone.TYPE_HOME));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_WORK));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_CUSTOM).setSecondary(true).setCustomColumn(
+                    Phone.LABEL));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_CALLBACK).setSecondary(true));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_CAR).setSecondary(true));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_COMPANY_MAIN).setSecondary(true));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_ISDN).setSecondary(true));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_MAIN).setSecondary(true));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER_FAX).setSecondary(true));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_RADIO).setSecondary(true));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_TELEX).setSecondary(true));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_TTY_TDD).setSecondary(true));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_WORK_MOBILE).setSecondary(true));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_WORK_PAGER).setSecondary(true));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_ASSISTANT).setSecondary(true)
+                    .setCustomColumn(Phone.LABEL));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_MMS).setSecondary(true));
+
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+        }
+
+        return kind;
+    }
+
+    protected DataKind inflateEmail(int inflateLevel) {
+        DataKind kind = getKindForMimetype(Email.CONTENT_ITEM_TYPE);
+        if (kind == null) {
+            kind = addKind(new DataKind(Email.CONTENT_ITEM_TYPE,
+                    R.string.emailLabelsGroup, android.R.drawable.sym_action_email, 15, true));
+            kind.actionHeader = new EmailActionInflater();
+            kind.actionBody = new SimpleInflater(Email.DATA);
+        }
+
+        if (inflateLevel >= ContactsSource.LEVEL_CONSTRAINTS) {
+            kind.typeColumn = Email.TYPE;
+            kind.typeList = Lists.newArrayList();
+            kind.typeList.add(buildEmailType(Email.TYPE_HOME));
+            kind.typeList.add(buildEmailType(Email.TYPE_WORK));
+            kind.typeList.add(buildEmailType(Email.TYPE_OTHER));
+            kind.typeList.add(buildEmailType(Email.TYPE_MOBILE));
+            kind.typeList.add(buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(
+                    Email.LABEL));
+
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+        }
+
+        return kind;
+    }
+
+    protected DataKind inflateStructuredPostal(int inflateLevel) {
+        DataKind kind = getKindForMimetype(StructuredPostal.CONTENT_ITEM_TYPE);
+        if (kind == null) {
+            kind = addKind(new DataKind(StructuredPostal.CONTENT_ITEM_TYPE,
+                    R.string.postalLabelsGroup, R.drawable.sym_action_map, 25, true));
+            kind.actionHeader = new PostalActionInflater();
+            kind.actionBody = new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS);
+        }
+
+        if (inflateLevel >= ContactsSource.LEVEL_CONSTRAINTS) {
+            kind.typeColumn = StructuredPostal.TYPE;
+            kind.typeList = Lists.newArrayList();
+            kind.typeList.add(buildPostalType(StructuredPostal.TYPE_HOME));
+            kind.typeList.add(buildPostalType(StructuredPostal.TYPE_WORK));
+            kind.typeList.add(buildPostalType(StructuredPostal.TYPE_OTHER));
+            kind.typeList.add(buildPostalType(StructuredPostal.TYPE_CUSTOM).setSecondary(true)
+                    .setCustomColumn(StructuredPostal.LABEL));
+
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(StructuredPostal.STREET, R.string.postal_street,
+                    FLAGS_POSTAL));
+            kind.fieldList.add(new EditField(StructuredPostal.POBOX, R.string.postal_pobox,
+                    FLAGS_POSTAL).setOptional(true));
+            kind.fieldList.add(new EditField(StructuredPostal.NEIGHBORHOOD,
+                    R.string.postal_neighborhood, FLAGS_POSTAL).setOptional(true));
+            kind.fieldList.add(new EditField(StructuredPostal.CITY, R.string.postal_city,
+                    FLAGS_POSTAL));
+            kind.fieldList.add(new EditField(StructuredPostal.REGION, R.string.postal_region,
+                    FLAGS_POSTAL));
+            kind.fieldList.add(new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode,
+                    FLAGS_POSTAL));
+            kind.fieldList.add(new EditField(StructuredPostal.COUNTRY, R.string.postal_country,
+                    FLAGS_POSTAL).setOptional(true));
+        }
+
+        return kind;
+    }
+
+    protected DataKind inflateIm(int inflateLevel) {
+        DataKind kind = getKindForMimetype(Im.CONTENT_ITEM_TYPE);
+        if (kind == null) {
+            kind = addKind(new DataKind(Im.CONTENT_ITEM_TYPE, R.string.imLabelsGroup,
+                    android.R.drawable.sym_action_chat, 20, true));
+            kind.actionHeader = new ImActionInflater();
+            kind.actionBody = new SimpleInflater(Im.DATA);
+        }
+
+        if (inflateLevel >= ContactsSource.LEVEL_CONSTRAINTS) {
+            // NOTE: even though a traditional "type" exists, for editing
+            // purposes we're using the protocol to pick labels
+
+            kind.defaultValues = new ContentValues();
+            kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER);
+
+            kind.typeColumn = Im.PROTOCOL;
+            kind.typeList = Lists.newArrayList();
+            kind.typeList.add(buildImType(Im.PROTOCOL_AIM));
+            kind.typeList.add(buildImType(Im.PROTOCOL_MSN));
+            kind.typeList.add(buildImType(Im.PROTOCOL_YAHOO));
+            kind.typeList.add(buildImType(Im.PROTOCOL_SKYPE));
+            kind.typeList.add(buildImType(Im.PROTOCOL_QQ));
+            kind.typeList.add(buildImType(Im.PROTOCOL_GOOGLE_TALK));
+            kind.typeList.add(buildImType(Im.PROTOCOL_ICQ));
+            kind.typeList.add(buildImType(Im.PROTOCOL_JABBER));
+            kind.typeList.add(buildImType(Im.PROTOCOL_CUSTOM).setSecondary(true).setCustomColumn(
+                    Im.CUSTOM_PROTOCOL));
+
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
+        }
+
+        return kind;
+    }
+
+    protected DataKind inflateOrganization(int inflateLevel) {
+        DataKind kind = getKindForMimetype(Organization.CONTENT_ITEM_TYPE);
+        if (kind == null) {
+            kind = addKind(new DataKind(Organization.CONTENT_ITEM_TYPE,
+                    R.string.organizationLabelsGroup, R.drawable.sym_action_organization, 30, true));
+            kind.actionHeader = new SimpleInflater(Organization.COMPANY);
+            kind.actionBody = new SimpleInflater(Organization.TITLE);
+        }
+
+        if (inflateLevel >= ContactsSource.LEVEL_CONSTRAINTS) {
+            kind.typeColumn = Organization.TYPE;
+            kind.typeList = Lists.newArrayList();
+            kind.typeList.add(buildOrgType(Organization.TYPE_WORK));
+            kind.typeList.add(buildOrgType(Organization.TYPE_OTHER));
+            kind.typeList.add(buildOrgType(Organization.TYPE_CUSTOM).setSecondary(true)
+                    .setCustomColumn(Organization.LABEL));
+
+            kind.fieldList = Lists.newArrayList();
+            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));
+        }
+
+        return kind;
+    }
+
+    protected DataKind inflatePhoto(int inflateLevel) {
+        DataKind kind = getKindForMimetype(Photo.CONTENT_ITEM_TYPE);
+        if (kind == null) {
+            kind = addKind(new DataKind(Photo.CONTENT_ITEM_TYPE, -1, -1, -1, true));
+        }
+
+        if (inflateLevel >= ContactsSource.LEVEL_CONSTRAINTS) {
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1));
+        }
+
+        return kind;
+    }
+
+    protected DataKind inflateNote(int inflateLevel) {
+        DataKind kind = getKindForMimetype(Note.CONTENT_ITEM_TYPE);
+        if (kind == null) {
+            kind = addKind(new DataKind(Note.CONTENT_ITEM_TYPE,
+                    R.string.label_notes, R.drawable.sym_note, 110, true));
+            kind.secondary = true;
+            kind.actionHeader = new SimpleInflater(R.string.label_notes);
+            kind.actionBody = new SimpleInflater(Note.NOTE);
+        }
+
+        if (inflateLevel >= ContactsSource.LEVEL_CONSTRAINTS) {
+
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE));
+        }
+
+        return kind;
+    }
+
+    protected DataKind inflateWebsite(int inflateLevel) {
+        DataKind kind = getKindForMimetype(Website.CONTENT_ITEM_TYPE);
+        if (kind == null) {
+            kind = addKind(new DataKind(Website.CONTENT_ITEM_TYPE,
+                    R.string.websiteLabelsGroup, -1, 120, true));
+            kind.secondary = true;
+            kind.actionHeader = new SimpleInflater(R.string.websiteLabelsGroup);
+            kind.actionBody = new SimpleInflater(Website.URL);
+        }
+
+        if (inflateLevel >= ContactsSource.LEVEL_CONSTRAINTS) {
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE));
+        }
+
+        return kind;
+    }
+
+    /**
+     * 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 int mStringRes;
+        private final String mColumnName;
+
+        public SimpleInflater(int stringRes) {
+            this(stringRes, null);
+        }
+
+        public SimpleInflater(String columnName) {
+            this(-1, columnName);
+        }
+
+        public SimpleInflater(int stringRes, String columnName) {
+            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.getText(mStringRes) : 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;
+            }
+        }
+
+        public CharSequence inflateUsing(Context context, ContentValues values) {
+            final boolean validColumn = values.containsKey(mColumnName);
+            final boolean validString = mStringRes > 0;
+
+            final CharSequence stringValue = validString ? context.getText(mStringRes) : null;
+            final CharSequence columnValue = validColumn ? values.getAsString(mColumnName) : null;
+
+            if (validString && validColumn) {
+                return String.format(stringValue.toString(), columnValue);
+            } else if (validString) {
+                return stringValue;
+            } else if (validColumn) {
+                return columnValue;
+            } else {
+                return null;
+            }
+        }
+    }
+
+    public static abstract class CommonInflater implements StringInflater {
+        protected abstract int getTypeLabelResource(Integer type);
+
+        protected boolean isCustom(Integer type) {
+            return type == BaseTypes.TYPE_CUSTOM;
+        }
+
+        protected CharSequence getTypeLabel(Resources res, Integer type, CharSequence label) {
+            final int labelRes = getTypeLabelResource(type);
+            if (type == null) {
+                return res.getText(labelRes);
+            } else if (isCustom(type)) {
+                return res.getString(labelRes, label == null ? "" : label);
+            } else {
+                return res.getText(labelRes);
+            }
+        }
+
+        public CharSequence inflateUsing(Context context, Cursor cursor) {
+            final Integer type = cursor.getInt(cursor.getColumnIndex(Phone.TYPE));
+            final String label = cursor.getString(cursor.getColumnIndex(Phone.LABEL));
+            return getTypeLabel(context.getResources(), type, label);
+        }
+
+        public CharSequence inflateUsing(Context context, ContentValues values) {
+            final Integer type = values.getAsInteger(Phone.TYPE);
+            final String label = values.getAsString(Phone.LABEL);
+            return getTypeLabel(context.getResources(), type, label);
+        }
+    }
+
+    public static class PhoneActionInflater extends CommonInflater {
+        @Override
+        protected boolean isCustom(Integer type) {
+            return type == Phone.TYPE_CUSTOM || type == Phone.TYPE_ASSISTANT;
+        }
+
+        @Override
+        protected int getTypeLabelResource(Integer type) {
+            if (type == null) return R.string.call_other;
+            switch (type) {
+                case Phone.TYPE_HOME: return R.string.call_home;
+                case Phone.TYPE_MOBILE: return R.string.call_mobile;
+                case Phone.TYPE_WORK: return R.string.call_work;
+                case Phone.TYPE_FAX_WORK: return R.string.call_fax_work;
+                case Phone.TYPE_FAX_HOME: return R.string.call_fax_home;
+                case Phone.TYPE_PAGER: return R.string.call_pager;
+                case Phone.TYPE_OTHER: return R.string.call_other;
+                case Phone.TYPE_CALLBACK: return R.string.call_callback;
+                case Phone.TYPE_CAR: return R.string.call_car;
+                case Phone.TYPE_COMPANY_MAIN: return R.string.call_company_main;
+                case Phone.TYPE_ISDN: return R.string.call_isdn;
+                case Phone.TYPE_MAIN: return R.string.call_main;
+                case Phone.TYPE_OTHER_FAX: return R.string.call_other_fax;
+                case Phone.TYPE_RADIO: return R.string.call_radio;
+                case Phone.TYPE_TELEX: return R.string.call_telex;
+                case Phone.TYPE_TTY_TDD: return R.string.call_tty_tdd;
+                case Phone.TYPE_WORK_MOBILE: return R.string.call_work_mobile;
+                case Phone.TYPE_WORK_PAGER: return R.string.call_work_pager;
+                case Phone.TYPE_ASSISTANT: return R.string.call_assistant;
+                case Phone.TYPE_MMS: return R.string.call_mms;
+                default: return R.string.call_custom;
+            }
+        }
+    }
+
+    public static class PhoneActionAltInflater extends CommonInflater {
+        @Override
+        protected boolean isCustom(Integer type) {
+            return (type == Phone.TYPE_CUSTOM || type == Phone.TYPE_ASSISTANT);
+        }
+
+        @Override
+        protected int getTypeLabelResource(Integer type) {
+            if (type == null) return R.string.sms_other;
+            switch (type) {
+                case Phone.TYPE_HOME: return R.string.sms_home;
+                case Phone.TYPE_MOBILE: return R.string.sms_mobile;
+                case Phone.TYPE_WORK: return R.string.sms_work;
+                case Phone.TYPE_FAX_WORK: return R.string.sms_fax_work;
+                case Phone.TYPE_FAX_HOME: return R.string.sms_fax_home;
+                case Phone.TYPE_PAGER: return R.string.sms_pager;
+                case Phone.TYPE_OTHER: return R.string.sms_other;
+                case Phone.TYPE_CALLBACK: return R.string.sms_callback;
+                case Phone.TYPE_CAR: return R.string.sms_car;
+                case Phone.TYPE_COMPANY_MAIN: return R.string.sms_company_main;
+                case Phone.TYPE_ISDN: return R.string.sms_isdn;
+                case Phone.TYPE_MAIN: return R.string.sms_main;
+                case Phone.TYPE_OTHER_FAX: return R.string.sms_other_fax;
+                case Phone.TYPE_RADIO: return R.string.sms_radio;
+                case Phone.TYPE_TELEX: return R.string.sms_telex;
+                case Phone.TYPE_TTY_TDD: return R.string.sms_tty_tdd;
+                case Phone.TYPE_WORK_MOBILE: return R.string.sms_work_mobile;
+                case Phone.TYPE_WORK_PAGER: return R.string.sms_work_pager;
+                case Phone.TYPE_ASSISTANT: return R.string.sms_assistant;
+                case Phone.TYPE_MMS: return R.string.sms_mms;
+                default: return R.string.sms_custom;
+            }
+        }
+    }
+
+    public static class EmailActionInflater extends CommonInflater {
+        @Override
+        protected int getTypeLabelResource(Integer type) {
+            if (type == null) return R.string.email;
+            switch (type) {
+                case Email.TYPE_HOME: return R.string.email_home;
+                case Email.TYPE_WORK: return R.string.email_work;
+                case Email.TYPE_OTHER: return R.string.email_other;
+                case Email.TYPE_MOBILE: return R.string.email_mobile;
+                default: return R.string.email_custom;
+            }
+        }
+    }
+
+    public static class PostalActionInflater extends CommonInflater {
+        @Override
+        protected int getTypeLabelResource(Integer type) {
+            if (type == null) return R.string.map_other;
+            switch (type) {
+                case StructuredPostal.TYPE_HOME: return R.string.map_home;
+                case StructuredPostal.TYPE_WORK: return R.string.map_work;
+                case StructuredPostal.TYPE_OTHER: return R.string.map_other;
+                default: return R.string.map_custom;
+            }
+        }
+    }
+
+    public static class ImActionInflater extends CommonInflater {
+        @Override
+        protected int getTypeLabelResource(Integer type) {
+            if (type == null) return R.string.chat;
+            switch (type) {
+                case Im.PROTOCOL_AIM: return R.string.chat_aim;
+                case Im.PROTOCOL_MSN: return R.string.chat_msn;
+                case Im.PROTOCOL_YAHOO: return R.string.chat_yahoo;
+                case Im.PROTOCOL_SKYPE: return R.string.chat_skype;
+                case Im.PROTOCOL_QQ: return R.string.chat_qq;
+                case Im.PROTOCOL_GOOGLE_TALK: return R.string.chat_gtalk;
+                case Im.PROTOCOL_ICQ: return R.string.chat_icq;
+                case Im.PROTOCOL_JABBER: return R.string.chat_jabber;
+                case Im.PROTOCOL_NETMEETING: return R.string.chat;
+                default: return R.string.chat;
+            }
+        }
+    }
+}
diff --git a/src/com/android/contacts/model/GoogleSource.java b/src/com/android/contacts/model/GoogleSource.java
new file mode 100644
index 0000000..46d0623
--- /dev/null
+++ b/src/com/android/contacts/model/GoogleSource.java
@@ -0,0 +1,247 @@
+/*
+ * 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.EntityDelta.ValuesDelta;
+import com.google.android.collect.Lists;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts.Data;
+
+import java.util.ArrayList;
+
+public class GoogleSource extends FallbackSource {
+    public static final String ACCOUNT_TYPE = "com.google.GAIA";
+
+    public GoogleSource(String resPackageName) {
+        this.accountType = ACCOUNT_TYPE;
+        this.resPackageName = null;
+        this.summaryResPackageName = resPackageName;
+    }
+
+    @Override
+    protected void inflate(Context context, int inflateLevel) {
+
+        inflateStructuredName(inflateLevel);
+        inflateNickname(inflateLevel);
+        inflatePhone(inflateLevel);
+        inflateEmail(inflateLevel);
+        inflateStructuredPostal(inflateLevel);
+        inflateIm(inflateLevel);
+        inflateOrganization(inflateLevel);
+        inflatePhoto(inflateLevel);
+        inflateNote(inflateLevel);
+        inflateWebsite(inflateLevel);
+
+        // TODO: GOOGLE: GROUPMEMBERSHIP
+
+        setInflatedLevel(inflateLevel);
+
+    }
+
+    @Override
+    protected DataKind inflateStructuredName(int inflateLevel) {
+        return super.inflateStructuredName(inflateLevel);
+    }
+
+    @Override
+    protected DataKind inflateNickname(int inflateLevel) {
+        return super.inflateNickname(inflateLevel);
+    }
+
+    @Override
+    protected DataKind inflatePhone(int inflateLevel) {
+        final DataKind kind = super.inflatePhone(ContactsSource.LEVEL_MIMETYPES);
+
+        if (inflateLevel >= ContactsSource.LEVEL_CONSTRAINTS) {
+            kind.typeColumn = Phone.TYPE;
+            kind.typeList = Lists.newArrayList();
+            kind.typeList.add(buildPhoneType(Phone.TYPE_HOME));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_WORK));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER));
+            kind.typeList.add(buildPhoneType(Phone.TYPE_CUSTOM).setSecondary(true).setCustomColumn(
+                    Phone.LABEL));
+
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+        }
+
+        return kind;
+    }
+
+    @Override
+    protected DataKind inflateEmail(int inflateLevel) {
+        final DataKind kind = super.inflateEmail(ContactsSource.LEVEL_MIMETYPES);
+
+        if (inflateLevel >= ContactsSource.LEVEL_CONSTRAINTS) {
+            kind.typeColumn = Email.TYPE;
+            kind.typeList = Lists.newArrayList();
+            kind.typeList.add(buildEmailType(Email.TYPE_HOME));
+            kind.typeList.add(buildEmailType(Email.TYPE_WORK));
+            kind.typeList.add(buildEmailType(Email.TYPE_OTHER));
+            kind.typeList.add(buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(
+                    Email.LABEL));
+
+            kind.fieldList = Lists.newArrayList();
+            kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+        }
+
+        return kind;
+    }
+
+    @Override
+    protected DataKind inflateStructuredPostal(int inflateLevel) {
+        return super.inflateStructuredPostal(inflateLevel);
+    }
+
+    @Override
+    protected DataKind inflateIm(int inflateLevel) {
+        return super.inflateIm(inflateLevel);
+    }
+
+    @Override
+    protected DataKind inflateOrganization(int inflateLevel) {
+        return super.inflateOrganization(inflateLevel);
+    }
+
+    @Override
+    protected DataKind inflatePhoto(int inflateLevel) {
+        return super.inflatePhoto(inflateLevel);
+    }
+
+    @Override
+    protected DataKind inflateNote(int inflateLevel) {
+        return super.inflateNote(inflateLevel);
+    }
+
+    @Override
+    protected DataKind inflateWebsite(int inflateLevel) {
+        return super.inflateWebsite(inflateLevel);
+    }
+
+    // TODO: this should come from resource in the future
+    private static final String GOOGLE_MY_CONTACTS_GROUP = "System Group: My Contacts";
+
+    public static final void attemptMyContactsMembership(EntityDelta state, Context context) {
+        attemptMyContactsMembership(state, context, true);
+    }
+
+    /**
+     *
+     * @param allowRecur If the group is created between querying/about to create, we recur.  But
+     *     to prevent excess recursion, we provide a flag to make sure we only do the recursion loop
+     *     once
+     */
+    private static final void attemptMyContactsMembership(EntityDelta state, Context context,
+            boolean allowRecur) {
+        final ContentResolver resolver = context.getContentResolver();
+        final ValuesDelta stateValues = state.getValues();
+        final String accountName = stateValues.getAsString(RawContacts.ACCOUNT_NAME);
+        final String accountType = stateValues.getAsString(RawContacts.ACCOUNT_TYPE);
+
+        Cursor cursor = resolver.query(Groups.CONTENT_URI,
+                new String[] {Groups.TITLE, Groups.SOURCE_ID, Groups.SHOULD_SYNC},
+                Groups.ACCOUNT_NAME + " =? AND " + Groups.ACCOUNT_TYPE + " =?",
+                new String[] {accountName, accountType}, null);
+
+        boolean myContactsExists = false;
+        long assignToGroupSourceId = -1;
+        while (cursor.moveToNext()) {
+            if (GOOGLE_MY_CONTACTS_GROUP.equals(cursor.getString(0))) {
+                myContactsExists = true;
+            }
+            if (assignToGroupSourceId == -1 && cursor.getInt(2) != 0) {
+                assignToGroupSourceId = cursor.getInt(1);
+            }
+
+            if (myContactsExists && assignToGroupSourceId != -1) {
+                break;
+            }
+        }
+
+        try {
+            final ContentValues values = new ContentValues();
+            values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
+
+            if (!myContactsExists) {
+                // create the group if it doesn't exist
+                final ContentValues newGroup = new ContentValues();
+                newGroup.put(Groups.TITLE, GOOGLE_MY_CONTACTS_GROUP);
+
+                newGroup.put(Groups.ACCOUNT_NAME, accountName);
+                newGroup.put(Groups.ACCOUNT_TYPE, accountType);
+                newGroup.put(Groups.GROUP_VISIBLE, "1");
+
+                ArrayList<ContentProviderOperation> operations =
+                    new ArrayList<ContentProviderOperation>();
+
+                operations.add(ContentProviderOperation
+                        .newAssertQuery(Groups.CONTENT_URI)
+                        .withSelection(Groups.TITLE + "=?",
+                                new String[] { GOOGLE_MY_CONTACTS_GROUP })
+                        .withExpectedCount(0).build());
+                operations.add(ContentProviderOperation
+
+                        .newInsert(Groups.CONTENT_URI)
+                        .withValues(newGroup)
+                        .build());
+                try {
+                    ContentProviderResult[] results = resolver.applyBatch(
+                            ContactsContract.AUTHORITY, operations);
+                    values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(results[1].uri));
+                } catch (RemoteException e) {
+                    throw new IllegalStateException("Problem querying for groups", e);
+                } catch (OperationApplicationException e) {
+                    // the group was created after the query but before we tried to create it
+                    if (allowRecur) {
+                        attemptMyContactsMembership(state, context, false);
+                    }
+                    return;
+                }
+            } else {
+                if (assignToGroupSourceId != -1) {
+                    values.put(GroupMembership.GROUP_SOURCE_ID, assignToGroupSourceId);
+                } else {
+                    // there are no Groups to add this contact to, so don't apply any membership
+                    // TODO: alert user that their contact will be dropped?
+                }
+            }
+            state.addEntry(ValuesDelta.fromAfter(values));
+        } finally {
+            cursor.close();
+        }
+    }
+}
diff --git a/src/com/android/contacts/model/HardCodedSources.java b/src/com/android/contacts/model/HardCodedSources.java
deleted file mode 100644
index 503eed8..0000000
--- a/src/com/android/contacts/model/HardCodedSources.java
+++ /dev/null
@@ -1,1009 +0,0 @@
-/*
- * 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 android.content.ContentProviderOperation;
-import android.content.ContentProviderResult;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.OperationApplicationException;
-import android.database.Cursor;
-import android.os.RemoteException;
-import android.provider.ContactsContract;
-import android.provider.ContactsContract.Groups;
-import android.provider.ContactsContract.RawContacts;
-import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
-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.provider.ContactsContract.Contacts.Data;
-import android.view.inputmethod.EditorInfo;
-
-import com.google.android.collect.Lists;
-
-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 com.android.contacts.model.EntityDelta.ValuesDelta;
-
-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";
-    public static final String ACCOUNT_TYPE_FALLBACK = "com.example.fallback-contacts";
-
-    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 fallback use.
-     */
-    static void buildFallback(Context context, ContactsSource list) {
-        {
-            // FALLBACK: STRUCTUREDNAME
-            DataKind kind = new DataKind(StructuredName.CONTENT_ITEM_TYPE,
-                    R.string.nameLabelsGroup, -1, -1, true);
-
-            kind.fieldList = Lists.newArrayList();
-            kind.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
-                    FLAGS_PERSON_NAME));
-            kind.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
-                    FLAGS_PERSON_NAME));
-
-            list.add(kind);
-        }
-
-        {
-            // FALLBACK: 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 = Lists.newArrayList();
-            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.typeList.add(new EditType(Phone.TYPE_CAR, R.string.type_car, R.string.call_car,
-                    R.string.sms_car).setSecondary(true));
-            kind.typeList.add(new EditType(Phone.TYPE_COMPANY_MAIN, R.string.type_company_main,
-                    R.string.call_company_main, R.string.sms_company_main).setSecondary(true));
-            kind.typeList.add(new EditType(Phone.TYPE_MMS, R.string.type_mms, R.string.call_mms,
-                    R.string.sms_mms).setSecondary(true));
-            kind.typeList.add(new EditType(Phone.TYPE_RADIO, R.string.type_radio, R.string.call_radio,
-                    R.string.sms_radio).setSecondary(true));
-
-            kind.fieldList = Lists.newArrayList();
-            kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
-
-            list.add(kind);
-        }
-
-        {
-            // FALLBACK: 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 = Lists.newArrayList();
-            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 = Lists.newArrayList();
-            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);
-        }
-
-        {
-            // FALLBACK: 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 = Lists.newArrayList();
-            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 = Lists.newArrayList();
-            kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
-
-            list.add(kind);
-        }
-    }
-
-    /**
-     * Hard-coded instance of {@link ContactsSource} for Google Contacts.
-     */
-    static void buildGoogle(Context context, ContactsSource list) {
-        {
-            // GOOGLE: STRUCTUREDNAME
-            DataKind kind = new DataKind(StructuredName.CONTENT_ITEM_TYPE,
-                    R.string.nameLabelsGroup, -1, -1, true);
-
-            kind.fieldList = Lists.newArrayList();
-            kind.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
-                    FLAGS_PERSON_NAME, true));
-            kind.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
-                    FLAGS_PERSON_NAME));
-            kind.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
-                    FLAGS_PERSON_NAME, true));
-            kind.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
-                    FLAGS_PERSON_NAME));
-            kind.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
-                    FLAGS_PERSON_NAME, true));
-            kind.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME,
-                    R.string.name_phonetic_given, FLAGS_PHONETIC, true));
-            kind.fieldList.add(new EditField(StructuredName.PHONETIC_MIDDLE_NAME,
-                    R.string.name_phonetic_middle, FLAGS_PHONETIC, true));
-            kind.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME,
-                    R.string.name_phonetic_family, FLAGS_PHONETIC, true));
-
-            list.add(kind);
-        }
-
-        {
-            // GOOGLE: PHOTO
-            DataKind kind = new DataKind(Photo.CONTENT_ITEM_TYPE, -1, -1, -1, true);
-
-            kind.fieldList = Lists.newArrayList();
-            kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1));
-
-            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 = Lists.newArrayList();
-            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 = Lists.newArrayList();
-            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 = Lists.newArrayList();
-            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 = Lists.newArrayList();
-            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 protocol to pick labels
-
-            kind.defaultValues = new ContentValues();
-            kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER);
-
-            kind.typeColumn = Im.PROTOCOL;
-            kind.typeList = Lists.newArrayList();
-            kind.typeList.add(new EditType(Im.PROTOCOL_AIM, R.string.type_im_aim,
-                    R.string.chat_aim));
-            kind.typeList.add(new EditType(Im.PROTOCOL_MSN, R.string.type_im_msn,
-                    R.string.chat_msn));
-            kind.typeList.add(new EditType(Im.PROTOCOL_YAHOO, R.string.type_im_yahoo,
-                    R.string.chat_yahoo));
-            kind.typeList.add(new EditType(Im.PROTOCOL_SKYPE, R.string.type_im_skype,
-                    R.string.chat_skype));
-            kind.typeList.add(new EditType(Im.PROTOCOL_QQ, R.string.type_im_qq, R.string.chat_qq));
-            kind.typeList.add(new EditType(Im.PROTOCOL_GOOGLE_TALK, R.string.type_im_google_talk,
-                    R.string.chat_gtalk));
-            kind.typeList.add(new EditType(Im.PROTOCOL_ICQ, R.string.type_im_icq,
-                    R.string.chat_icq));
-            kind.typeList.add(new EditType(Im.PROTOCOL_JABBER, R.string.type_im_jabber,
-                    R.string.chat_jabber));
-            kind.typeList.add(new EditType(Im.PROTOCOL_CUSTOM, R.string.type_custom,
-                    R.string.chat_other).setSecondary(true).setCustomColumn(Im.CUSTOM_PROTOCOL));
-
-            kind.fieldList = Lists.newArrayList();
-            kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
-
-            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 = Lists.newArrayList();
-            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 = Lists.newArrayList();
-            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(Organization.COMPANY);
-            // TODO: build body from multiple fields
-            kind.actionBody = new SimpleInflater(Organization.TITLE);
-
-            kind.typeColumn = Organization.TYPE;
-            kind.typeList = Lists.newArrayList();
-            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 = Lists.newArrayList();
-            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 = Lists.newArrayList();
-            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 = Lists.newArrayList();
-            kind.fieldList.add(new EditField(Nickname.NAME, R.string.nicknameLabelsGroup,
-                    FLAGS_PERSON_NAME));
-
-            list.add(kind);
-        }
-
-        // TODO: GOOGLE: GROUPMEMBERSHIP
-
-        {
-            // GOOGLE: WEBSITE
-            DataKind kind = new DataKind(Website.CONTENT_ITEM_TYPE,
-                    R.string.websiteLabelsGroup, -1, 120, true);
-            kind.secondary = true;
-
-            kind.actionHeader = new SimpleInflater(list.resPackageName, R.string.websiteLabelsGroup);
-            kind.actionBody = new SimpleInflater(Website.URL);
-
-            kind.fieldList = Lists.newArrayList();
-            kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE));
-
-            list.add(kind);
-        }
-    }
-
-    // TODO: this should come from resource in the future
-    private static final String GOOGLE_MY_CONTACTS_GROUP = "System Group: My Contacts";
-
-    public static final void attemptMyContactsMembership(EntityDelta state, Context context) {
-        attemptMyContactsMembership(state, context, true);
-    }
-
-    /**
-     *
-     * @param allowRecur If the group is created between querying/about to create, we recur.  But
-     *     to prevent excess recursion, we provide a flag to make sure we only do the recursion loop
-     *     once
-     */
-    private static final void attemptMyContactsMembership(EntityDelta state, Context context,
-            boolean allowRecur) {
-        final ContentResolver resolver = context.getContentResolver();
-        final ValuesDelta stateValues = state.getValues();
-        final String accountName = stateValues.getAsString(RawContacts.ACCOUNT_NAME);
-        final String accountType = stateValues.getAsString(RawContacts.ACCOUNT_TYPE);
-
-        Cursor cursor = resolver.query(Groups.CONTENT_URI,
-                new String[] {Groups.TITLE, Groups.SOURCE_ID, Groups.SHOULD_SYNC},
-                Groups.ACCOUNT_NAME + " =? AND " + Groups.ACCOUNT_TYPE + " =?",
-                new String[] {accountName, accountType}, null);
-
-        boolean myContactsExists = false;
-        long assignToGroupSourceId = -1;
-        while (cursor.moveToNext()) {
-            if (GOOGLE_MY_CONTACTS_GROUP.equals(cursor.getString(0))) {
-                myContactsExists = true;
-            }
-            if (assignToGroupSourceId == -1 && cursor.getInt(2) != 0) {
-                assignToGroupSourceId = cursor.getInt(1);
-            }
-
-            if (myContactsExists && assignToGroupSourceId != -1) {
-                break;
-            }
-        }
-
-        try {
-            final ContentValues values = new ContentValues();
-            values.put(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
-
-            if (!myContactsExists) {
-                // create the group if it doesn't exist
-                final ContentValues newGroup = new ContentValues();
-                newGroup.put(Groups.TITLE, GOOGLE_MY_CONTACTS_GROUP);
-
-                newGroup.put(Groups.ACCOUNT_NAME, accountName);
-                newGroup.put(Groups.ACCOUNT_TYPE, accountType);
-                newGroup.put(Groups.GROUP_VISIBLE, "1");
-
-                ArrayList<ContentProviderOperation> operations =
-                    new ArrayList<ContentProviderOperation>();
-
-                operations.add(ContentProviderOperation
-                        .newAssertQuery(Groups.CONTENT_URI)
-                        .withSelection(Groups.TITLE + "=?",
-                                new String[] { GOOGLE_MY_CONTACTS_GROUP })
-                        .withExpectedCount(0).build());
-                operations.add(ContentProviderOperation
-
-                        .newInsert(Groups.CONTENT_URI)
-                        .withValues(newGroup)
-                        .build());
-                try {
-                    ContentProviderResult[] results = resolver.applyBatch(
-                            ContactsContract.AUTHORITY, operations);
-                    values.put(GroupMembership.GROUP_ROW_ID, ContentUris.parseId(results[1].uri));
-                } catch (RemoteException e) {
-                    throw new IllegalStateException("Problem querying for groups", e);
-                } catch (OperationApplicationException e) {
-                    // the group was created after the query but before we tried to create it
-                    if (allowRecur) {
-                        attemptMyContactsMembership(state, context, false);
-                    }
-                    return;
-                }
-            } else {
-                if (assignToGroupSourceId != -1) {
-                    values.put(GroupMembership.GROUP_SOURCE_ID, assignToGroupSourceId);
-                } else {
-                    // there are no Groups to add this contact to, so don't apply any membership
-                    // TODO: alert user that their contact will be dropped?
-                }
-            }
-            state.addEntry(ValuesDelta.fromAfter(values));
-        } finally {
-            cursor.close();
-        }
-    }
-
-    /**
-     * Hard-coded instance of {@link ContactsSource} for Exchange.
-     */
-    static void buildExchange(Context context, ContactsSource list) {
-        {
-            // EXCHANGE: STRUCTUREDNAME
-            DataKind kind = new DataKind(StructuredName.CONTENT_ITEM_TYPE,
-                    R.string.nameLabelsGroup, -1, -1, true);
-            kind.typeOverallMax = 1;
-
-            kind.fieldList = Lists.newArrayList();
-            kind.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
-                    FLAGS_PERSON_NAME, true));
-            kind.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
-                    FLAGS_PERSON_NAME));
-            kind.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
-                    FLAGS_PERSON_NAME, true));
-            kind.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
-                    FLAGS_PERSON_NAME));
-            kind.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
-                    FLAGS_PERSON_NAME, true));
-            kind.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME,
-                    R.string.name_phonetic_given, FLAGS_PHONETIC, true));
-            kind.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME,
-                    R.string.name_phonetic_family, FLAGS_PHONETIC, true));
-
-            list.add(kind);
-        }
-
-        {
-            // EXCHANGE: PHOTO
-            DataKind kind = new DataKind(Photo.CONTENT_ITEM_TYPE, -1, -1, -1, true);
-            kind.typeOverallMax = 1;
-
-            kind.fieldList = Lists.newArrayList();
-            kind.fieldList.add(new EditField(Photo.PHOTO, -1, -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 = Lists.newArrayList();
-            kind.typeList.add(new EditType(Phone.TYPE_HOME, R.string.type_home, R.string.call_home,
-                    R.string.sms_home).setSpecificMax(2));
-            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(2));
-            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(Phone.TYPE_CAR, R.string.type_car, R.string.call_car,
-                    R.string.sms_car).setSecondary(true).setSpecificMax(1));
-            kind.typeList.add(new EditType(Phone.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(Phone.TYPE_MMS, R.string.type_mms, R.string.call_mms,
-                    R.string.sms_mms).setSecondary(true).setSpecificMax(1));
-            kind.typeList.add(new EditType(Phone.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 = Lists.newArrayList();
-            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.typeOverallMax = 3;
-
-            kind.fieldList = Lists.newArrayList();
-            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.typeOverallMax = 3;
-
-            // NOTE: even though a traditional "type" exists, for editing
-            // purposes we're using the protocol to pick labels
-
-            kind.defaultValues = new ContentValues();
-            kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER);
-
-            kind.typeColumn = Im.PROTOCOL;
-            kind.typeList = Lists.newArrayList();
-            kind.typeList.add(new EditType(Im.PROTOCOL_AIM, R.string.type_im_aim,
-                    R.string.chat_aim));
-            kind.typeList.add(new EditType(Im.PROTOCOL_MSN, R.string.type_im_msn,
-                    R.string.chat_msn));
-            kind.typeList.add(new EditType(Im.PROTOCOL_YAHOO, R.string.type_im_yahoo,
-                    R.string.chat_yahoo));
-            kind.typeList.add(new EditType(Im.PROTOCOL_SKYPE, R.string.type_im_skype,
-                    R.string.chat_skype));
-            kind.typeList.add(new EditType(Im.PROTOCOL_QQ, R.string.type_im_qq, R.string.chat_qq));
-            kind.typeList.add(new EditType(Im.PROTOCOL_GOOGLE_TALK, R.string.type_im_google_talk,
-                    R.string.chat_gtalk));
-            kind.typeList.add(new EditType(Im.PROTOCOL_ICQ, R.string.type_im_icq,
-                    R.string.chat_icq));
-            kind.typeList.add(new EditType(Im.PROTOCOL_JABBER, R.string.type_im_jabber,
-                    R.string.chat_jabber));
-            kind.typeList.add(new EditType(Im.PROTOCOL_CUSTOM, R.string.type_custom,
-                    R.string.chat_other).setSecondary(true).setCustomColumn(Im.CUSTOM_PROTOCOL));
-
-            kind.fieldList = Lists.newArrayList();
-            kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
-
-            list.add(kind);
-        }
-
-        {
-            // EXCHANGE: 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 = Lists.newArrayList();
-            kind.typeList.add(new EditType(StructuredPostal.TYPE_WORK, R.string.type_work,
-                    R.string.map_work).setSpecificMax(1));
-            kind.typeList.add(new EditType(StructuredPostal.TYPE_HOME, R.string.type_home,
-                    R.string.map_home).setSpecificMax(1));
-            kind.typeList.add(new EditType(StructuredPostal.TYPE_OTHER, R.string.type_other,
-                    R.string.map_other).setSpecificMax(1));
-
-            kind.fieldList = Lists.newArrayList();
-            kind.fieldList.add(new EditField(StructuredPostal.STREET, R.string.postal_street,
-                    FLAGS_POSTAL));
-            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);
-        }
-
-        {
-            // 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 = Lists.newArrayList();
-            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 = Lists.newArrayList();
-            kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE));
-
-            list.add(kind);
-        }
-
-        {
-            // EXCHANGE: ORGANIZATION
-            DataKind kind = new DataKind(Organization.CONTENT_ITEM_TYPE,
-                    R.string.organizationLabelsGroup, R.drawable.sym_action_organization, 30, true);
-
-            kind.actionHeader = new SimpleInflater(Organization.COMPANY);
-            // TODO: build body from multiple fields
-            kind.actionBody = new SimpleInflater(Organization.TITLE);
-
-            kind.typeColumn = Organization.TYPE;
-            kind.typeList = Lists.newArrayList();
-            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 = Lists.newArrayList();
-            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);
-        }
-
-        {
-            // EXCHANGE: 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 = Lists.newArrayList();
-            kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE));
-
-            list.add(kind);
-        }
-    }
-
-    /**
-     * Hard-coded instance of {@link ContactsSource} for Facebook.
-     */
-    static void buildFacebook(Context context, ContactsSource list) {
-        list.accountType = ACCOUNT_TYPE_FACEBOOK;
-        list.readOnly = true;
-
-        {
-            // FACEBOOK: 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 = Lists.newArrayList();
-            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_OTHER, R.string.type_other,
-                    R.string.call_other, R.string.sms_other));
-
-            list.add(kind);
-        }
-
-        {
-            // FACEBOOK: 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 = Lists.newArrayList();
-            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 = Lists.newArrayList();
-            kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
-
-            list.add(kind);
-        }
-    }
-
-    /**
-     * 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.getText(mStringRes) : 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;
-            }
-        }
-
-        public CharSequence inflateUsing(Context context, ContentValues values) {
-            final boolean validColumn = values.containsKey(mColumnName);
-            final boolean validString = mStringRes > 0;
-
-            final CharSequence stringValue = validString ? context.getText(mStringRes) : null;
-            final CharSequence columnValue = validColumn ? values.getAsString(mColumnName) : 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);
-            if (!validString) return null;
-
-            if (type.customColumn != null) {
-                final int index = cursor.getColumnIndex(type.customColumn);
-                final String customLabel = cursor.getString(index);
-                return String.format(context.getString(type.actionRes), customLabel);
-            } else {
-                return context.getText(type.actionRes);
-            }
-        }
-
-        public CharSequence inflateUsing(Context context, ContentValues values) {
-            final EditType type = EntityModifier.getCurrentType(values, mKind);
-            final boolean validString = (type != null && type.actionRes != 0);
-            if (!validString) return null;
-
-            if (type.customColumn != null) {
-                final String customLabel = values.getAsString(type.customColumn);
-                return String.format(context.getString(type.actionRes), customLabel);
-            } else {
-                return context.getText(type.actionRes);
-            }
-        }
-    }
-
-    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);
-            CharSequence actionString;
-            if (type.customColumn != null) {
-                final int index = cursor.getColumnIndex(type.customColumn);
-                final String customLabel = cursor.getString(index);
-                actionString = String.format(context.getString(type.actionAltRes),
-                        customLabel);
-            } else {
-                actionString = context.getText(type.actionAltRes);
-            }
-            return validString ? actionString : null;
-        }
-
-        public CharSequence inflateUsing(Context context, ContentValues values) {
-            final EditType type = EntityModifier.getCurrentType(values, mKind);
-            final boolean validString = (type != null && type.actionAltRes != 0);
-            CharSequence actionString;
-            if (type.customColumn != null) {
-                final String customLabel = values.getAsString(type.customColumn);
-                actionString = String.format(context.getString(type.actionAltRes),
-                        customLabel);
-            } else {
-                actionString = context.getText(type.actionAltRes);
-            }
-            return validString ? actionString : null;
-        }
-    }
-}
diff --git a/src/com/android/contacts/model/Sources.java b/src/com/android/contacts/model/Sources.java
index 71607c7..ad9ddf1 100644
--- a/src/com/android/contacts/model/Sources.java
+++ b/src/com/android/contacts/model/Sources.java
@@ -16,38 +16,45 @@
 
 package com.android.contacts.model;
 
-import com.android.contacts.R;
+import com.android.contacts.model.ContactsSource.DataKind;
 import com.google.android.collect.Lists;
 import com.google.android.collect.Maps;
+import com.google.android.collect.Sets;
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.accounts.AuthenticatorDescription;
+import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.IContentService;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.SyncAdapterType;
 import android.content.pm.PackageManager;
 import android.os.RemoteException;
 import android.provider.ContactsContract;
+import android.text.TextUtils;
 import android.util.Log;
 
 import java.lang.ref.SoftReference;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 
 /**
  * Singleton holder for all parsed {@link ContactsSource} available on the
  * system, typically filled through {@link PackageManager} queries.
  */
-public class Sources {
+public class Sources extends BroadcastReceiver {
     private static final String TAG = "Sources";
 
-    public static final String ACCOUNT_TYPE_FALLBACK = HardCodedSources.ACCOUNT_TYPE_FALLBACK;
-
     private Context mContext;
 
+    private ContactsSource mFallbackSource = null;
+
     private HashMap<String, ContactsSource> mSources = Maps.newHashMap();
+    private HashSet<String> mKnownPackages = Sets.newHashSet();
 
     private static SoftReference<Sources> sInstance = null;
 
@@ -70,13 +77,58 @@
      */
     private Sources(Context context) {
         mContext = context;
+
+        // Create fallback contacts source for on-phone contacts
+        mFallbackSource = new FallbackSource();
+
         loadAccounts();
+        registerIntentReceivers(context);
     }
 
     /** @hide exposed for unit tests */
     public Sources(ContactsSource... sources) {
         for (ContactsSource source : sources) {
-            mSources.put(source.accountType, source);
+            addSource(source);
+        }
+    }
+
+    protected void addSource(ContactsSource source) {
+        mSources.put(source.accountType, source);
+        mKnownPackages.add(source.resPackageName);
+    }
+
+    private void registerIntentReceivers(Context context) {
+        IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
+        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+        filter.addDataScheme("package");
+
+        // We use getApplicationContext() so that the broadcast reciever can stay registered for
+        // the length of the application lifetime (instead of the calling activity's lifetime).
+        // This is so that we can notified of package changes, and purge the cache accordingly,
+        // but not be woken up if the application process isn't already running, since we will
+        // have no cache to clear at that point.
+        context.getApplicationContext().registerReceiver(this, filter);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        final String action = intent.getAction();
+        final String packageName = intent.getData().getSchemeSpecificPart();
+
+        final boolean matchingPackage = mKnownPackages.contains(packageName);
+        final boolean validAction = Intent.ACTION_PACKAGE_REMOVED.equals(action)
+                || Intent.ACTION_PACKAGE_ADDED.equals(action)
+                || Intent.ACTION_PACKAGE_CHANGED.equals(action);
+
+        if (matchingPackage && validAction) {
+            for (ContactsSource source : mSources.values()) {
+                if (TextUtils.equals(packageName, source.resPackageName)) {
+                    // Invalidate any cache for the changed package
+                    source.invalidateCache();
+                }
+            }
         }
     }
 
@@ -87,17 +139,6 @@
     protected void loadAccounts() {
         mSources.clear();
 
-        {
-            // Create fallback contacts source for on-phone contacts
-            final ContactsSource source = new ContactsSource();
-            source.accountType = HardCodedSources.ACCOUNT_TYPE_FALLBACK;
-            source.resPackageName = mContext.getPackageName();
-            source.titleRes = R.string.account_phone;
-            source.iconRes = R.drawable.ic_launcher_contacts;
-
-            mSources.put(source.accountType, source);
-        }
-
         final AccountManager am = AccountManager.get(mContext);
         final IContentService cs = ContentResolver.getContentService();
 
@@ -116,14 +157,23 @@
                 final String accountType = sync.accountType;
                 final AuthenticatorDescription auth = findAuthenticator(auths, accountType);
 
-                final ContactsSource source = new ContactsSource();
+                ContactsSource source;
+                if (GoogleSource.ACCOUNT_TYPE.equals(accountType)) {
+                    source = new GoogleSource(auth.packageName);
+                } else if (ExchangeSource.ACCOUNT_TYPE.equals(accountType)) {
+                    source = new ExchangeSource(auth.packageName);
+                } else {
+                    // TODO: use syncadapter package instead, since it provides resources
+                    Log.d(TAG, "Creating external source for type=" + accountType
+                            + ", packageName=" + auth.packageName);
+                    source = new ExternalSource(auth.packageName);
+                }
+
                 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);
+                addSource(source);
             }
         } catch (RemoteException e) {
             Log.w(TAG, "Problem loading accounts: " + e.toString());
@@ -166,22 +216,45 @@
         return matching;
     }
 
-    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);
+    /**
+     * Find the best {@link DataKind} matching the requested
+     * {@link ContactsSource#accountType} and {@link DataKind#mimeType}. If no
+     * direct match found, we try searching {@link #mFallbackSource}.
+     */
+    public DataKind getKindOrFallback(String accountType, String mimeType, Context context,
+            int inflateLevel) {
+        DataKind kind = null;
+
+        // Try finding source and kind matching request
+        final ContactsSource source = mSources.get(accountType);
+        if (source != null) {
+            source.ensureInflated(context, inflateLevel);
+            kind = source.getKindForMimetype(mimeType);
         }
-        return source;
+
+        if (kind == null) {
+            // Nothing found, so try fallback as last resort
+            mFallbackSource.ensureInflated(context, inflateLevel);
+            kind = mFallbackSource.getKindForMimetype(mimeType);
+        }
+
+        if (kind == null) {
+            Log.w(TAG, "Unknown type=" + accountType + ", mime=" + mimeType);
+        }
+
+        return kind;
     }
 
     /**
      * Return {@link ContactsSource} for the given account type.
      */
     public ContactsSource getInflatedSource(String accountType, int inflateLevel) {
-        final ContactsSource source = getSourceForType(accountType);
-        if (source == null || source.isInflated(inflateLevel)) {
-            // Found inflated, so return directly
+        // Try finding specific source, otherwise use fallback
+        ContactsSource source = mSources.get(accountType);
+        if (source == null) source = mFallbackSource;
+
+        if (source.isInflated(inflateLevel)) {
+            // Already inflated, so return directly
             return source;
         } else {
             // Not inflated, but requested that we force-inflate
diff --git a/src/com/android/contacts/ui/EditContactActivity.java b/src/com/android/contacts/ui/EditContactActivity.java
index 6059e62..c02ee80 100644
--- a/src/com/android/contacts/ui/EditContactActivity.java
+++ b/src/com/android/contacts/ui/EditContactActivity.java
@@ -62,12 +62,12 @@
 import com.android.contacts.ContactsUtils;
 import com.android.contacts.R;
 import com.android.contacts.ScrollingTabWidget;
+import com.android.contacts.model.GoogleSource;
 import com.android.contacts.model.ContactsSource;
 import com.android.contacts.model.Editor;
 import com.android.contacts.model.EntityDelta;
 import com.android.contacts.model.EntityModifier;
 import com.android.contacts.model.EntitySet;
-import com.android.contacts.model.HardCodedSources;
 import com.android.contacts.model.Sources;
 import com.android.contacts.model.Editor.EditorListener;
 import com.android.contacts.model.EntityDelta.ValuesDelta;
@@ -843,8 +843,8 @@
 
             // Create "My Contacts" membership for Google contacts
             // TODO: move this off into "templates" for each given source
-            if (HardCodedSources.ACCOUNT_TYPE_GOOGLE.equals(source.accountType)) {
-                HardCodedSources.attemptMyContactsMembership(insert, target);
+            if (GoogleSource.ACCOUNT_TYPE.equals(source.accountType)) {
+                GoogleSource.attemptMyContactsMembership(insert, target);
             }
 
 	    // TODO: no synchronization here on target.mState.  This
diff --git a/src/com/android/contacts/ui/FastTrackActivity.java b/src/com/android/contacts/ui/FastTrackActivity.java
index 8fdc40d..6958298 100644
--- a/src/com/android/contacts/ui/FastTrackActivity.java
+++ b/src/com/android/contacts/ui/FastTrackActivity.java
@@ -35,6 +35,10 @@
     protected void onCreate(Bundle icicle) {
         super.onCreate(icicle);
 
+        if (FastTrackWindow.TRACE_LAUNCH) {
+            android.os.Debug.startMethodTracing(FastTrackWindow.TRACE_TAG);
+        }
+
         // Use our local window token for now
         final Intent intent = getIntent();
         final Uri lookupUri = intent.getData();
diff --git a/src/com/android/contacts/ui/FastTrackWindow.java b/src/com/android/contacts/ui/FastTrackWindow.java
index be0e185..bd68290 100644
--- a/src/com/android/contacts/ui/FastTrackWindow.java
+++ b/src/com/android/contacts/ui/FastTrackWindow.java
@@ -20,6 +20,7 @@
 import com.android.contacts.model.ContactsSource;
 import com.android.contacts.model.Sources;
 import com.android.contacts.model.ContactsSource.DataKind;
+import com.android.contacts.ui.widget.CheckableImageView;
 import com.android.contacts.util.Constants;
 import com.android.contacts.util.NotifyingAsyncQueryHandler;
 import com.android.internal.policy.PolicyManager;
@@ -39,14 +40,12 @@
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.FastTrack;
-import android.provider.ContactsContract.Intents;
 import android.provider.ContactsContract.Presence;
 import android.provider.ContactsContract.RawContacts;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.SocialContract.Activities;
 import android.text.TextUtils;
-import android.text.format.DateUtils;
 import android.util.Log;
 import android.view.ContextThemeWrapper;
 import android.view.Gravity;
@@ -133,7 +132,7 @@
     private View mFooter;
     private View mFooterDisambig;
     private ListView mResolveList;
-    private CheckableImageView mLastChiclet;
+    private CheckableImageView mLastAction;
 
     /**
      * Set of {@link Action} that are associated with the aggregate currently
@@ -159,6 +158,9 @@
     private static final int TOKEN_SOCIAL = 2;
     private static final int TOKEN_DATA = 3;
 
+    static final boolean TRACE_LAUNCH = false;
+    static final String TRACE_TAG = "fasttrack";
+
     /**
      * Prepare a fast-track window to show in the given {@link Context}.
      */
@@ -245,6 +247,10 @@
             return;
         }
 
+        if (TRACE_LAUNCH && !android.os.Debug.isMethodTracingActive()) {
+            android.os.Debug.startMethodTracing(TRACE_TAG);
+        }
+
         // Prepare header view for requested mode
         mMode = mode;
         mHeader = getHeaderView(mode);
@@ -330,6 +336,10 @@
         mQuerying = false;
 
         mTrack.startAnimation(mTrackAnim);
+
+        if (TRACE_LAUNCH) {
+            android.os.Debug.stopMethodTracing();
+        }
     }
 
     /**
@@ -350,7 +360,7 @@
         }
 
         // Release refrence to last chiclet.
-        mLastChiclet = null;
+        mLastAction = null;
 
         // Completely hide header from current mode
         mHeader.setVisibility(View.GONE);
@@ -367,7 +377,7 @@
         mTrackScroll.fullScroll(View.FOCUS_LEFT);
         mWasDownArrow = false;
 
-        setResolveVisible(false);
+        setResolveVisible(false, null);
 
         mQuerying = false;
         mHasSummary = false;
@@ -576,7 +586,6 @@
      */
     private static class DataAction implements Action {
         private final Context mContext;
-        private final ContactsSource mSource;
         private final DataKind mKind;
         private final String mMimeType;
 
@@ -589,10 +598,8 @@
         /**
          * Create an action from common {@link Data} elements.
          */
-        public DataAction(Context context, ContactsSource source, String mimeType, DataKind kind,
-                Cursor cursor) {
+        public DataAction(Context context, String mimeType, DataKind kind, Cursor cursor) {
             mContext = context;
-            mSource = source;
             mKind = kind;
             mMimeType = mimeType;
 
@@ -656,13 +663,14 @@
         /** {@inheritDoc} */
         public Drawable getFallbackIcon() {
             // Bail early if no valid resources
-            if (mSource.resPackageName == null) return null;
+            final String resPackageName = mKind.resPackageName;
+            if (resPackageName == null) return null;
 
             final PackageManager pm = mContext.getPackageManager();
-            if (mAlternate && mKind.iconAltRes > 0) {
-                return pm.getDrawable(mSource.resPackageName, mKind.iconAltRes, null);
-            } else if (mKind.iconRes > 0) {
-                return pm.getDrawable(mSource.resPackageName, mKind.iconRes, null);
+            if (mAlternate && mKind.iconAltRes != -1) {
+                return pm.getDrawable(resPackageName, mKind.iconAltRes, null);
+            } else if (mKind.iconRes != -1) {
+                return pm.getDrawable(resPackageName, mKind.iconRes, null);
             } else {
                 return null;
             }
@@ -880,22 +888,21 @@
             // TODO: find the ContactsSource for this, either from accountType,
             // or through lazy-loading when resPackage is set, or default.
 
-            final ContactsSource source = sources.getInflatedSource(accountType,
+            final DataKind kind = sources.getKindOrFallback(accountType, mimeType, mContext,
                     ContactsSource.LEVEL_MIMETYPES);
-            final DataKind kind = source.getKindForMimetype(mimeType);
 
             if (kind != null) {
                 // Build an action for this data entry, find a mapping to a UI
                 // element, build its summary from the cursor, and collect it
                 // along with all others of this MIME-type.
-                final Action action = new DataAction(mContext, source, mimeType, kind, cursor);
+                final Action action = new DataAction(mContext, mimeType, kind, cursor);
                 considerAdd(action, mimeType);
             }
 
             // If phone number, also insert as text message action
             if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) && kind != null) {
-                final Action action = new DataAction(mContext, source, Constants.MIME_SMS_ADDRESS,
-                        kind, cursor);
+                final Action action = new DataAction(mContext, Constants.MIME_SMS_ADDRESS, kind,
+                        cursor);
                 considerAdd(action, Constants.MIME_SMS_ADDRESS);
             }
         }
@@ -972,7 +979,7 @@
      * Helper for showing and hiding {@link #mFooterDisambig}, which will
      * correctly manage {@link #mArrowDown} as needed.
      */
-    private void setResolveVisible(boolean visible) {
+    private void setResolveVisible(boolean visible, CheckableImageView actionView) {
         // Show or hide the resolve list if needed
         boolean visibleNow = mFooterDisambig.getVisibility() == View.VISIBLE;
 
@@ -986,24 +993,25 @@
             // If showing list, then hide and save state of down arrow
             mWasDownArrow = mWasDownArrow || (mArrowDown.getVisibility() == View.VISIBLE);
             mArrowDown.setVisibility(View.INVISIBLE);
-            mLastChiclet.setChecked(true);
         } else {
             // If hiding list, restore any down arrow state
             mArrowDown.setVisibility(mWasDownArrow ? View.VISIBLE : View.INVISIBLE);
-            mLastChiclet.setChecked(false);
         }
+
+        if (mLastAction != null) mLastAction.setChecked(!visible);
+        if (actionView != null) actionView.setChecked(visible);
+        mLastAction = actionView;
     }
 
     /** {@inheritDoc} */
-    public void onClick(View v) {
-        if (v instanceof CheckableImageView) {
-            mLastChiclet = (CheckableImageView)v;
-        }
+    public void onClick(View view) {
+        final boolean isActionView = (view instanceof CheckableImageView);
+        final CheckableImageView actionView = isActionView ? (CheckableImageView)view : null;
 
-        final Object tag = v.getTag();
+        final Object tag = view.getTag();
         if (tag instanceof Intent) {
             // Hide the resolution list, if present
-            setResolveVisible(false);
+            setResolveVisible(false, actionView);
             this.dismiss();
 
             try {
@@ -1017,7 +1025,7 @@
             final ActionList children = (ActionList)tag;
 
             // Show resolution list and set adapter
-            setResolveVisible(true);
+            setResolveVisible(true, actionView);
 
             mResolveList.setOnItemClickListener(this);
             mResolveList.setAdapter(new BaseAdapter() {
@@ -1066,7 +1074,7 @@
             // Back key will first dismiss any expanded resolve list, otherwise
             // it will close the entire dialog.
             if (mFooterDisambig.getVisibility() == View.VISIBLE) {
-                setResolveVisible(false);
+                setResolveVisible(false, null);
             } else {
                 dismiss();
             }
@@ -1208,6 +1216,7 @@
                 Data.MIMETYPE,
                 Data.IS_PRIMARY,
                 Data.IS_SUPER_PRIMARY,
+                Data.RAW_CONTACT_ID,
                 Data.DATA1, Data.DATA2, Data.DATA3, Data.DATA4, Data.DATA5,
                 Data.DATA6, Data.DATA7, Data.DATA8, Data.DATA9, Data.DATA10, Data.DATA11,
                 Data.DATA12, Data.DATA13, Data.DATA14, Data.DATA15,
diff --git a/src/com/android/contacts/ui/CheckableImageView.java b/src/com/android/contacts/ui/widget/CheckableImageView.java
similarity index 97%
rename from src/com/android/contacts/ui/CheckableImageView.java
rename to src/com/android/contacts/ui/widget/CheckableImageView.java
index 257e407..ceddf57 100644
--- a/src/com/android/contacts/ui/CheckableImageView.java
+++ b/src/com/android/contacts/ui/widget/CheckableImageView.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.contacts.ui;
+package com.android.contacts.ui.widget;
 
 import android.content.Context;
 import android.util.AttributeSet;
@@ -49,7 +49,7 @@
     public void toggle() {
         setChecked(!mChecked);
     }
-    
+
     public boolean isChecked() {
         return mChecked;
     }
diff --git a/tests/src/com/android/contacts/EntityModifierTests.java b/tests/src/com/android/contacts/EntityModifierTests.java
index dd40634..6bc3005 100644
--- a/tests/src/com/android/contacts/EntityModifierTests.java
+++ b/tests/src/com/android/contacts/EntityModifierTests.java
@@ -33,6 +33,7 @@
 
 import android.content.ContentProviderOperation;
 import android.content.ContentValues;
+import android.content.Context;
 import android.content.Entity;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.RawContacts;
@@ -67,16 +68,12 @@
         mContext = getContext();
     }
 
-    /**
-     * Build a {@link ContactsSource} that has various odd constraints for
-     * testing purposes.
-     */
-    protected ContactsSource getSource() {
-        final ContactsSource list = new ContactsSource();
-        list.accountType = TEST_ACCOUNT_TYPE;
-        list.setInflatedLevel(ContactsSource.LEVEL_CONSTRAINTS);
+    public static class MockContactsSource extends ContactsSource {
+        @Override
+        protected void inflate(Context context, int inflateLevel) {
+            this.accountType = TEST_ACCOUNT_TYPE;
+            this.setInflatedLevel(ContactsSource.LEVEL_CONSTRAINTS);
 
-        {
             // Phone allows maximum 2 home, 1 work, and unlimited other, with
             // constraint of 5 numbers maximum.
             DataKind kind = new DataKind(Phone.CONTENT_ITEM_TYPE, -1, -1, 10, true);
@@ -93,10 +90,16 @@
             kind.fieldList.add(new EditField(Phone.NUMBER, -1, -1));
             kind.fieldList.add(new EditField(Phone.LABEL, -1, -1));
 
-            list.add(kind);
+            addKind(kind);
         }
+    }
 
-        return list;
+    /**
+     * Build a {@link ContactsSource} that has various odd constraints for
+     * testing purposes.
+     */
+    protected ContactsSource getSource() {
+        return new MockContactsSource();
     }
 
     /**
diff --git a/tests/src/com/android/contacts/StyleManagerTests.java b/tests/src/com/android/contacts/StyleManagerTests.java
deleted file mode 100644
index aaf54be..0000000
--- a/tests/src/com/android/contacts/StyleManagerTests.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * Copyright (C) 2009 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.contacts;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.LargeTest;
-
-import java.util.Arrays;
-
-import com.android.contacts.StyleManager;
-import com.android.contacts.tests.R;
-
-/**
- * Tests for the StyleManager class.
- */
-@LargeTest
-public class StyleManagerTests extends AndroidTestCase {
-
-    public static final String LOG_TAG = "StyleManagerTests";
-
-    private StyleManager mStyleManager;
-    private static final String PACKAGE_NAME = "com.android.contacts.tests";
-    private static final String PHONE_MIMETYPE = "vnd.android.cursor.item/phone";
-    private Context mContext;
-
-    public StyleManagerTests() {
-        super();
-    }
-
-    @Override
-    public void setUp() {
-        mContext = getContext();
-        mStyleManager = StyleManager.getInstance(mContext);
-    }
-
-    public void testGetMimetypeIcon() {
-        Bitmap phoneIconFromSm = mStyleManager.getMimetypeIcon(mContext, PACKAGE_NAME, PHONE_MIMETYPE);
-        int smHeight = phoneIconFromSm.getHeight();
-        int smWidth = phoneIconFromSm.getWidth();
-
-        Bitmap phoneIconFromRes = BitmapFactory.decodeResource(mContext.getResources(),
-                R.drawable.phone_icon, null);
-        int resHeight = phoneIconFromRes.getHeight();
-        int resWidth = phoneIconFromRes.getWidth();
-
-        int[] smPixels = new int[smWidth*smHeight];
-        phoneIconFromSm.getPixels(smPixels, 0, smWidth, 0, 0, smWidth, smHeight);
-
-        int[] resPixels = new int[resWidth*resHeight];
-        phoneIconFromRes.getPixels(resPixels, 0, resWidth, 0, 0, resWidth, resHeight);
-
-        assertTrue(Arrays.equals(smPixels, resPixels));
-    }
-
-    public void testGetMissingMimetypeIcon() {
-        Bitmap postalIconFromSm = mStyleManager.getMimetypeIcon(mContext, PACKAGE_NAME,
-                "vnd.android.cursor.item/postal-address");
-
-        assertNull(postalIconFromSm);
-    }
-
-    public void testGetDefaultIcon() {
-        Bitmap defaultIconFromSm = mStyleManager.getDefaultIcon(mContext, PACKAGE_NAME);
-
-        int smHeight = defaultIconFromSm.getHeight();
-        int smWidth = defaultIconFromSm.getWidth();
-
-        Bitmap defaultIconFromRes = BitmapFactory.decodeResource(mContext.getResources(),
-                R.drawable.default_icon, null);
-        int resHeight = defaultIconFromRes.getHeight();
-        int resWidth = defaultIconFromRes.getWidth();
-
-        int[] smPixels = new int[smWidth*smHeight];
-        defaultIconFromSm.getPixels(smPixels, 0, smWidth, 0, 0, smWidth, smHeight);
-
-        int[] resPixels = new int[resWidth*resHeight];
-        defaultIconFromRes.getPixels(resPixels, 0, resWidth, 0, 0, resWidth, resHeight);
-
-        assertTrue(Arrays.equals(smPixels, resPixels));
-    }
-
-    public void testCaching() {
-        // Clear cache
-        mStyleManager.onPackageChange(PACKAGE_NAME);
-        assertTrue(mStyleManager.getIconCacheSize() == 0);
-        assertTrue(mStyleManager.getStyleSetCacheSize() == 0);
-
-        // Getting the icon should add it to the cache.
-        mStyleManager.getDefaultIcon(mContext, PACKAGE_NAME);
-        assertTrue(mStyleManager.getIconCacheSize() == 1);
-        assertTrue(mStyleManager.getStyleSetCacheSize() == 1);
-        assertTrue(mStyleManager.isIconCacheHit(PACKAGE_NAME, StyleManager.DEFAULT_MIMETYPE));
-        assertFalse(mStyleManager.isIconCacheHit(PACKAGE_NAME, PHONE_MIMETYPE));
-        assertTrue(mStyleManager.isStyleSetCacheHit(PACKAGE_NAME));
-
-        mStyleManager.getMimetypeIcon(mContext, PACKAGE_NAME, PHONE_MIMETYPE);
-        assertTrue(mStyleManager.getIconCacheSize() == 2);
-        assertTrue(mStyleManager.getStyleSetCacheSize() == 1);
-        assertTrue(mStyleManager.isIconCacheHit(PACKAGE_NAME, StyleManager.DEFAULT_MIMETYPE));
-        assertTrue(mStyleManager.isIconCacheHit(PACKAGE_NAME, PHONE_MIMETYPE));
-        assertTrue(mStyleManager.isStyleSetCacheHit(PACKAGE_NAME));
-
-        // Clear cache
-        mStyleManager.onPackageChange(PACKAGE_NAME);
-        assertTrue(mStyleManager.getIconCacheSize() == 0);
-        assertTrue(mStyleManager.getStyleSetCacheSize() == 0);
-        assertFalse(mStyleManager.isIconCacheHit(PACKAGE_NAME, StyleManager.DEFAULT_MIMETYPE));
-        assertFalse(mStyleManager.isIconCacheHit(PACKAGE_NAME, PHONE_MIMETYPE));
-        assertFalse(mStyleManager.isStyleSetCacheHit(PACKAGE_NAME));
-    }
-
-}