Merge "EditSchema parser for ExternalAccountType"
diff --git a/src/com/android/contacts/model/AccountType.java b/src/com/android/contacts/model/AccountType.java
index 7871dbb..654adeb 100644
--- a/src/com/android/contacts/model/AccountType.java
+++ b/src/com/android/contacts/model/AccountType.java
@@ -32,6 +32,7 @@
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
 import android.view.inputmethod.EditorInfo;
 import android.widget.EditText;
 
@@ -82,6 +83,14 @@
      */
     private HashMap<String, DataKind> mMimeKinds = Maps.newHashMap();
 
+    /**
+     * Whether this account type was able to be fully initialized.  This may be false if
+     * (for example) the package name associated with the account type could not be found.
+     */
+    public boolean isInitialized() {
+        return true;
+    }
+
     public boolean isExtension() {
         return false;
     }
@@ -234,10 +243,6 @@
      */
     abstract public boolean isGroupMembershipEditable();
 
-    abstract public int getHeaderColor(Context context);
-
-    abstract public int getSideBarColor(Context context);
-
     /**
      * {@link Comparator} to sort by {@link DataKind#weight}.
      */
@@ -270,6 +275,11 @@
      * Add given {@link DataKind} to list of those provided by this source.
      */
     public DataKind addKind(DataKind kind) {
+        if (mMimeKinds.get(kind.mimeType) != null) {
+            // TODO Make it exception.
+            Log.w(TAG, "mime type '" + kind.mimeType + "' is already registered");
+        }
+
         kind.resPackageName = this.resPackageName;
         this.mKinds.add(kind);
         this.mMimeKinds.put(kind.mimeType, kind);
@@ -351,7 +361,7 @@
      * {@link Phone#NUMBER}. Includes flags to apply to an {@link EditText}, and
      * the column where this field is stored.
      */
-    public static class EditField {
+    public static final class EditField {
         public String column;
         public int titleRes;
         public int inputType;
diff --git a/src/com/android/contacts/model/AccountTypeManager.java b/src/com/android/contacts/model/AccountTypeManager.java
index cb4e9f5..92323fa 100644
--- a/src/com/android/contacts/model/AccountTypeManager.java
+++ b/src/com/android/contacts/model/AccountTypeManager.java
@@ -411,7 +411,7 @@
                     Log.d(TAG, "Registering external account type=" + type
                             + ", packageName=" + auth.packageName);
                     accountType = new ExternalAccountType(mContext, auth.packageName, false);
-                    if (!((ExternalAccountType) accountType).isInitialized()) {
+                    if (!accountType.isInitialized()) {
                         // Skip external account types that couldn't be initialized.
                         continue;
                     }
diff --git a/src/com/android/contacts/model/BaseAccountType.java b/src/com/android/contacts/model/BaseAccountType.java
index a685dee..34beca2 100644
--- a/src/com/android/contacts/model/BaseAccountType.java
+++ b/src/com/android/contacts/model/BaseAccountType.java
@@ -17,7 +17,12 @@
 package com.android.contacts.model;
 
 import com.android.contacts.R;
+import com.android.contacts.util.DateUtils;
 import com.google.android.collect.Lists;
+import com.google.android.collect.Maps;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
 
 import android.content.ContentValues;
 import android.content.Context;
@@ -38,9 +43,18 @@
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.util.AttributeSet;
+import android.util.Log;
 import android.view.inputmethod.EditorInfo;
 
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
 public abstract class BaseAccountType extends AccountType {
+    private static final String TAG = "BaseAccountType";
+
     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;
@@ -61,6 +75,47 @@
     protected static final int FLAGS_SIP_ADDRESS = EditorInfo.TYPE_CLASS_TEXT
             | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;  // since SIP addresses have the same
                                                              // basic format as email addresses
+    protected static final int FLAGS_RELATION = EditorInfo.TYPE_CLASS_TEXT
+            | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS | EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME;
+
+    private interface Tag {
+        static final String DATA_KIND = "DataKind";
+        static final String TYPE = "Type";
+    }
+
+    private interface Attr {
+        static final String MAX_OCCURRENCE = "maxOccurs";
+        static final String DATE_WITH_TIME = "dateWithTime";
+        static final String YEAR_OPTIONAL = "yearOptional";
+        static final String KIND = "kind";
+        static final String TYPE = "type";
+    }
+
+    private interface Weight {
+        static final int NONE = -1;
+        static final int ORGANIZATION = 5;
+        static final int PHONE = 10;
+        static final int EMAIL = 15;
+        static final int IM = 20;
+        static final int STRUCTURED_POSTAL = 25;
+        static final int NOTE = 110;
+        static final int NICKNAME = 115;
+        static final int WEBSITE = 120;
+        static final int SIP_ADDRESS = 130;
+        static final int EVENT = 150;
+        static final int RELATIONSHIP = 160;
+        static final int GROUP_MEMBERSHIP = 999;
+    }
+
+    protected static class DefinitionException extends Exception {
+        public DefinitionException(String message) {
+            super(message);
+        }
+
+        public DefinitionException(String message, Exception inner) {
+            super(message, inner);
+        }
+    }
 
     public BaseAccountType() {
         this.accountType = null;
@@ -69,27 +124,27 @@
         this.iconRes = R.mipmap.ic_launcher_contacts;
     }
 
-    protected EditType buildPhoneType(int type) {
+    protected static EditType buildPhoneType(int type) {
         return new EditType(type, Phone.getTypeLabelResource(type));
     }
 
-    protected EditType buildEmailType(int type) {
+    protected static EditType buildEmailType(int type) {
         return new EditType(type, Email.getTypeLabelResource(type));
     }
 
-    protected EditType buildPostalType(int type) {
+    protected static EditType buildPostalType(int type) {
         return new EditType(type, StructuredPostal.getTypeLabelResource(type));
     }
 
-    protected EditType buildImType(int type) {
+    protected static EditType buildImType(int type) {
         return new EditType(type, Im.getProtocolLabelResource(type));
     }
 
-    protected EditType buildEventType(int type, boolean yearOptional) {
+    protected static EditType buildEventType(int type, boolean yearOptional) {
         return new EventEditType(type, Event.getTypeResource(type)).setYearOptional(yearOptional);
     }
 
-    protected EditType buildRelationType(int type) {
+    protected static EditType buildRelationType(int type) {
         return new EditType(type, Relation.getTypeLabelResource(type));
     }
 
@@ -360,14 +415,6 @@
     }
 
     protected DataKind addDataKindSipAddress(Context context) {
-        // The icon specified here is the one that gets displayed for
-        // "Internet call" items, in the "view contact" UI within the
-        // Contacts app.
-        //
-        // This is independent of the "SIP call" icon that gets
-        // displayed in the Quick Contacts widget, which comes from
-        // the android:icon attribute of the SIP-related
-        // intent-filters in the Phone app's manifest.
         DataKind kind = addKind(new DataKind(SipAddress.CONTENT_ITEM_TYPE,
                     R.string.label_sip_address, 130, true, R.layout.text_fields_editor_view));
 
@@ -382,8 +429,7 @@
     }
 
     protected DataKind addDataKindGroupMembership(Context context) {
-        DataKind kind = getKindForMimetype(GroupMembership.CONTENT_ITEM_TYPE);
-        kind = addKind(new DataKind(GroupMembership.CONTENT_ITEM_TYPE,
+        DataKind kind = addKind(new DataKind(GroupMembership.CONTENT_ITEM_TYPE,
                 R.string.groupsLabel, 999, true, -1));
 
         kind.typeOverallMax = 1;
@@ -632,17 +678,857 @@
     }
 
     @Override
-    public int getHeaderColor(Context context) {
-        return 0xff7f93bc;
-    }
-
-    @Override
-    public int getSideBarColor(Context context) {
-        return 0xffbdc7b8;
-    }
-
-    @Override
     public boolean isGroupMembershipEditable() {
         return false;
     }
+
+    /**
+     * Parses the content of the EditSchema tag in contacts.xml.
+     */
+    protected final void parseEditSchema(Context context, XmlPullParser parser, AttributeSet attrs)
+            throws XmlPullParserException, IOException, DefinitionException {
+
+        final int outerDepth = parser.getDepth();
+        int type;
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+            final int depth = parser.getDepth();
+            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT
+                    || depth != outerDepth + 1) {
+                continue; // Not direct child tag
+            }
+
+            final String tag = parser.getName();
+
+            if (Tag.DATA_KIND.equals(tag)) {
+                for (DataKind kind : KindParser.INSTANCE.parseDataKindTag(context, parser, attrs)) {
+                    addKind(kind);
+                }
+            } else {
+                Log.w(TAG, "Skipping unknown tag " + tag);
+            }
+        }
+    }
+
+    // Utility methods to keep code shorter.
+    private static boolean getAttr(AttributeSet attrs, String attribute, boolean defaultValue) {
+        return attrs.getAttributeBooleanValue(null, attribute, defaultValue);
+    }
+
+    private static int getAttr(AttributeSet attrs, String attribute, int defaultValue) {
+        return attrs.getAttributeIntValue(null, attribute, defaultValue);
+    }
+
+    private static String getAttr(AttributeSet attrs, String attribute) {
+        return attrs.getAttributeValue(null, attribute);
+    }
+
+    // TODO Extract it to its own class, and move all KindBuilders to it as well.
+    private static class KindParser {
+        public static final KindParser INSTANCE = new KindParser();
+
+        private final Map<String, KindBuilder> mBuilders = Maps.newHashMap();
+
+        private KindParser() {
+            addBuilder(new NameKindBuilder());
+            addBuilder(new NicknameKindBuilder());
+            addBuilder(new PhoneKindBuilder());
+            addBuilder(new EmailKindBuilder());
+            addBuilder(new StructuredPostalKindBuilder());
+            addBuilder(new ImKindBuilder());
+            addBuilder(new OrganizationKindBuilder());
+            addBuilder(new PhotoKindBuilder());
+            addBuilder(new NoteKindBuilder());
+            addBuilder(new WebsiteKindBuilder());
+            addBuilder(new SipAddressKindBuilder());
+            addBuilder(new GroupMembershipKindBuilder());
+            addBuilder(new EventKindBuilder());
+            addBuilder(new RelationshipKindBuilder());
+        }
+
+        private void addBuilder(KindBuilder builder) {
+            mBuilders.put(builder.getTagName(), builder);
+        }
+
+        /**
+         * Takes a {@link XmlPullParser} at the start of a DataKind tag, parses it and returns
+         * {@link DataKind}s.  (Usually just one, but there are three for the "name" kind.)
+         *
+         * This method returns a list, because we need to add 3 kinds for the name data kind.
+         * (structured, display and phonetic)
+         */
+        public List<DataKind> parseDataKindTag(Context context, XmlPullParser parser,
+                AttributeSet attrs)
+                throws DefinitionException, XmlPullParserException, IOException {
+            final String kind = getAttr(attrs, Attr.KIND);
+            final KindBuilder builder = mBuilders.get(kind);
+            if (builder != null) {
+                return builder.parseDataKind(context, parser, attrs);
+            } else {
+                throw new DefinitionException("Undefined data kind '" + kind + "'");
+            }
+        }
+    }
+
+    private static abstract class KindBuilder {
+
+        public abstract String getTagName();
+
+        /**
+         * DataKind tag parser specific to each kind.  Subclasses must implement it.
+         */
+        public abstract List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException, IOException;
+
+        /**
+         * Creates a new {@link DataKind}, and also parses the child Type tags in the DataKind
+         * tag.
+         */
+        protected final DataKind newDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs, boolean isPseudo, String mimeType, String typeColumn,
+                int titleRes, int weight, int editorLayoutResourceId,
+                StringInflater actionHeader, StringInflater actionBody)
+                throws DefinitionException, XmlPullParserException, IOException {
+
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Adding DataKind: " + mimeType);
+            }
+
+            final DataKind kind = new DataKind(mimeType, titleRes, weight, true,
+                    editorLayoutResourceId);
+            kind.typeColumn = typeColumn;
+            kind.actionHeader = actionHeader;
+            kind.actionBody = actionBody;
+            kind.fieldList = Lists.newArrayList();
+
+            kind.typeOverallMax = getAttr(attrs, Attr.MAX_OCCURRENCE, -1);
+
+            // Handle "types".
+            // If a kind has the type column, contacts.xml must have at least one type definition.
+            // Otherwise, it mustn't have a type definition.
+            //
+            // If it's a pseudo data kind (== data kind that doesn't have the corresponding
+            // DataKind tag in the XML), we just skip this process.
+            if (!isPseudo) {
+                if (kind.typeColumn != null) {
+                    // Parse and add types.
+                    kind.typeList = Lists.newArrayList();
+                    parseTypes(context, parser, attrs, kind, true);
+                    if (kind.typeList.size() == 0) {
+                        throw new DefinitionException(
+                                "Kind " + kind.mimeType + " must have at least one type");
+                    }
+                } else {
+                    // Make sure it has no types.
+                    parseTypes(context, parser, attrs, kind, false /* can't have types */);
+                }
+            }
+
+            return kind;
+        }
+
+        /**
+         * Parses Type elements in a DataKind element, and if {@code canHaveTypes} is true adds
+         * them to the given {@link DataKind}. Otherwise the {@link DataKind} can't have a type,
+         * so throws {@link DefinitionException}.
+         */
+        private void parseTypes(Context context, XmlPullParser parser, AttributeSet attrs,
+                DataKind kind, boolean canHaveTypes)
+                throws DefinitionException, XmlPullParserException, IOException {
+            final int outerDepth = parser.getDepth();
+            int type;
+            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                    && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+                final int depth = parser.getDepth();
+                if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT
+                        || depth != outerDepth + 1) {
+                    continue; // Not direct child tag
+                }
+
+                final String tag = parser.getName();
+                if (Tag.TYPE.equals(tag)) {
+                    if (canHaveTypes) {
+                        kind.typeList.add(parseTypeTag(parser, attrs, kind));
+                    } else {
+                        throw new DefinitionException(
+                                "Kind " + kind.mimeType + " can't have types");
+                    }
+                } else {
+                    throw new DefinitionException("Unknown tag: " + tag);
+                }
+            }
+        }
+
+        /**
+         * Parses a single Type element and returns an {@link EditType} built from it.  Uses
+         * {@link #buildEditTypeForTypeTag} defined in subclasses to actually build an
+         * {@link EditType}.
+         */
+        private EditType parseTypeTag(XmlPullParser parser, AttributeSet attrs, DataKind kind)
+                throws DefinitionException {
+
+            final String typeName = getAttr(attrs, Attr.TYPE);
+
+            final EditType et = buildEditTypeForTypeTag(attrs, typeName);
+            if (et == null) {
+                throw new DefinitionException(
+                        "Undefined type '" + typeName + "' for data kind '" + kind.mimeType + "'");
+            }
+            et.specificMax = getAttr(attrs, Attr.MAX_OCCURRENCE, -1);
+
+            return et;
+        }
+
+        /**
+         * Returns an {@link EditType} for the given "type".  Subclasses may optionally use
+         * the attributes in the tag to set optional values.
+         * (e.g. "yearOptional" for the event kind)
+         */
+        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+            return null;
+        }
+
+        protected final void throwIfList(DataKind kind) throws DefinitionException {
+            if (kind.typeOverallMax != 1) {
+                throw new DefinitionException(
+                        "Kind " + kind.mimeType + " must have 'overallMax=\"1\"'");
+            }
+        }
+    }
+
+    /**
+     * DataKind parser for Name. (structured, display, phonetic)
+     */
+    private static class NameKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "name";
+        }
+
+        private static void checkAttributeTrue(boolean value, String attrName)
+                throws DefinitionException {
+            if (!value) {
+                throw new DefinitionException(attrName + "must be true");
+            }
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+
+            // Build 3 data kinds:
+            // - StructuredName.CONTENT_ITEM_TYPE
+            // - DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME
+            // - DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME
+
+            final boolean displayOrderPrimary =
+                    context.getResources().getBoolean(R.bool.config_editor_field_order_primary);
+
+            final boolean supportsDisplayName = getAttr(attrs, "supportsDisplayName", true);
+            final boolean supportsPrefix = getAttr(attrs, "supportsPrefix", true);
+            final boolean supportsMiddleName = getAttr(attrs, "supportsMiddleName", true);
+            final boolean supportsSuffix = getAttr(attrs, "supportsSuffix", true);
+            final boolean supportsPhoneticFamilyName =
+                    getAttr(attrs, "supportsPhoneticFamilyName", true);
+            final boolean supportsPhoneticMiddleName =
+                    getAttr(attrs, "supportsPhoneticMiddleName", true);
+            final boolean supportsPhoneticGivenName =
+                    getAttr(attrs, "supportsPhoneticGivenName", true);
+
+            // For now, every things must be supported.
+            checkAttributeTrue(supportsDisplayName, "supportsDisplayName");
+            checkAttributeTrue(supportsPrefix, "supportsPrefix");
+            checkAttributeTrue(supportsMiddleName, "supportsMiddleName");
+            checkAttributeTrue(supportsSuffix, "supportsSuffix");
+            checkAttributeTrue(supportsPhoneticFamilyName, "supportsPhoneticFamilyName");
+            checkAttributeTrue(supportsPhoneticMiddleName, "supportsPhoneticMiddleName");
+            checkAttributeTrue(supportsPhoneticGivenName, "supportsPhoneticGivenName");
+
+            final List<DataKind> kinds = Lists.newArrayList();
+
+            // Structured name
+            final DataKind ks = newDataKind(context, parser, attrs, false,
+                    StructuredName.CONTENT_ITEM_TYPE, null, R.string.nameLabelsGroup, Weight.NONE,
+                    R.layout.structured_name_editor_view,
+                    new SimpleInflater(R.string.nameLabelsGroup),
+                    new SimpleInflater(Nickname.NAME));
+
+            throwIfList(ks);
+            kinds.add(ks);
+
+
+            // Note about setLongForm/setShortForm below.
+            // We need to set this only when the type supports display name. (=supportsDisplayName)
+            // Otherwise (i.e. Exchange) we don't set these flags, but instead make some fields
+            // "optional".
+
+            ks.fieldList.add(new EditField(StructuredName.DISPLAY_NAME, R.string.full_name,
+                    FLAGS_PERSON_NAME));
+            ks.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
+                    FLAGS_PERSON_NAME).setLongForm(true));
+            ks.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
+                    FLAGS_PERSON_NAME).setLongForm(true));
+            ks.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
+                    FLAGS_PERSON_NAME).setLongForm(true));
+            ks.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
+                    FLAGS_PERSON_NAME).setLongForm(true));
+            ks.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
+                    FLAGS_PERSON_NAME).setLongForm(true));
+            ks.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME,
+                    R.string.name_phonetic_family, FLAGS_PHONETIC));
+            ks.fieldList.add(new EditField(StructuredName.PHONETIC_MIDDLE_NAME,
+                    R.string.name_phonetic_middle, FLAGS_PHONETIC));
+            ks.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME,
+                    R.string.name_phonetic_given, FLAGS_PHONETIC));
+
+            // Display name
+            final DataKind kd = newDataKind(context, parser, attrs, true,
+                    DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME, null,
+                    R.string.nameLabelsGroup, Weight.NONE, R.layout.text_fields_editor_view,
+                    new SimpleInflater(R.string.nameLabelsGroup),
+                    new SimpleInflater(Nickname.NAME));
+            kinds.add(kd);
+
+            kd.fieldList.add(new EditField(StructuredName.DISPLAY_NAME,
+                    R.string.full_name, FLAGS_PERSON_NAME).setShortForm(true));
+
+            if (!displayOrderPrimary) {
+                kd.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
+                        FLAGS_PERSON_NAME).setLongForm(true));
+                kd.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
+                        FLAGS_PERSON_NAME).setLongForm(true));
+                kd.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
+                        FLAGS_PERSON_NAME).setLongForm(true));
+                kd.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
+                        FLAGS_PERSON_NAME).setLongForm(true));
+                kd.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
+                        FLAGS_PERSON_NAME).setLongForm(true));
+            } else {
+                kd.fieldList.add(new EditField(StructuredName.PREFIX, R.string.name_prefix,
+                        FLAGS_PERSON_NAME).setLongForm(true));
+                kd.fieldList.add(new EditField(StructuredName.GIVEN_NAME, R.string.name_given,
+                        FLAGS_PERSON_NAME).setLongForm(true));
+                kd.fieldList.add(new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle,
+                        FLAGS_PERSON_NAME).setLongForm(true));
+                kd.fieldList.add(new EditField(StructuredName.FAMILY_NAME, R.string.name_family,
+                        FLAGS_PERSON_NAME).setLongForm(true));
+                kd.fieldList.add(new EditField(StructuredName.SUFFIX, R.string.name_suffix,
+                        FLAGS_PERSON_NAME).setLongForm(true));
+            }
+
+            // Phonetic name
+            final DataKind kp = newDataKind(context, parser, attrs, true,
+                    DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME, null,
+                    R.string.name_phonetic, Weight.NONE, R.layout.phonetic_name_editor_view,
+                    new SimpleInflater(R.string.nameLabelsGroup),
+                    new SimpleInflater(Nickname.NAME));
+            kinds.add(kp);
+
+            kp.fieldList.add(new EditField(DataKind.PSEUDO_COLUMN_PHONETIC_NAME,
+                    R.string.name_phonetic, FLAGS_PHONETIC).setShortForm(true));
+            if (!displayOrderPrimary) {
+                kp.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME,
+                        R.string.name_phonetic_family, FLAGS_PHONETIC).setLongForm(true));
+                kp.fieldList.add(new EditField(StructuredName.PHONETIC_MIDDLE_NAME,
+                        R.string.name_phonetic_middle, FLAGS_PHONETIC).setLongForm(true));
+                kp.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME,
+                        R.string.name_phonetic_given, FLAGS_PHONETIC).setLongForm(true));
+            } else {
+                kp.fieldList.add(new EditField(StructuredName.PHONETIC_GIVEN_NAME,
+                        R.string.name_phonetic_given, FLAGS_PHONETIC).setLongForm(true));
+                kp.fieldList.add(new EditField(StructuredName.PHONETIC_MIDDLE_NAME,
+                        R.string.name_phonetic_middle, FLAGS_PHONETIC).setLongForm(true));
+                kp.fieldList.add(new EditField(StructuredName.PHONETIC_FAMILY_NAME,
+                        R.string.name_phonetic_family, FLAGS_PHONETIC).setLongForm(true));
+            }
+            return kinds;
+        }
+    }
+
+    private static class NicknameKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "nickname";
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+            final DataKind kind = newDataKind(context, parser, attrs, false,
+                    Nickname.CONTENT_ITEM_TYPE, null, R.string.nicknameLabelsGroup, Weight.NICKNAME,
+                    R.layout.text_fields_editor_view,
+                    new SimpleInflater(R.string.nicknameLabelsGroup),
+                    new SimpleInflater(Nickname.NAME));
+
+            kind.fieldList.add(new EditField(Nickname.NAME, R.string.nicknameLabelsGroup,
+                    FLAGS_PERSON_NAME));
+
+            kind.defaultValues = new ContentValues();
+            kind.defaultValues.put(Nickname.TYPE, Nickname.TYPE_DEFAULT);
+
+            throwIfList(kind);
+            return Lists.newArrayList(kind);
+        }
+    }
+
+    private static class PhoneKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "phone";
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+            final DataKind kind = newDataKind(context, parser, attrs, false,
+                    Phone.CONTENT_ITEM_TYPE, Phone.TYPE, R.string.phoneLabelsGroup, Weight.PHONE,
+                    R.layout.text_fields_editor_view,
+                    new PhoneActionInflater(), new SimpleInflater(Phone.NUMBER));
+
+            kind.iconAltRes = R.drawable.ic_text_holo_light;
+            kind.iconAltDescriptionRes = R.string.sms;
+            kind.actionAltHeader = new PhoneActionAltInflater();
+
+            kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+
+            return Lists.newArrayList(kind);
+        }
+
+        /** Just to avoid line-wrapping... */
+        protected static EditType build(int type, boolean secondary) {
+            return new EditType(type, Phone.getTypeLabelResource(type)).setSecondary(secondary);
+        }
+
+        @Override
+        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+            if ("home".equals(type)) return build(Phone.TYPE_HOME, false);
+            if ("mobile".equals(type)) return build(Phone.TYPE_MOBILE, false);
+            if ("work".equals(type)) return build(Phone.TYPE_WORK, false);
+            if ("fax_work".equals(type)) return build(Phone.TYPE_FAX_WORK, true);
+            if ("fax_home".equals(type)) return build(Phone.TYPE_FAX_HOME, true);
+            if ("pager".equals(type)) return build(Phone.TYPE_PAGER, true);
+            if ("other".equals(type)) return build(Phone.TYPE_OTHER, false);
+            if ("callback".equals(type)) return build(Phone.TYPE_CALLBACK, true);
+            if ("car".equals(type)) return build(Phone.TYPE_CAR, true);
+            if ("company_main".equals(type)) return build(Phone.TYPE_COMPANY_MAIN, true);
+            if ("isdn".equals(type)) return build(Phone.TYPE_ISDN, true);
+            if ("main".equals(type)) return build(Phone.TYPE_MAIN, true);
+            if ("other_fax".equals(type)) return build(Phone.TYPE_OTHER_FAX, true);
+            if ("radio".equals(type)) return build(Phone.TYPE_RADIO, true);
+            if ("telex".equals(type)) return build(Phone.TYPE_TELEX, true);
+            if ("tty_tdd".equals(type)) return build(Phone.TYPE_TTY_TDD, true);
+            if ("work_mobile".equals(type)) return build(Phone.TYPE_WORK_MOBILE, true);
+            if ("work_pager".equals(type)) return build(Phone.TYPE_WORK_PAGER, true);
+
+            // Note "assistant" used to be a custom column for the fallback type, but not anymore.
+            if ("assistant".equals(type)) return build(Phone.TYPE_ASSISTANT, true);
+            if ("mms".equals(type)) return build(Phone.TYPE_MMS, true);
+            if ("custom".equals(type)) {
+                return build(Phone.TYPE_CUSTOM, true).setCustomColumn(Phone.LABEL);
+            }
+            return null;
+        }
+    }
+
+    private static class EmailKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "email";
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+            final DataKind kind = newDataKind(context, parser, attrs, false,
+                    Email.CONTENT_ITEM_TYPE, Email.TYPE, R.string.emailLabelsGroup, Weight.EMAIL,
+                    R.layout.text_fields_editor_view,
+                    new EmailActionInflater(), new SimpleInflater(Email.DATA));
+            kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+
+            return Lists.newArrayList(kind);
+        }
+
+        @Override
+        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+            // EditType is mutable, so we need to create a new instance every time.
+            if ("home".equals(type)) return buildEmailType(Email.TYPE_HOME);
+            if ("work".equals(type)) return buildEmailType(Email.TYPE_WORK);
+            if ("other".equals(type)) return buildEmailType(Email.TYPE_OTHER);
+            if ("mobile".equals(type)) return buildEmailType(Email.TYPE_MOBILE);
+            if ("custom".equals(type)) {
+                return buildEmailType(Email.TYPE_CUSTOM)
+                        .setSecondary(true).setCustomColumn(Email.LABEL);
+            }
+            return null;
+        }
+    }
+
+    private static class StructuredPostalKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "postal";
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+            final DataKind kind = newDataKind(context, parser, attrs, false,
+                    StructuredPostal.CONTENT_ITEM_TYPE, StructuredPostal.TYPE,
+                    R.string.postalLabelsGroup, Weight.STRUCTURED_POSTAL,
+                    R.layout.text_fields_editor_view, new PostalActionInflater(),
+                    new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS));
+
+            if (getAttr(attrs, "needsStructured", false)) {
+                if (Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage())) {
+                    // Japanese order
+                    kind.fieldList.add(new EditField(StructuredPostal.COUNTRY,
+                            R.string.postal_country, FLAGS_POSTAL).setOptional(true));
+                    kind.fieldList.add(new EditField(StructuredPostal.POSTCODE,
+                            R.string.postal_postcode, FLAGS_POSTAL));
+                    kind.fieldList.add(new EditField(StructuredPostal.REGION,
+                            R.string.postal_region, FLAGS_POSTAL));
+                    kind.fieldList.add(new EditField(StructuredPostal.CITY,
+                            R.string.postal_city,FLAGS_POSTAL));
+                    kind.fieldList.add(new EditField(StructuredPostal.STREET,
+                            R.string.postal_street, FLAGS_POSTAL));
+                } else {
+                    // Generic order
+                    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));
+                }
+            } else {
+                kind.fieldList.add(
+                        new EditField(StructuredPostal.FORMATTED_ADDRESS, R.string.postal_address,
+                                FLAGS_POSTAL));
+            }
+
+            return Lists.newArrayList(kind);
+        }
+
+        @Override
+        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+            // EditType is mutable, so we need to create a new instance every time.
+            if ("home".equals(type)) return buildPostalType(StructuredPostal.TYPE_HOME);
+            if ("work".equals(type)) return buildPostalType(StructuredPostal.TYPE_WORK);
+            if ("other".equals(type)) return buildPostalType(StructuredPostal.TYPE_OTHER);
+            if ("custom".equals(type)) {
+                return buildPostalType(StructuredPostal.TYPE_CUSTOM)
+                        .setSecondary(true).setCustomColumn(Email.LABEL);
+            }
+            return null;
+        }
+    }
+
+    private static class ImKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "im";
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+
+            // IM is special:
+            // - It uses "protocol" as the custom label field
+            // - Its TYPE is fixed to TYPE_OTHER
+
+            final DataKind kind = newDataKind(context, parser, attrs, false,
+                    Im.CONTENT_ITEM_TYPE, Im.PROTOCOL, R.string.imLabelsGroup, Weight.IM,
+                    R.layout.text_fields_editor_view,
+                    new ImActionInflater(), new SimpleInflater(Im.DATA) // header / action
+                    );
+            kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
+
+            kind.defaultValues = new ContentValues();
+            kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER);
+
+            return Lists.newArrayList(kind);
+        }
+
+        @Override
+        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+            if ("aim".equals(type)) return buildImType(Im.PROTOCOL_AIM);
+            if ("msn".equals(type)) return buildImType(Im.PROTOCOL_MSN);
+            if ("yahoo".equals(type)) return buildImType(Im.PROTOCOL_YAHOO);
+            if ("skype".equals(type)) return buildImType(Im.PROTOCOL_SKYPE);
+            if ("qq".equals(type)) return buildImType(Im.PROTOCOL_QQ);
+            if ("google_talk".equals(type)) return buildImType(Im.PROTOCOL_GOOGLE_TALK);
+            if ("icq".equals(type)) return buildImType(Im.PROTOCOL_ICQ);
+            if ("jabber".equals(type)) return buildImType(Im.PROTOCOL_JABBER);
+            if ("custom".equals(type)) {
+                return buildImType(Im.PROTOCOL_CUSTOM).setSecondary(true)
+                        .setCustomColumn(Im.CUSTOM_PROTOCOL);
+            }
+            return null;
+        }
+    }
+
+    private static class OrganizationKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "organization";
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+            final DataKind kind = newDataKind(context, parser, attrs, false,
+                    Organization.CONTENT_ITEM_TYPE, null, R.string.organizationLabelsGroup,
+                    Weight.ORGANIZATION, R.layout.text_fields_editor_view ,
+                    new SimpleInflater(Organization.COMPANY),
+                    new SimpleInflater(Organization.TITLE));
+
+            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));
+
+            throwIfList(kind);
+
+            return Lists.newArrayList(kind);
+        }
+    }
+
+    private static class PhotoKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "photo";
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+            final DataKind kind = newDataKind(context, parser, attrs, false,
+                    Photo.CONTENT_ITEM_TYPE, null /* no type */, -1, Weight.NONE, -1,
+                    null, null // no header, no body
+                    );
+
+            kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1));
+
+            throwIfList(kind);
+
+            return Lists.newArrayList(kind);
+        }
+    }
+
+    private static class NoteKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "note";
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+            final DataKind kind = newDataKind(context, parser, attrs, false,
+                    Note.CONTENT_ITEM_TYPE, null, R.string.label_notes, Weight.NOTE,
+                    R.layout.text_fields_editor_view,
+                    new SimpleInflater(R.string.label_notes), new SimpleInflater(Note.NOTE));
+
+            kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE));
+
+            throwIfList(kind);
+
+            return Lists.newArrayList(kind);
+        }
+    }
+
+    private static class WebsiteKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "website";
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+            final DataKind kind = newDataKind(context, parser, attrs, false,
+                    Website.CONTENT_ITEM_TYPE, null, R.string.websiteLabelsGroup, Weight.WEBSITE,
+                    R.layout.text_fields_editor_view,
+                    new SimpleInflater(R.string.websiteLabelsGroup),
+                    new SimpleInflater(Website.URL));
+
+            kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup,
+                    FLAGS_WEBSITE));
+
+            kind.defaultValues = new ContentValues();
+            kind.defaultValues.put(Website.TYPE, Website.TYPE_OTHER);
+
+            return Lists.newArrayList(kind);
+        }
+    }
+
+    private static class SipAddressKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "sip_address";
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+            final DataKind kind = newDataKind(context, parser, attrs, false,
+                    SipAddress.CONTENT_ITEM_TYPE, null, R.string.label_sip_address,
+                    Weight.SIP_ADDRESS, R.layout.text_fields_editor_view,
+                    new SimpleInflater(R.string.label_sip_address),
+                    new SimpleInflater(SipAddress.SIP_ADDRESS));
+
+            kind.fieldList.add(new EditField(SipAddress.SIP_ADDRESS,
+                    R.string.label_sip_address, FLAGS_SIP_ADDRESS));
+
+            throwIfList(kind);
+
+            return Lists.newArrayList(kind);
+        }
+    }
+
+    private static class GroupMembershipKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "group_membership";
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+            final DataKind kind = newDataKind(context, parser, attrs, false,
+                    GroupMembership.CONTENT_ITEM_TYPE, null,
+                    R.string.groupsLabel, Weight.GROUP_MEMBERSHIP, -1, null, null);
+
+            kind.fieldList.add(new EditField(GroupMembership.GROUP_ROW_ID, -1, -1));
+
+            throwIfList(kind);
+
+            return Lists.newArrayList(kind);
+        }
+    }
+
+    /**
+     * Event DataKind parser.
+     *
+     * Event DataKind is used only for Google/Exchange types, so this parser is not used for now.
+     */
+    private static class EventKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "event";
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+            final DataKind kind = newDataKind(context, parser, attrs, false,
+                    Event.CONTENT_ITEM_TYPE, Event.TYPE, R.string.eventLabelsGroup, Weight.EVENT,
+                    R.layout.event_field_editor_view,
+                    new EventActionInflater(), new SimpleInflater(Event.START_DATE));
+
+            kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT));
+
+            if (getAttr(attrs, Attr.DATE_WITH_TIME, false)) {
+                kind.dateFormatWithoutYear = DateUtils.NO_YEAR_DATE_AND_TIME_FORMAT;
+                kind.dateFormatWithYear = DateUtils.DATE_AND_TIME_FORMAT;
+            } else {
+                kind.dateFormatWithoutYear = DateUtils.NO_YEAR_DATE_FORMAT;
+                kind.dateFormatWithYear = DateUtils.FULL_DATE_FORMAT;
+            }
+
+            return Lists.newArrayList(kind);
+        }
+
+        @Override
+        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+            final boolean yo = getAttr(attrs, Attr.YEAR_OPTIONAL, false);
+
+            if ("birthday".equals(type)) {
+                return buildEventType(Event.TYPE_BIRTHDAY, yo).setSpecificMax(1);
+            }
+            if ("anniversary".equals(type)) return buildEventType(Event.TYPE_ANNIVERSARY, yo);
+            if ("other".equals(type)) return buildEventType(Event.TYPE_OTHER, yo);
+            if ("custom".equals(type)) {
+                return buildEventType(Event.TYPE_CUSTOM, yo)
+                        .setSecondary(true).setCustomColumn(Event.LABEL);
+            }
+            return null;
+        }
+    }
+
+    /**
+     * Relationship DataKind parser.
+     *
+     * Relationship DataKind is used only for Google/Exchange types, so this parser is not used for
+     * now.
+     */
+    private static class RelationshipKindBuilder extends KindBuilder {
+        @Override
+        public String getTagName() {
+            return "relationship";
+        }
+
+        @Override
+        public List<DataKind> parseDataKind(Context context, XmlPullParser parser,
+                AttributeSet attrs) throws DefinitionException, XmlPullParserException,
+                IOException {
+            final DataKind kind = newDataKind(context, parser, attrs, false,
+                    Relation.CONTENT_ITEM_TYPE, Relation.TYPE,
+                    R.string.relationLabelsGroup, Weight.RELATIONSHIP,
+                    R.layout.text_fields_editor_view,
+                    new RelationActionInflater(), new SimpleInflater(Relation.NAME));
+
+            kind.fieldList.add(new EditField(Relation.DATA, R.string.relationLabelsGroup,
+                    FLAGS_RELATION));
+
+            kind.defaultValues = new ContentValues();
+            kind.defaultValues.put(Relation.TYPE, Relation.TYPE_SPOUSE);
+
+            return Lists.newArrayList(kind);
+        }
+
+        @Override
+        protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+            // EditType is mutable, so we need to create a new instance every time.
+            if ("assistant".equals(type)) return buildRelationType(Relation.TYPE_ASSISTANT);
+            if ("brother".equals(type)) return buildRelationType(Relation.TYPE_BROTHER);
+            if ("child".equals(type)) return buildRelationType(Relation.TYPE_CHILD);
+            if ("domestic_partner".equals(type)) {
+                    return buildRelationType(Relation.TYPE_DOMESTIC_PARTNER);
+            }
+            if ("father".equals(type)) return buildRelationType(Relation.TYPE_FATHER);
+            if ("friend".equals(type)) return buildRelationType(Relation.TYPE_FRIEND);
+            if ("manager".equals(type)) return buildRelationType(Relation.TYPE_MANAGER);
+            if ("mother".equals(type)) return buildRelationType(Relation.TYPE_MOTHER);
+            if ("parent".equals(type)) return buildRelationType(Relation.TYPE_PARENT);
+            if ("partner".equals(type)) return buildRelationType(Relation.TYPE_PARTNER);
+            if ("referred_by".equals(type)) return buildRelationType(Relation.TYPE_REFERRED_BY);
+            if ("relative".equals(type)) return buildRelationType(Relation.TYPE_RELATIVE);
+            if ("sister".equals(type)) return buildRelationType(Relation.TYPE_SISTER);
+            if ("spouse".equals(type)) return buildRelationType(Relation.TYPE_SPOUSE);
+            if ("custom".equals(type)) {
+                return buildRelationType(Relation.TYPE_CUSTOM).setSecondary(true)
+                        .setCustomColumn(Relation.LABEL);
+            }
+            return null;
+        }
+    }
 }
diff --git a/src/com/android/contacts/model/DataKind.java b/src/com/android/contacts/model/DataKind.java
index 70f43ab..aaf7bb5 100644
--- a/src/com/android/contacts/model/DataKind.java
+++ b/src/com/android/contacts/model/DataKind.java
@@ -33,7 +33,7 @@
  * {@link Data} rows of this kind, including the possible {@link EditType}
  * labels and editable {@link EditField}.
  */
-public class DataKind {
+public final class DataKind {
 
     public static final String PSEUDO_MIME_TYPE_DISPLAY_NAME = "#displayName";
     public static final String PSEUDO_MIME_TYPE_PHONETIC_NAME = "#phoneticName";
diff --git a/src/com/android/contacts/model/ExchangeAccountType.java b/src/com/android/contacts/model/ExchangeAccountType.java
index c109fea..bb11cf6 100644
--- a/src/com/android/contacts/model/ExchangeAccountType.java
+++ b/src/com/android/contacts/model/ExchangeAccountType.java
@@ -323,16 +323,6 @@
     }
 
     @Override
-    public int getHeaderColor(Context context) {
-        return 0xffd5ba96;
-    }
-
-    @Override
-    public int getSideBarColor(Context context) {
-        return 0xffb58e59;
-    }
-
-    @Override
     public boolean isGroupMembershipEditable() {
         return true;
     }
diff --git a/src/com/android/contacts/model/ExternalAccountType.java b/src/com/android/contacts/model/ExternalAccountType.java
index 73aa773..968993a 100644
--- a/src/com/android/contacts/model/ExternalAccountType.java
+++ b/src/com/android/contacts/model/ExternalAccountType.java
@@ -16,6 +16,8 @@
 
 package com.android.contacts.model;
 
+import com.android.contacts.R;
+import com.android.contacts.model.BaseAccountType.DefinitionException;
 import com.google.common.annotations.VisibleForTesting;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -97,11 +99,12 @@
 
         // Handle unknown sources by searching their package
         final PackageManager pm = context.getPackageManager();
+        XmlResourceParser parser = null;
         try {
             PackageInfo packageInfo = pm.getPackageInfo(resPackageName,
                     PackageManager.GET_SERVICES|PackageManager.GET_META_DATA);
             for (ServiceInfo serviceInfo : packageInfo.services) {
-                final XmlResourceParser parser = serviceInfo.loadXmlMetaData(pm,
+                parser = serviceInfo.loadXmlMetaData(pm,
                         METADATA_CONTACTS);
                 if (parser == null) continue;
                 inflate(context, parser);
@@ -109,6 +112,17 @@
         } catch (NameNotFoundException nnfe) {
             // If the package name is not found, we can't initialize this account type.
             return;
+        } catch (DefinitionException e) {
+            String message = "Problem reading XML";
+            if (parser != null) {
+                message = message + " in line " + parser.getLineNumber();
+            }
+            Log.e(TAG, message, e);
+            return;
+        } finally {
+            if (parser != null) {
+                parser.close();
+            }
         }
 
         mExtensionPackageNames = new ArrayList<String>();
@@ -138,10 +152,7 @@
         return mIsExtension;
     }
 
-    /**
-     * Whether this account type was able to be fully initialized.  This may be false if
-     * (for example) the package name associated with the account type could not be found.
-     */
+    @Override
     public boolean isInitialized() {
         return mInitSuccessful;
     }
@@ -212,7 +223,7 @@
      * Inflate this {@link AccountType} from the given parser. This may only
      * load details matching the publicly-defined schema.
      */
-    protected void inflate(Context context, XmlPullParser parser) {
+    protected void inflate(Context context, XmlPullParser parser) throws DefinitionException {
         final AttributeSet attrs = Xml.asAttributeSet(parser);
 
         try {
@@ -279,9 +290,11 @@
             final int depth = parser.getDepth();
             while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
                     && type != XmlPullParser.END_DOCUMENT) {
+
                 String tag = parser.getName();
                 if (TAG_EDIT_SCHEMA.equals(tag)) {
-                    parseEditSchema(context, parser);
+                    mHasEditSchema = true;
+                    parseEditSchema(context, parser, attrs);
                 } else if (TAG_CONTACTS_DATA_KIND.equals(tag)) {
                     final TypedArray a = context.obtainStyledAttributes(attrs,
                             android.R.styleable.ContactsDataKind);
@@ -319,53 +332,13 @@
                 }
             }
         } catch (XmlPullParserException e) {
-            throw new IllegalStateException("Problem reading XML", e);
+            throw new DefinitionException("Problem reading XML", e);
         } catch (IOException e) {
-            throw new IllegalStateException("Problem reading XML", e);
+            throw new DefinitionException("Problem reading XML", e);
         }
     }
 
     /**
-     * Has to be started while the parser is on the EditSchema tag. Will finish on the end tag
-     */
-    private void parseEditSchema(Context context, XmlPullParser parser)
-            throws XmlPullParserException, IOException {
-        // Loop until we left this tag
-        final int startingDepth = parser.getDepth();
-        int type;
-        do {
-            type = parser.next();
-        } while (!(parser.getDepth() == startingDepth && type == XmlPullParser.END_TAG));
-
-        // Just add all defaults for now
-        addDataKindStructuredName(context);
-        addDataKindDisplayName(context);
-        addDataKindPhoneticName(context);
-        addDataKindNickname(context);
-        addDataKindPhone(context);
-        addDataKindEmail(context);
-        addDataKindStructuredPostal(context);
-        addDataKindIm(context);
-        addDataKindOrganization(context);
-        addDataKindPhoto(context);
-        addDataKindNote(context);
-        addDataKindWebsite(context);
-        addDataKindSipAddress(context);
-
-        mHasEditSchema = true;
-    }
-
-    @Override
-    public int getHeaderColor(Context context) {
-        return 0xff6d86b4;
-    }
-
-    @Override
-    public int getSideBarColor(Context context) {
-        return 0xff6d86b4;
-    }
-
-    /**
      * Takes a string in the "@xxx/yyy" format and return the resource ID for the resource in
      * the resource package.
      *
diff --git a/src/com/android/contacts/model/FallbackAccountType.java b/src/com/android/contacts/model/FallbackAccountType.java
index 3b56b04..216d6d0 100644
--- a/src/com/android/contacts/model/FallbackAccountType.java
+++ b/src/com/android/contacts/model/FallbackAccountType.java
@@ -47,16 +47,6 @@
     }
 
     @Override
-    public int getHeaderColor(Context context) {
-        return 0xff7f93bc;
-    }
-
-    @Override
-    public int getSideBarColor(Context context) {
-        return 0xffbdc7b8;
-    }
-
-    @Override
     public boolean areContactsWritable() {
         return true;
     }
diff --git a/src/com/android/contacts/model/GoogleAccountType.java b/src/com/android/contacts/model/GoogleAccountType.java
index 094312b..c101602 100644
--- a/src/com/android/contacts/model/GoogleAccountType.java
+++ b/src/com/android/contacts/model/GoogleAccountType.java
@@ -26,14 +26,11 @@
 import android.provider.ContactsContract.CommonDataKinds.Event;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.Relation;
-import android.view.inputmethod.EditorInfo;
 
 import java.util.List;
 
 public class GoogleAccountType extends BaseAccountType {
     public static final String ACCOUNT_TYPE = "com.google";
-    protected static final int FLAGS_RELATION = EditorInfo.TYPE_CLASS_TEXT
-    | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS | EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME;
 
     private static final List<String> mExtensionPackages =
             Lists.newArrayList("com.google.android.apps.plus");
@@ -168,16 +165,6 @@
     }
 
     @Override
-    public int getHeaderColor(Context context) {
-        return 0xff89c2c2;
-    }
-
-    @Override
-    public int getSideBarColor(Context context) {
-        return 0xff5bb4b4;
-    }
-
-    @Override
     public boolean isGroupMembershipEditable() {
         return true;
     }
diff --git a/src/com/android/contacts/util/DateUtils.java b/src/com/android/contacts/util/DateUtils.java
index ed9eb94..1ea84a1 100644
--- a/src/com/android/contacts/util/DateUtils.java
+++ b/src/com/android/contacts/util/DateUtils.java
@@ -38,6 +38,8 @@
             new SimpleDateFormat("yyyy-MM-dd", Locale.US);
     public static final SimpleDateFormat DATE_AND_TIME_FORMAT =
             new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
+    public static final SimpleDateFormat NO_YEAR_DATE_AND_TIME_FORMAT =
+            new SimpleDateFormat("--MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
 
     // Variations of ISO 8601 date format.  Do not change the order - it does affect the
     // result in ambiguous cases.
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 739f5f0..6ea42d6 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -21,6 +21,13 @@
     <uses-permission android:name="android.permission.WRITE_CONTACTS" />
     <uses-permission android:name="android.permission.GET_ACCOUNTS" />
 
+    <uses-permission android:name="android.permission.USE_CREDENTIALS" />
+    <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
+    <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
+    <uses-permission android:name="android.permission.READ_SYNC_STATS" />
+    <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
+    <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
+
     <application>
         <uses-library android:name="android.test.runner" />
         <meta-data android:name="com.android.contacts.iconset" android:resource="@xml/iconset" />
@@ -83,6 +90,35 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
+
+        <!--
+          Test authenticators/sync adapters.
+
+          The idea is to have multiple account types with various edit schemas.  We use subclasses
+          so we could easily add multiple pairs of authenticators and sync adapters.
+          Unfortunately there's an issue with the contacts app which prevents a single apk from
+          having multiple contacts.xml files, so for now we only declare one account type here.
+        -->
+        <service android:name=".testauth.TestAuthenticationService$Basic" android:exported="true">
+            <intent-filter>
+                <action android:name="android.accounts.AccountAuthenticator" />
+            </intent-filter>
+            <meta-data
+                android:name="android.accounts.AccountAuthenticator"
+                android:resource="@xml/test_basic_authenticator" />
+        </service>
+
+        <service android:name=".testauth.TestSyncService$Basic" android:exported="true">
+            <intent-filter>
+                <action android:name="android.content.SyncAdapter" />
+            </intent-filter>
+            <meta-data
+                android:name="android.content.SyncAdapter"
+                android:resource="@xml/test_basic_syncadapter" />
+            <meta-data
+                android:name="android.provider.CONTACTS_STRUCTURE"
+                android:resource="@xml/test_basic_contacts" />
+        </service>
     </application>
 
     <instrumentation android:name="android.test.InstrumentationTestRunner"
diff --git a/tests/res/values/donottranslate_strings.xml b/tests/res/values/donottranslate_strings.xml
index 27b9176..19ebde3 100644
--- a/tests/res/values/donottranslate_strings.xml
+++ b/tests/res/values/donottranslate_strings.xml
@@ -113,4 +113,6 @@
     <string name="attribution_google_talk">Google Talk</string>
     <string name="attribution_flicker">Flicker</string>
     <string name="attribution_twitter">Twitter</string>
+
+    <string name="authenticator_basic_label">Test adapter</string>
 </resources>
diff --git a/tests/res/xml/test_basic_authenticator.xml b/tests/res/xml/test_basic_authenticator.xml
new file mode 100644
index 0000000..ecd100a
--- /dev/null
+++ b/tests/res/xml/test_basic_authenticator.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2011, 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.
+ */
+-->
+
+<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
+    android:accountType="com.android.contacts.tests.authtest.basic"
+    android:icon="@drawable/ic_contact_picture"
+    android:smallIcon="@drawable/ic_contact_picture"
+    android:label="@string/authenticator_basic_label"
+/>
diff --git a/tests/res/xml/test_basic_contacts.xml b/tests/res/xml/test_basic_contacts.xml
new file mode 100644
index 0000000..ad82706
--- /dev/null
+++ b/tests/res/xml/test_basic_contacts.xml
@@ -0,0 +1,296 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2011, 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.
+ */
+-->
+
+<!--
+    contacts.xml to build "fallback account type" equivalent.
+-->
+
+<ContactsAccountType
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    >
+    <EditSchema
+        >
+        <!--
+            Name:
+            - maxOccurs must be 1
+            - No types.
+
+            - Currently all the supportsXxx attributes must be true, but here's the plan for the
+              future:
+              (There's some hardcoded assumptions in the contact editor, which is one reason
+              for the above restriction)
+
+                - "Family name" and "Given name" must be supported.
+                    - All sync adapters must support structured name. "display name only" is not
+                      supported.
+                      -> Supporting this would require relatively large changes to
+                         the contact editor.
+
+                - Fields are decided from the attributes:
+                    StructuredName.DISPLAY_NAME         if supportsDisplayName == true
+                    StructuredName.PREFIX               if supportsPrefix == true
+                    StructuredName.FAMILY_NAME          (always)
+                    StructuredName.MIDDLE_NAME          if supportsPrefix == true
+                    StructuredName.GIVEN_NAME           (always)
+                    StructuredName.SUFFIX               if supportsSuffix == true
+                    StructuredName.PHONETIC_FAMILY_NAME if supportsPhoneticFamilyName == true
+                    StructuredName.PHONETIC_MIDDLE_NAME if supportsPhoneticMiddleName == true
+                    StructuredName.PHONETIC_GIVEN_NAME  if supportsPhoneticGivenName == true
+
+                - DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME  is always added.
+                - DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME is added
+                  if any of supportsPhoneticXxx == true
+        -->
+        <!-- Fallback/Google definition.  Supports all. -->
+        <DataKind kind="name"
+            maxOccurs="1"
+            supportsDisplayName="true"
+            supportsPrefix="true"
+            supportsMiddleName="true"
+            supportsSuffix="true"
+            supportsPhoneticFamilyName="true"
+            supportsPhoneticMiddleName="true"
+            supportsPhoneticGivenName="true"
+            >
+        </DataKind>
+
+        <!-- Exchange definition.  No display-name, no phonetic-middle.
+        <DataKind kind="name"
+            supportsDisplayName="false"
+            supportsPrefix="true"
+            supportsMiddleName="true"
+            supportsSuffix="true"
+            supportsPhoneticFamilyName="true"
+            supportsPhoneticMiddleName="false"
+            supportsPhoneticGivenName ="true"
+            >
+        </DataKind>
+        -->
+
+        <!--
+            Photo:
+            - maxOccurs must be 1
+            - No types.
+        -->
+        <DataKind kind="photo" maxOccurs="1" />
+
+        <!--
+            Phone definition.
+            - "is secondary?" is inferred from type.
+        -->
+        <!-- Fallback, Google definition.  -->
+        <DataKind kind="phone" >
+            <!-- Note: Google type doesn't have obsolete ones -->
+            <Type type="mobile" />
+            <Type type="work" />
+            <Type type="home" />
+            <Type type="fax_work" />
+            <Type type="fax_home" />
+            <Type type="pager" />
+            <Type type="other" />
+            <Type type="custom"/>
+            <Type type="callback" />
+            <Type type="car" />
+            <Type type="company_main" />
+            <Type type="isdn" />
+            <Type type="main" />
+            <Type type="other_fax" />
+            <Type type="radio" />
+            <Type type="telex" />
+            <Type type="tty_tdd" />
+            <Type type="work_mobile"/>
+            <Type type="work_pager" />
+            <Type type="assistant" />
+            <Type type="mms" />
+        </DataKind>
+
+        <!-- Exchange definition.
+        <DataKind kind="phone" >
+            <Type type="home" maxOccurs="2" />
+            <Type type="mobile" maxOccurs="1" />
+            <Type type="work" maxOccurs="2" />
+            <Type type="fax_work" maxOccurs="1" />
+            <Type type="fax_home" maxOccurs="1" />
+            <Type type="pager" maxOccurs="1" />
+            <Type type="car" maxOccurs="1" />
+            <Type type="company_main" maxOccurs="1" />
+            <Type type="mms" maxOccurs="1" />
+            <Type type="radio" maxOccurs="1" />
+            <Type type="assistant" maxOccurs="1" />
+        </DataKind>
+        -->
+
+        <!--
+            Email
+        -->
+        <!-- Fallback/ Google definition.  -->
+        <DataKind kind="email" >
+            <!-- Note: Google type doesn't support some of these. -->
+            <Type type="home" />
+            <Type type="work" />
+            <Type type="other" />
+            <Type type="mobile" />
+            <Type type="custom" />
+        </DataKind>
+
+        <!--
+            Exchange definition.
+            - Same definition as "fallback" except for maxOccurs=3
+        <DataKind kind="email" maxOccurs="3" >
+            <Type type="home" />
+            <Type type="work" />
+            <Type type="other" />
+            <Type type="mobile" />
+            <Type type="custom" />
+        </DataKind>
+        -->
+
+        <!--
+            Nickname
+            - maxOccurs must be 1
+            - No types.
+        -->
+        <DataKind kind="nickname" maxOccurs="1" />
+
+        <!--
+            Im:
+             - The TYPE column always stores Im.TYPE_OTHER (defaultValues is always set)
+             - The user-selected type is stored in Im.PROTOCOL
+        -->
+        <!-- Fallback, Google definition.  -->
+        <DataKind kind="im" >
+            <Type type="aim" />
+            <Type type="msn" />
+            <Type type="yahoo" />
+            <Type type="skype" />
+            <Type type="qq" />
+            <Type type="google_talk" />
+            <Type type="icq" />
+            <Type type="jabber" />
+            <Type type="custom" />
+        </DataKind>
+
+        <!-- Exchange definition.
+        <DataKind kind="im" maxOccurs="3" >
+            <Type type="aim" />
+            <Type type="msn" />
+            <Type type="yahoo" />
+            <Type type="skype" />
+            <Type type="qq" />
+            <Type type="google_talk" />
+            <Type type="icq" />
+            <Type type="jabber" />
+            <Type type="custom" />
+        </DataKind>
+        -->
+
+        <!--
+            Postal address.
+        -->
+        <!-- Fallback/Google definition.  Not structured. -->
+        <DataKind kind="postal" needsStructured="false" >
+            <Type type="home" />
+            <Type type="work" />
+            <Type type="other" />
+            <Type type="custom" />
+        </DataKind>
+
+        <!-- Exchange definition.  Structured.
+        <DataKind kind="postal" needsStructured="true" >
+            <Type type="work" />
+            <Type type="home" />
+            <Type type="other" />
+        </DataKind>
+        -->
+
+        <!--
+            Organization:
+            - Fields are fixed: COMPANY, TITLE
+            - maxOccurs must be 1
+            - No types.
+        -->
+        <DataKind kind="organization" maxOccurs="1" />
+
+        <!--
+            Website:
+            - No types.
+        -->
+        <DataKind kind="website" />
+
+        <!--
+            Below kinds have nothing configurable.
+            - No types are supported.
+            - maxOccurs must be 1
+        -->
+        <DataKind kind="sip_address" maxOccurs="1" />
+        <DataKind kind="note" maxOccurs="1" />
+
+        <!--
+            Google/Exchange supports it, but fallback doesn't.
+        <DataKind kind="group_membership" maxOccurs="1" />
+        -->
+
+        <!--
+            Event
+
+            The parser should be able to handle it, but not tested.
+        -->
+        <!-- Google definition.
+        <DataKind kind="event" dateWithTime="false">
+            <Type type="birthday" maxOccurs="1" yearOptional="true" />
+            <Type type="anniversary" />
+            <Type type="other" />
+            <Type type="custom" />
+        </DataKind>
+        -->
+
+        <!--
+            Exchange definition.  dateWithTime is needed only for Exchange.
+        <DataKind kind="event" dateWithTime="true">
+            <Type type="birthday" maxOccurs="1" />
+        </DataKind>
+        -->
+
+        <!--
+            Relationship.
+
+            The parser should be able to handle it, but not tested.
+
+        <DataKind kind="relation" >
+            <Type type="assistant" />
+            <Type type="brother" />
+            <Type type="child" />
+            <Type type="domestic_partner" />
+            <Type type="father" />
+            <Type type="friend" />
+            <Type type="manager" />
+            <Type type="mother" />
+            <Type type="parent" />
+            <Type type="partner" />
+            <Type type="referred_by" />
+            <Type type="relative" />
+            <Type type="sister" />
+            <Type type="spouse" />
+            <Type type="custom" />
+        </DataKind>
+        -->
+
+    </EditSchema>
+
+</ContactsAccountType>
diff --git a/tests/res/xml/test_basic_syncadapter.xml b/tests/res/xml/test_basic_syncadapter.xml
new file mode 100644
index 0000000..fecc0eb
--- /dev/null
+++ b/tests/res/xml/test_basic_syncadapter.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2011, 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.
+ */
+-->
+
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+    android:contentAuthority="com.android.contacts"
+    android:accountType="com.android.contacts.tests.authtest.basic"
+    android:supportsUploading="true"
+    android:userVisible="true"
+/>
diff --git a/tests/src/com/android/contacts/EntityModifierTests.java b/tests/src/com/android/contacts/EntityModifierTests.java
index d6f99ce..4db73b3 100644
--- a/tests/src/com/android/contacts/EntityModifierTests.java
+++ b/tests/src/com/android/contacts/EntityModifierTests.java
@@ -140,16 +140,6 @@
         }
 
         @Override
-        public int getHeaderColor(Context context) {
-            return 0;
-        }
-
-        @Override
-        public int getSideBarColor(Context context) {
-            return 0xffffff;
-        }
-
-        @Override
         public boolean isGroupMembershipEditable() {
             return false;
         }
diff --git a/tests/src/com/android/contacts/editor/ContactEditorUtilsTest.java b/tests/src/com/android/contacts/editor/ContactEditorUtilsTest.java
index b2cb39c..8007aee 100644
--- a/tests/src/com/android/contacts/editor/ContactEditorUtilsTest.java
+++ b/tests/src/com/android/contacts/editor/ContactEditorUtilsTest.java
@@ -313,16 +313,6 @@
         }
 
         @Override
-        public int getHeaderColor(Context context) {
-            return 0;
-        }
-
-        @Override
-        public int getSideBarColor(Context context) {
-            return 0;
-        }
-
-        @Override
         public boolean isGroupMembershipEditable() {
             return true;
         }
diff --git a/tests/src/com/android/contacts/model/AccountTypeManagerTest.java b/tests/src/com/android/contacts/model/AccountTypeManagerTest.java
index 09902a3..6f5bbf2 100644
--- a/tests/src/com/android/contacts/model/AccountTypeManagerTest.java
+++ b/tests/src/com/android/contacts/model/AccountTypeManagerTest.java
@@ -183,16 +183,6 @@
         }
 
         @Override
-        public int getHeaderColor(Context context) {
-            return 0;
-        }
-
-        @Override
-        public int getSideBarColor(Context context) {
-            return 0;
-        }
-
-        @Override
         public boolean isGroupMembershipEditable() {
             return false;
         }
diff --git a/tests/src/com/android/contacts/model/AccountTypeTest.java b/tests/src/com/android/contacts/model/AccountTypeTest.java
index 42fe200..8bc7429 100644
--- a/tests/src/com/android/contacts/model/AccountTypeTest.java
+++ b/tests/src/com/android/contacts/model/AccountTypeTest.java
@@ -73,14 +73,6 @@
                 return externalResID;
             }
 
-            @Override public int getHeaderColor(Context context) {
-                return 0;
-            }
-
-            @Override public int getSideBarColor(Context context) {
-                return 0;
-            }
-
             @Override public boolean isGroupMembershipEditable() {
                 return false;
             }
@@ -128,16 +120,6 @@
         }
 
         @Override
-        public int getHeaderColor(Context context) {
-            return 0;
-        }
-
-        @Override
-        public int getSideBarColor(Context context) {
-            return 0;
-        }
-
-        @Override
         public boolean isGroupMembershipEditable() {
             return false;
         }
diff --git a/tests/src/com/android/contacts/model/ExternalAccountTypeTest.java b/tests/src/com/android/contacts/model/ExternalAccountTypeTest.java
index eb8c059..15a1320 100644
--- a/tests/src/com/android/contacts/model/ExternalAccountTypeTest.java
+++ b/tests/src/com/android/contacts/model/ExternalAccountTypeTest.java
@@ -19,6 +19,16 @@
 import com.android.contacts.tests.R;
 
 import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 
@@ -52,4 +62,26 @@
         assertEquals(R.string.test_string, ExternalAccountType.resolveExternalResId(c,
                 "@string/test_string", packageName, ""));
     }
+
+    public void testEditSchema() {
+        final ExternalAccountType type = new ExternalAccountType(getContext(),
+                getTestContext().getPackageName(), false);
+
+        assertTrue(type.isInitialized());
+
+        assertNotNull(type.getKindForMimetype(StructuredName.CONTENT_ITEM_TYPE));
+        assertNotNull(type.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME));
+        assertNotNull(type.getKindForMimetype(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME));
+        assertNotNull(type.getKindForMimetype(Email.CONTENT_ITEM_TYPE));
+        assertNotNull(type.getKindForMimetype(StructuredPostal.CONTENT_ITEM_TYPE));
+        assertNotNull(type.getKindForMimetype(Im.CONTENT_ITEM_TYPE));
+        assertNotNull(type.getKindForMimetype(Organization.CONTENT_ITEM_TYPE));
+        assertNotNull(type.getKindForMimetype(Photo.CONTENT_ITEM_TYPE));
+        assertNotNull(type.getKindForMimetype(Note.CONTENT_ITEM_TYPE));
+        assertNotNull(type.getKindForMimetype(Website.CONTENT_ITEM_TYPE));
+        assertNotNull(type.getKindForMimetype(SipAddress.CONTENT_ITEM_TYPE));
+        assertNotNull(type.getKindForMimetype(GroupMembership.CONTENT_ITEM_TYPE));
+
+        // TODO Write more extensive check -- compare to FallbackAccountType?
+    }
 }
diff --git a/tests/src/com/android/contacts/tests/testauth/TestAuthenticationService.java b/tests/src/com/android/contacts/tests/testauth/TestAuthenticationService.java
new file mode 100644
index 0000000..84f3f0f
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/testauth/TestAuthenticationService.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2011 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.tests.testauth;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.util.Log;
+
+public abstract class TestAuthenticationService extends Service {
+
+    private TestAuthenticator mAuthenticator;
+
+    @Override
+    public void onCreate() {
+        Log.v(TestauthConstants.LOG_TAG, this + " Service started.");
+        mAuthenticator = new TestAuthenticator(this);
+    }
+
+    @Override
+    public void onDestroy() {
+        Log.v(TestauthConstants.LOG_TAG, this + " Service stopped.");
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        Log.v(TestauthConstants.LOG_TAG, this + " getBinder() intent=" + intent);
+        return mAuthenticator.getIBinder();
+    }
+
+    public static class Basic extends TestAuthenticationService {
+    }
+}
diff --git a/tests/src/com/android/contacts/tests/testauth/TestAuthenticator.java b/tests/src/com/android/contacts/tests/testauth/TestAuthenticator.java
new file mode 100644
index 0000000..97e2e4d
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/testauth/TestAuthenticator.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2011 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.tests.testauth;
+
+import android.accounts.AbstractAccountAuthenticator;
+import android.accounts.Account;
+import android.accounts.AccountAuthenticatorResponse;
+import android.accounts.AccountManager;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+/**
+ * Simple authenticator.  It has no "login" dialogs/activities.  When you add a new account, it'll
+ * just create a new account with a unique name.
+ */
+class TestAuthenticator extends AbstractAccountAuthenticator {
+    private static final String PASSWORD = "xxx"; // any string will do.
+
+    // To remember the last user-ID.
+    private static final String PREF_KEY_LAST_USER_ID = "TestAuthenticator.PREF_KEY_LAST_USER_ID";
+
+    private final Context mContext;
+
+    public TestAuthenticator(Context context) {
+        super(context);
+        mContext = context.getApplicationContext();
+    }
+
+    /**
+     * @return a new, unique username.
+     */
+    private String newUniqueUserName() {
+        final SharedPreferences prefs =
+                PreferenceManager.getDefaultSharedPreferences(mContext);
+        final int nextId = prefs.getInt(PREF_KEY_LAST_USER_ID, 0) + 1;
+        prefs.edit().putInt(PREF_KEY_LAST_USER_ID, nextId).apply();
+
+        return "User-" + nextId;
+    }
+
+    /**
+     * Create a new account with the name generated by {@link #newUniqueUserName()}.
+     */
+    @Override
+    public Bundle addAccount(AccountAuthenticatorResponse response, String accountType,
+            String authTokenType, String[] requiredFeatures, Bundle options) {
+        Log.v(TestauthConstants.LOG_TAG, "addAccount() type=" + accountType);
+        final Bundle bundle = new Bundle();
+
+        final Account account = new Account(newUniqueUserName(), accountType);
+
+        // Create an account.
+        AccountManager.get(mContext).addAccountExplicitly(account, PASSWORD, null);
+
+        // And return it.
+        bundle.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
+        bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
+        return bundle;
+    }
+
+    /**
+     * Just return the user name as the authtoken.
+     */
+    @Override
+    public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
+            String authTokenType, Bundle loginOptions) {
+        Log.v(TestauthConstants.LOG_TAG, "getAuthToken() account=" + account);
+        final Bundle bundle = new Bundle();
+        bundle.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
+        bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
+        bundle.putString(AccountManager.KEY_AUTHTOKEN, account.name);
+
+        return bundle;
+    }
+
+    @Override
+    public Bundle confirmCredentials(
+            AccountAuthenticatorResponse response, Account account, Bundle options) {
+        Log.v(TestauthConstants.LOG_TAG, "confirmCredentials()");
+        return null;
+    }
+
+    @Override
+    public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
+        Log.v(TestauthConstants.LOG_TAG, "editProperties()");
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getAuthTokenLabel(String authTokenType) {
+        // null means we don't support multiple authToken types
+        Log.v(TestauthConstants.LOG_TAG, "getAuthTokenLabel()");
+        return null;
+    }
+
+    @Override
+    public Bundle hasFeatures(
+            AccountAuthenticatorResponse response, Account account, String[] features) {
+        // This call is used to query whether the Authenticator supports
+        // specific features. We don't expect to get called, so we always
+        // return false (no) for any queries.
+        Log.v(TestauthConstants.LOG_TAG, "hasFeatures()");
+        final Bundle result = new Bundle();
+        result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
+        return result;
+    }
+
+    @Override
+    public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account,
+            String authTokenType, Bundle loginOptions) {
+        Log.v(TestauthConstants.LOG_TAG, "updateCredentials()");
+        return null;
+    }
+}
diff --git a/tests/src/com/android/contacts/tests/testauth/TestSyncAdapter.java b/tests/src/com/android/contacts/tests/testauth/TestSyncAdapter.java
new file mode 100644
index 0000000..9e6fbf8
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/testauth/TestSyncAdapter.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2011 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.tests.testauth;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.AbstractThreadedSyncAdapter;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SyncResult;
+import android.os.Bundle;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
+
+/**
+ * Simple (minimal) sync adapter.
+ *
+ */
+public class TestSyncAdapter extends AbstractThreadedSyncAdapter {
+    private final AccountManager mAccountManager;
+
+    private final Context mContext;
+
+    public TestSyncAdapter(Context context, boolean autoInitialize) {
+        super(context, autoInitialize);
+        mContext = context.getApplicationContext();
+        mAccountManager = AccountManager.get(mContext);
+    }
+
+    /**
+     * Doesn't actually sync, but sweep up all existing local-only contacts.
+     */
+    @Override
+    public void onPerformSync(Account account, Bundle extras, String authority,
+            ContentProviderClient provider, SyncResult syncResult) {
+        Log.v(TestauthConstants.LOG_TAG, "TestSyncAdapter.onPerformSync() account=" + account);
+
+        // First, claim all local-only contacts, if any.
+        ContentResolver cr = mContext.getContentResolver();
+        ContentValues values = new ContentValues();
+        values.put(RawContacts.ACCOUNT_NAME, account.name);
+        values.put(RawContacts.ACCOUNT_TYPE, account.type);
+        final int count = cr.update(RawContacts.CONTENT_URI, values,
+                RawContacts.ACCOUNT_NAME + " IS NULL AND " + RawContacts.ACCOUNT_TYPE + " IS NULL",
+                null);
+        if (count > 0) {
+            Log.v(TestauthConstants.LOG_TAG, "Claimed " + count + " local raw contacts");
+        }
+
+        // TODO: Clear isDirty flag
+        // TODO: Remove isDeleted raw contacts
+    }
+}
diff --git a/tests/src/com/android/contacts/tests/testauth/TestSyncService.java b/tests/src/com/android/contacts/tests/testauth/TestSyncService.java
new file mode 100644
index 0000000..9928777
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/testauth/TestSyncService.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2011 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.tests.testauth;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+public abstract class TestSyncService extends Service {
+
+    private static TestSyncAdapter sSyncAdapter;
+
+    @Override
+    public void onCreate() {
+        if (sSyncAdapter == null) {
+            sSyncAdapter = new TestSyncAdapter(getApplicationContext(), true);
+        }
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return sSyncAdapter.getSyncAdapterBinder();
+    }
+
+    public static class Basic extends TestSyncService {
+    }
+}
diff --git a/tests/src/com/android/contacts/tests/testauth/TestauthConstants.java b/tests/src/com/android/contacts/tests/testauth/TestauthConstants.java
new file mode 100644
index 0000000..717ed35
--- /dev/null
+++ b/tests/src/com/android/contacts/tests/testauth/TestauthConstants.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2011 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.tests.testauth;
+
+class TestauthConstants {
+    public static final String LOG_TAG = "Testauth";
+}