Follow up to EditSchema parser

- Now AccountType.addKind() throws DefinitionException instead of just logging.

- Now the test contacts.xml (= test_basic_contacts.xml) defines "event" and
"relationship" DataKinds too.  BUA should be able to copy this file.

- Added another xml, contacts_fallback.xml, to the test apk.
This defines what's equivalent to the fallback type.  Unittests load this file
directly and compares the result to the fallback account type.

- Cleaned up existing account definitions in order to make sure
contacts_fallback.xml is really identical to the fallback type. This includes:

** Now structured name, display name, phonetic name, and phone DataKinds
all have 'kind.typeOverallMax = 1'.

** The "assistant" phone type is no longer a custom column.  It's only used for
the fallback type and I don't think it's too critical.

- Also, NameKindBuilder no longer re-order phonetic fields, because no
other account types do this.  In the previous CL I did it because I thought
that'd be more "correct", but on the second thought it's probably not a good
idea to make too many non-critical changes at this point.

Bug 5381810

Change-Id: Ie6a4eb3b876ab22a3dcdb6a9c278e387f8166125
diff --git a/tests/res/xml/contacts_fallback.xml b/tests/res/xml/contacts_fallback.xml
new file mode 100644
index 0000000..ae262eb
--- /dev/null
+++ b/tests/res/xml/contacts_fallback.xml
@@ -0,0 +1,96 @@
+<?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.
+    This is directly used in ExternalAccountTypeTest to test the parser.  There's no sync adapter
+    that actually defined with this definition.
+-->
+
+<ContactsAccountType
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    >
+    <EditSchema
+        >
+        <DataKind kind="name"
+            maxOccurs="1"
+            supportsDisplayName="true"
+            supportsPrefix="true"
+            supportsMiddleName="true"
+            supportsSuffix="true"
+            supportsPhoneticFamilyName="true"
+            supportsPhoneticMiddleName="true"
+            supportsPhoneticGivenName="true"
+            >
+        </DataKind>
+        <DataKind kind="photo" maxOccurs="1" />
+        <DataKind kind="phone" >
+            <Type type="mobile" />
+            <Type type="home" />
+            <Type type="work" />
+            <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>
+        <DataKind kind="email" >
+            <Type type="home" />
+            <Type type="work" />
+            <Type type="other" />
+            <Type type="mobile" />
+            <Type type="custom" />
+        </DataKind>
+        <DataKind kind="nickname" maxOccurs="1" />
+        <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>
+        <DataKind kind="postal" needsStructured="false" >
+            <Type type="home" />
+            <Type type="work" />
+            <Type type="other" />
+            <Type type="custom" />
+        </DataKind>
+        <DataKind kind="organization" maxOccurs="1" />
+        <DataKind kind="website" />
+        <DataKind kind="sip_address" maxOccurs="1" />
+        <DataKind kind="note" maxOccurs="1" />
+    </EditSchema>
+</ContactsAccountType>
diff --git a/tests/res/xml/test_basic_contacts.xml b/tests/res/xml/test_basic_contacts.xml
index ad82706..0047204 100644
--- a/tests/res/xml/test_basic_contacts.xml
+++ b/tests/res/xml/test_basic_contacts.xml
@@ -17,10 +17,6 @@
  */
 -->
 
-<!--
-    contacts.xml to build "fallback account type" equivalent.
--->
-
 <ContactsAccountType
     xmlns:android="http://schemas.android.com/apk/res/android"
     >
@@ -98,8 +94,8 @@
         <DataKind kind="phone" >
             <!-- Note: Google type doesn't have obsolete ones -->
             <Type type="mobile" />
-            <Type type="work" />
             <Type type="home" />
+            <Type type="work" />
             <Type type="fax_work" />
             <Type type="fax_home" />
             <Type type="pager" />
@@ -139,9 +135,9 @@
         <!--
             Email
         -->
-        <!-- Fallback/ Google definition.  -->
+        <!-- Fallback/Google definition.  -->
         <DataKind kind="email" >
-            <!-- Note: Google type doesn't support some of these. -->
+            <!-- Note: Google type doesn't have obsolete ones -->
             <Type type="home" />
             <Type type="work" />
             <Type type="other" />
@@ -248,17 +244,13 @@
 
         <!--
             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.
@@ -268,11 +260,9 @@
         -->
 
         <!--
-            Relationship.
-
-            The parser should be able to handle it, but not tested.
-
-        <DataKind kind="relation" >
+            Relationship
+        -->
+        <DataKind kind="relationship" >
             <Type type="assistant" />
             <Type type="brother" />
             <Type type="child" />
@@ -289,8 +279,5 @@
             <Type type="spouse" />
             <Type type="custom" />
         </DataKind>
-        -->
-
     </EditSchema>
-
 </ContactsAccountType>
diff --git a/tests/src/com/android/contacts/EntityModifierTests.java b/tests/src/com/android/contacts/EntityModifierTests.java
index 4db73b3..6872604 100644
--- a/tests/src/com/android/contacts/EntityModifierTests.java
+++ b/tests/src/com/android/contacts/EntityModifierTests.java
@@ -20,8 +20,6 @@
 import static android.content.ContentProviderOperation.TYPE_INSERT;
 import static android.content.ContentProviderOperation.TYPE_UPDATE;
 
-import com.google.android.collect.Lists;
-
 import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountType.EditType;
 import com.android.contacts.model.AccountTypeManager;
@@ -35,10 +33,10 @@
 import com.android.contacts.tests.mocks.ContactsMockContext;
 import com.android.contacts.tests.mocks.MockAccountTypeManager;
 import com.android.contacts.tests.mocks.MockContentProvider;
+import com.google.android.collect.Lists;
 
 import android.content.ContentProviderOperation;
 import android.content.ContentValues;
-import android.content.Context;
 import android.content.Entity;
 import android.net.Uri;
 import android.os.Bundle;
@@ -87,56 +85,60 @@
     public static class MockContactsSource extends AccountType {
 
         MockContactsSource() {
-            this.accountType = TEST_ACCOUNT_TYPE;
+            try {
+                this.accountType = TEST_ACCOUNT_TYPE;
 
-            final DataKind nameKind = new DataKind(StructuredName.CONTENT_ITEM_TYPE,
-                    R.string.nameLabelsGroup, -1, true, -1);
-            nameKind.typeOverallMax = 1;
-            addKind(nameKind);
+                final DataKind nameKind = new DataKind(StructuredName.CONTENT_ITEM_TYPE,
+                        R.string.nameLabelsGroup, -1, true, -1);
+                nameKind.typeOverallMax = 1;
+                addKind(nameKind);
 
-            // Phone allows maximum 2 home, 1 work, and unlimited other, with
-            // constraint of 5 numbers maximum.
-            final DataKind phoneKind = new DataKind(
-                    Phone.CONTENT_ITEM_TYPE, -1, 10, true, -1);
+                // Phone allows maximum 2 home, 1 work, and unlimited other, with
+                // constraint of 5 numbers maximum.
+                final DataKind phoneKind = new DataKind(
+                        Phone.CONTENT_ITEM_TYPE, -1, 10, true, -1);
 
-            phoneKind.typeOverallMax = 5;
-            phoneKind.typeColumn = Phone.TYPE;
-            phoneKind.typeList = Lists.newArrayList();
-            phoneKind.typeList.add(new EditType(Phone.TYPE_HOME, -1).setSpecificMax(2));
-            phoneKind.typeList.add(new EditType(Phone.TYPE_WORK, -1).setSpecificMax(1));
-            phoneKind.typeList.add(new EditType(Phone.TYPE_FAX_WORK, -1).setSecondary(true));
-            phoneKind.typeList.add(new EditType(Phone.TYPE_OTHER, -1));
+                phoneKind.typeOverallMax = 5;
+                phoneKind.typeColumn = Phone.TYPE;
+                phoneKind.typeList = Lists.newArrayList();
+                phoneKind.typeList.add(new EditType(Phone.TYPE_HOME, -1).setSpecificMax(2));
+                phoneKind.typeList.add(new EditType(Phone.TYPE_WORK, -1).setSpecificMax(1));
+                phoneKind.typeList.add(new EditType(Phone.TYPE_FAX_WORK, -1).setSecondary(true));
+                phoneKind.typeList.add(new EditType(Phone.TYPE_OTHER, -1));
 
-            phoneKind.fieldList = Lists.newArrayList();
-            phoneKind.fieldList.add(new EditField(Phone.NUMBER, -1, -1));
-            phoneKind.fieldList.add(new EditField(Phone.LABEL, -1, -1));
+                phoneKind.fieldList = Lists.newArrayList();
+                phoneKind.fieldList.add(new EditField(Phone.NUMBER, -1, -1));
+                phoneKind.fieldList.add(new EditField(Phone.LABEL, -1, -1));
 
-            addKind(phoneKind);
+                addKind(phoneKind);
 
-            // Email is unlimited
-            final DataKind emailKind = new DataKind(
-                    Email.CONTENT_ITEM_TYPE, -1, 10, true, -1);
-            emailKind.typeOverallMax = -1;
-            emailKind.fieldList = Lists.newArrayList();
-            emailKind.fieldList.add(new EditField(Email.DATA, -1, -1));
-            addKind(emailKind);
+                // Email is unlimited
+                final DataKind emailKind = new DataKind(
+                        Email.CONTENT_ITEM_TYPE, -1, 10, true, -1);
+                emailKind.typeOverallMax = -1;
+                emailKind.fieldList = Lists.newArrayList();
+                emailKind.fieldList.add(new EditField(Email.DATA, -1, -1));
+                addKind(emailKind);
 
-            // IM is only one
-            final DataKind imKind = new DataKind(Im.CONTENT_ITEM_TYPE, -1, 10,
-                    true, -1);
-            imKind.typeOverallMax = 1;
-            imKind.fieldList = Lists.newArrayList();
-            imKind.fieldList.add(new EditField(Im.DATA, -1, -1));
-            addKind(imKind);
+                // IM is only one
+                final DataKind imKind = new DataKind(Im.CONTENT_ITEM_TYPE, -1, 10,
+                        true, -1);
+                imKind.typeOverallMax = 1;
+                imKind.fieldList = Lists.newArrayList();
+                imKind.fieldList.add(new EditField(Im.DATA, -1, -1));
+                addKind(imKind);
 
-            // Organization is only one
-            final DataKind orgKind = new DataKind(
-                    Organization.CONTENT_ITEM_TYPE, -1, 10, true, -1);
-            orgKind.typeOverallMax = 1;
-            orgKind.fieldList = Lists.newArrayList();
-            orgKind.fieldList.add(new EditField(Organization.COMPANY, -1, -1));
-            orgKind.fieldList.add(new EditField(Organization.TITLE, -1, -1));
-            addKind(orgKind);
+                // Organization is only one
+                final DataKind orgKind = new DataKind(
+                        Organization.CONTENT_ITEM_TYPE, -1, 10, true, -1);
+                orgKind.typeOverallMax = 1;
+                orgKind.fieldList = Lists.newArrayList();
+                orgKind.fieldList.add(new EditField(Organization.COMPANY, -1, -1));
+                orgKind.fieldList.add(new EditField(Organization.TITLE, -1, -1));
+                addKind(orgKind);
+            } catch (DefinitionException e) {
+                throw new RuntimeException(e);
+            }
         }
 
         @Override
diff --git a/tests/src/com/android/contacts/model/ExternalAccountTypeTest.java b/tests/src/com/android/contacts/model/ExternalAccountTypeTest.java
index 15a1320..ba3d0eb 100644
--- a/tests/src/com/android/contacts/model/ExternalAccountTypeTest.java
+++ b/tests/src/com/android/contacts/model/ExternalAccountTypeTest.java
@@ -19,12 +19,15 @@
 import com.android.contacts.tests.R;
 
 import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.XmlResourceParser;
 import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Event;
 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.Relation;
 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
@@ -32,6 +35,10 @@
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import java.util.List;
+
+import libcore.util.Objects;
+
 /**
  * Test case for {@link ExternalAccountType}.
  *
@@ -40,7 +47,6 @@
  */
 @SmallTest
 public class ExternalAccountTypeTest extends AndroidTestCase {
-
     public void testResolveExternalResId() {
         final Context c = getContext();
         // In this test we use the test package itself as an external package.
@@ -63,12 +69,36 @@
                 "@string/test_string", packageName, ""));
     }
 
+    /**
+     * Initialize with an invalid package name and see if type type will *not* be initialized.
+     */
+    public void testNoPackage() {
+        final ExternalAccountType type = new ExternalAccountType(getContext(),
+                "!!!no such package name!!!", false);
+        assertFalse(type.isInitialized());
+    }
+
+    /**
+     * Initialize with the name of an existing package, which has no contacts.xml metadata.
+     */
+    public void testNoMetadata() {
+        // Use the main application package, which does exist, but has no contacts.xml in it.
+        String packageName = getContext().getPackageName();
+        final ExternalAccountType type = new ExternalAccountType(getContext(),
+                packageName, false);
+        assertTrue(type.isInitialized());
+    }
+
+    /**
+     * Initialize with the test package itself and see if EditSchema is correctly parsed.
+     */
     public void testEditSchema() {
         final ExternalAccountType type = new ExternalAccountType(getContext(),
                 getTestContext().getPackageName(), false);
 
         assertTrue(type.isInitialized());
 
+        // Let's just check if the DataKinds are registered.
         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));
@@ -80,8 +110,69 @@
         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));
+        assertNotNull(type.getKindForMimetype(Event.CONTENT_ITEM_TYPE));
+        assertNotNull(type.getKindForMimetype(Relation.CONTENT_ITEM_TYPE));
+    }
 
-        // TODO Write more extensive check -- compare to FallbackAccountType?
+    /**
+     * Initialize with "contacts_fallback.xml" and compare the DataKinds to those of
+     * {@link FallbackAccountType}.
+     */
+    public void testEditSchema_fallback() {
+        final ExternalAccountType type = new ExternalAccountType(getContext(),
+                getTestContext().getPackageName(), false,
+                getTestContext().getResources().getXml(R.xml.contacts_fallback)
+                );
+
+        assertTrue(type.isInitialized());
+
+        // Create a fallback type with the same resource package name, and compare all the data
+        // kinds to its.
+        final AccountType reference = FallbackAccountType.createForTest(
+                getContext(), type.resPackageName);
+
+        assertsDataKindEquals(reference.getSortedDataKinds(), type.getSortedDataKinds());
+    }
+
+    private static void assertsDataKindEquals(List<DataKind> expectedKinds,
+            List<DataKind> actualKinds) {
+        final int count = Math.max(actualKinds.size(), expectedKinds.size());
+        for (int i = 0; i < count; i++) {
+            String actual =  actualKinds.size() > i ? actualKinds.get(i).toString() : "(n/a)";
+            String expected =  expectedKinds.size() > i ? expectedKinds.get(i).toString() : "(n/a)";
+
+            // Because assertEquals()'s output is not very friendly when comparing two similar
+            // strings, we manually do the check.
+            if (!Objects.equal(actual, expected)) {
+                final int commonPrefixEnd = findCommonPrefixEnd(actual, expected);
+                fail("Kind #" + i
+                        + "\n[Actual]\n" + insertMarkerAt(actual, commonPrefixEnd)
+                        + "\n[Expected]\n" + insertMarkerAt(expected, commonPrefixEnd));
+            }
+        }
+    }
+
+    private static int findCommonPrefixEnd(String s1, String s2) {
+        int i = 0;
+        for (;;) {
+            final boolean s1End = (s1.length() <= i);
+            final boolean s2End = (s2.length() <= i);
+            if (s1End || s2End) {
+                return i;
+            }
+            if (s1.charAt(i) != s2.charAt(i)) {
+                return i;
+            }
+            i++;
+        }
+    }
+
+    private static String insertMarkerAt(String s, int position) {
+        final String MARKER = "***";
+        if (position > s.length()) {
+            return s + MARKER;
+        } else {
+            return new StringBuilder(s).insert(position, MARKER).toString();
+        }
     }
 }