EditSchema parser for ExternalAccountType

This also includes:
- Removed getHeaderColor/getSideBarColor from AccountType

- Implemented a test authenticator/sync adapter in the test apk.
This defines a test account type "com.android.contacts.tests.authtest.basic".
We could potentially add more account types to the test apk to test different
edit schema variations, but at this point this is impossible, as
ExternalAccountType doesn't have a way to tell which contacts.xml belongs
to which account type.  The current contacts.xml defined here builds
the fallback account type equivalent.

The sync adapter is pretty rudimentary; it doesn't clear the isDirty flag
on modified raw contacts or delete raw contacts with isDeleted set.  At
this point this doesn't seem to be necessary to test EditSchema.

Note:
For now it's still not meant to be public API.  Right now it's only manually
tested with the edit schema defition in the test apk described above.

Bug 5381810

Change-Id: Ifefdb969b4e08775125924b1366d24effc4db2f2
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";
+}