SampleSyncAdapter sample code.
diff --git a/samples/SampleSyncAdapter/Android.mk b/samples/SampleSyncAdapter/Android.mk
new file mode 100644
index 0000000..a27a68f
--- /dev/null
+++ b/samples/SampleSyncAdapter/Android.mk
@@ -0,0 +1,16 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := samples tests
+
+# Only compile source java files in this apk.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := Voiper
+
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_PACKAGE)
+
+# Use the folloing include to make our test apk.
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/samples/SampleSyncAdapter/AndroidManifest.xml b/samples/SampleSyncAdapter/AndroidManifest.xml
new file mode 100644
index 0000000..7f9f83b
--- /dev/null
+++ b/samples/SampleSyncAdapter/AndroidManifest.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2010, 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.
+ */
+-->
+<manifest
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.android.samplesync"
+    android:versionCode="1"
+    android:versionName="1.0">
+    <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.INTERNET" />
+    <uses-permission
+        android:name="android.permission.WRITE_SETTINGS" />
+    <uses-permission
+        android:name="android.permission.WRITE_SECURE_SETTINGS" />
+    <uses-permission
+        android:name="android.permission.READ_CONTACTS" />
+    <uses-permission
+        android:name="android.permission.WRITE_CONTACTS" />
+    <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
+        android:icon="@drawable/icon"
+        android:label="@string/label">
+        <!-- The authenticator service -->
+        <service
+            android:name=".authenticator.AuthenticationService"
+            android:exported="true">
+            <intent-filter>
+                <action
+                    android:name="android.accounts.AccountAuthenticator" />
+            </intent-filter>
+            <meta-data
+                android:name="android.accounts.AccountAuthenticator"
+                android:resource="@xml/authenticator" />
+        </service>
+        <service
+            android:name=".syncadapter.SyncService"
+            android:exported="true">
+            <intent-filter>
+                <action
+                    android:name="android.content.SyncAdapter" />
+            </intent-filter>
+            <meta-data
+                android:name="android.content.SyncAdapter"
+                android:resource="@xml/syncadapter" />
+            <meta-data
+                android:name="android.provider.CONTACTS_STRUCTURE"
+                android:resource="@xml/contacts" />
+        </service>
+        <activity
+            android:name=".authenticator.AuthenticatorActivity"
+            android:label="@string/ui_activity_title"
+            android:theme="@android:style/Theme.Dialog"
+            android:excludeFromRecents="true"
+            >
+            <!--
+                No intent-filter here! This activity is only ever launched by
+                someone who explicitly knows the class name
+            -->
+        </activity>
+    </application>
+    <uses-sdk
+        android:minSdkVersion="5" />
+</manifest>
\ No newline at end of file
diff --git a/samples/SampleSyncAdapter/_index.html b/samples/SampleSyncAdapter/_index.html
new file mode 100644
index 0000000..b0fbd4a
--- /dev/null
+++ b/samples/SampleSyncAdapter/_index.html
@@ -0,0 +1,26 @@
+<p>A sample that demonstrates how an application can communicate with cloud-based services and synchronize their data with data stored locally in a content provider.
+The sample uses two related parts of the Android framework &mdash; the account manager and the synchronization manager (through a sync adapter).</p>
+
+<p> The <a href="../../../android/accounts/AccountManager">account manager</a> allows sharing of credentials across multiple applications and services. 
+Users enter the credentials for each account only once &mdash; applications with the <code>USE_CREDENTIALS</code> permission can then query the account manager
+ to obtain an auth token for the account.The authenticator (a pluggable component of account manager) requests credentials from the user, validates them
+  with an authentication server running in the cloud, and then stores them to the AccountManager. 
+This sample demonstrates how to write an authenticator for your
+service by extending the new  <code><a href="../../../android/accounts/AbstractAccountAuthenticator.html">AbstractAccountAuthenticator</a></code> abstract class.
+</p>
+
+<p>The sync adapter (essential to the synchronization service) declares the account type and ContentProvider authority to the sync manager.
+This sample demosntrates how to write your own sync adapters by extending the <code><a href="../../../android/content/AbstractThreadedSyncAdapter.html">AbstractThreadedSyncAdapter</a></code>
+abstract class and implementing the onPerformSync() method that gets called whenever the sync manager issues a sync operation for that sync adapter.
+</p>
+
+<p> The service for this sample application is running at: <br>
+http://samplesyncadapter.appspot.com/users
+</p>
+
+<p class="note">When you install this sample application, a new syncable "SampleSyncAdapter" account will be added to your phone's account manager.
+You can go to "Settings | Accounts & sync" to view the accounts that are stored in the account manager and to change their sync settings. </p>
+
+<img alt="Screenshot 1" src="../images/SampleSyncAdapter1.png" />
+<img alt="Screenshot 2" src="../images/SampleSyncAdapter2.png" />
+<img alt="Screenshot 3" src="../images/SampleSyncAdapter3.png" />
\ No newline at end of file
diff --git a/samples/SampleSyncAdapter/res/drawable/icon.png b/samples/SampleSyncAdapter/res/drawable/icon.png
new file mode 100644
index 0000000..7502484
--- /dev/null
+++ b/samples/SampleSyncAdapter/res/drawable/icon.png
Binary files differ
diff --git a/samples/SampleSyncAdapter/res/layout/login_activity.xml b/samples/SampleSyncAdapter/res/layout/login_activity.xml
new file mode 100644
index 0000000..7408ffe
--- /dev/null
+++ b/samples/SampleSyncAdapter/res/layout/login_activity.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+/**
+ * Copyright (c) 2010, 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.
+ */
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content">
+    <ScrollView
+        android:layout_width="fill_parent"
+        android:layout_height="0dip"
+        android:layout_weight="1">
+        <LinearLayout
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:orientation="vertical"
+            android:paddingTop="5dip"
+            android:paddingBottom="13dip"
+            android:paddingLeft="20dip"
+            android:paddingRight="20dip">
+            <TextView
+                android:id="@+id/message"
+                android:textAppearance="?android:attr/textAppearanceSmall"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginBottom="5dip" />
+            <TextView
+                android:textAppearance="?android:attr/textAppearanceSmall"
+                android:textStyle="bold"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/login_activity_username_label" />
+            <EditText
+                android:id="@+id/username_edit"
+                android:singleLine="true"
+                android:layout_width="fill_parent"
+                android:layout_height="wrap_content"
+                android:minWidth="250dip"
+                android:scrollHorizontally="true"
+                android:capitalize="none"
+                android:autoText="false"
+                android:inputType="textEmailAddress" />
+            <TextView
+                android:id="@+id/username_fixed"
+                android:textAppearance="?android:attr/textAppearanceSmall"
+                android:singleLine="true"
+                android:layout_marginTop="2dip"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
+            <TextView
+                android:textAppearance="?android:attr/textAppearanceSmall"
+                android:textStyle="bold"
+                android:singleLine="true"
+                android:layout_marginTop="2dip"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/login_activity_password_label" />
+            <EditText
+                android:id="@+id/password_edit"
+                android:singleLine="true"
+                android:layout_width="fill_parent"
+                android:layout_height="wrap_content"
+                android:minWidth="250dip"
+                android:scrollHorizontally="true"
+                android:capitalize="none"
+                android:autoText="false"
+                android:password="true"
+                android:inputType="textPassword" />
+            <TextView
+                android:id="@+id/message_bottom"
+                android:textAppearance="?android:attr/textAppearanceSmall"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginBottom="5dip" />
+        </LinearLayout>
+    </ScrollView>
+    <FrameLayout
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:background="#c6c3c6"
+        android:minHeight="54dip"
+        android:paddingTop="4dip"
+        android:paddingLeft="2dip"
+        android:paddingRight="2dip">
+        <Button
+            android:id="@+id/ok_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:minWidth="100dip"
+            android:text="@string/login_activity_ok_button"
+            android:onClick="handleLogin" />
+    </FrameLayout>
+</LinearLayout>
diff --git a/samples/SampleSyncAdapter/res/values/strings.xml b/samples/SampleSyncAdapter/res/values/strings.xml
new file mode 100644
index 0000000..8139d65
--- /dev/null
+++ b/samples/SampleSyncAdapter/res/values/strings.xml
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2010, 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.
+ */
+-->
+<resources
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Label for this package -->
+    <string
+        name="label">SamplesyncAdapter</string>
+
+    <!-- Permission label -->
+    <string
+        name="permlab_samplesyncadapterAuthPassword">access to passwords for Sample SyncAdapter accounts</string>
+    <!-- Permission description -->
+    <string
+        name="permdesc_samplesyncadapterAuthPassword">Allows applications direct access to the passwords for the
+        Sample SyncAdapter account(s) you have configured.</string>
+
+    <!-- Permission label -->
+    <string
+        name="permlab_samplesyncadapterAuth">view configured accounts</string>
+    <!-- Permission description -->
+    <string
+        name="permdesc_samplesyncadapterAuth">Allows applications to see the usernames (email addresses) of
+        the Sample SyncAdapter account(s) you have configured.</string>
+    <string
+        name="notification_login_error">Touch to sign into your Sample SyncAdapter account.</string>
+
+    <!-- Title string for Login activity-->
+    <string
+        name="ui_activity_title">Sign-in</string>
+    <!-- Message shown in progress dialog while app connects to the server -->
+    <string
+        name="ui_activity_authenticating">Authenticating\u2026</string>
+
+    <!-- AuthenticatorActivity -->
+    <skip />
+    <!-- Label above username EditText -->
+    <string
+        name="login_activity_username_label">Username</string>
+    <!-- Label above password EditText -->
+    <string
+        name="login_activity_password_label">Password</string>
+    <!-- Button to sign in after entering username and password -->
+    <string
+        name="login_activity_ok_button">Sign in</string>
+
+    <!-- Message shown in dialog if the username or password is invalid. -->
+    <string
+        name="login_activity_loginfail_text_both">The username or password isn\'t valid. A Sample SyncAdapter account is
+        required. Please try again. </string>
+    <!-- Message shown in dialog if the password is invalid -->
+    <string
+        name="login_activity_loginfail_text_pwonly">You entered the wrong password or your account has changed.
+        Please re-enter your password.</string>
+    <!-- Message shown in dialog to prompt the user for their password -->
+    <string
+        name="login_activity_loginfail_text_pwmissing">Type the password for this account.</string>
+    <!--
+        Message shown if the provided account doesn't support the current
+        activity.
+    -->
+    <string
+        name="login_activity_newaccount_text">Sign in to your Sample SyncAdapter account. </string>
+
+    <!-- Button that takes the user to the "sign in" screen -->
+    <string
+        name="sign_in_button_label">Sign in</string>
+    <!-- Button for going to the previous screen or step -->
+    <string
+        name="back_button_label">Back</string>
+    <!-- Button to cancel the current operation -->
+    <string
+        name="cancel_button_label">Cancel</string>
+    <string
+        name="profile_action">Sample profile</string>
+    <string
+        name="view_profile">View Profile</string>
+</resources>
\ No newline at end of file
diff --git a/samples/SampleSyncAdapter/res/xml/authenticator.xml b/samples/SampleSyncAdapter/res/xml/authenticator.xml
new file mode 100644
index 0000000..e82f672
--- /dev/null
+++ b/samples/SampleSyncAdapter/res/xml/authenticator.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2010, 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.
+ */
+-->
+
+<!-- The attributes in this XML file provide configuration information -->
+<!-- for the Account Manager. -->
+
+<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
+    android:accountType="com.example.android.samplesync"
+    android:icon="@drawable/icon"
+    android:smallIcon="@drawable/icon"
+    android:label="@string/label"
+/>
diff --git a/samples/SampleSyncAdapter/res/xml/contacts.xml b/samples/SampleSyncAdapter/res/xml/contacts.xml
new file mode 100644
index 0000000..1ff9c05
--- /dev/null
+++ b/samples/SampleSyncAdapter/res/xml/contacts.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2010, 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.
+ */
+-->
+
+<ContactsSource xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <ContactsDataKind
+        android:mimeType="vnd.android.cursor.item/vnd.samplesyncadapter.profile"
+        android:icon="@drawable/icon"
+        android:summaryColumn="data2"
+        android:detailColumn="data3"
+        android:detailSocialSummary="true" />
+
+</ContactsSource>
diff --git a/samples/SampleSyncAdapter/res/xml/syncadapter.xml b/samples/SampleSyncAdapter/res/xml/syncadapter.xml
new file mode 100644
index 0000000..1f75947
--- /dev/null
+++ b/samples/SampleSyncAdapter/res/xml/syncadapter.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2010, 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.
+ */
+-->
+
+<!-- The attributes in this XML file provide configuration information -->
+<!-- for the SyncAdapter. -->
+
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+    android:contentAuthority="com.android.contacts"
+    android:accountType="com.example.android.samplesync"
+    android:supportsUploading="false"
+/>
diff --git a/samples/SampleSyncAdapter/samplesyncadapter_server/app.yaml b/samples/SampleSyncAdapter/samplesyncadapter_server/app.yaml
new file mode 100644
index 0000000..109eff3
--- /dev/null
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/app.yaml
@@ -0,0 +1,44 @@
+application: samplesyncadapter
+version: 1
+runtime: python
+api_version: 1
+
+handlers:
+- url: /auth
+  script: main.py
+
+- url: /login
+  script: main.py
+
+- url: /fetch_friend_updates
+  script: main.py
+
+- url: /fetch_status
+  script: main.py
+
+- url: /add_user
+  script: dashboard.py
+
+- url: /edit_user
+  script: dashboard.py
+
+- url: /users
+  script: dashboard.py
+
+- url: /delete_friend
+  script: dashboard.py
+
+- url: /edit_user
+  script: dashboard.py
+
+- url: /add_credentials
+  script: dashboard.py
+
+- url: /user_credentials
+  script: dashboard.py
+
+- url: /add_friend
+  script: dashboard.py
+
+- url: /user_friends
+  script: dashboard.py
diff --git a/samples/SampleSyncAdapter/samplesyncadapter_server/dashboard.py b/samples/SampleSyncAdapter/samplesyncadapter_server/dashboard.py
new file mode 100644
index 0000000..c986f7e
--- /dev/null
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/dashboard.py
@@ -0,0 +1,273 @@
+#!/usr/bin/python2.5
+
+# Copyright (C) 2010 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.
+
+"""Defines Django forms for inserting/updating/viewing data
+   to/from SampleSyncAdapter datastore."""
+
+import cgi
+import datetime
+import os
+
+from google.appengine.ext import db
+from google.appengine.ext import webapp
+from google.appengine.ext.webapp import template
+from google.appengine.ext.db import djangoforms
+from model import datastore
+
+import wsgiref.handlers
+
+
+class UserForm(djangoforms.ModelForm):
+  """Represents django form for entering user info."""
+
+  class Meta:
+    model = datastore.User
+
+
+class UserInsertPage(webapp.RequestHandler):
+  """Inserts new users. GET presents a blank form. POST processes it."""
+
+  def get(self):
+    self.response.out.write('<html><body>'
+                            '<form method="POST" '
+                            'action="/add_user">'
+                            '<table>')
+    # This generates our shopping list form and writes it in the response
+    self.response.out.write(UserForm())
+    self.response.out.write('</table>'
+                            '<input type="submit">'
+                            '</form></body></html>')
+
+  def post(self):
+    data = UserForm(data=self.request.POST)
+    if data.is_valid():
+      # Save the data, and redirect to the view page
+      entity = data.save(commit=False)
+      entity.put()
+      self.redirect('/users')
+    else:
+      # Reprint the form
+      self.response.out.write('<html><body>'
+                              '<form method="POST" '
+                              'action="/">'
+                              '<table>')
+      self.response.out.write(data)
+      self.response.out.write('</table>'
+                              '<input type="submit">'
+                              '</form></body></html>')
+
+
+class UserEditPage(webapp.RequestHandler):
+  """Edits users. GET presents a form prefilled with user info
+     from datastore. POST processes it."""
+
+  def get(self):
+    id = int(self.request.get('user'))
+    user = datastore.User.get(db.Key.from_path('User', id))
+    self.response.out.write('<html><body>'
+                            '<form method="POST" '
+                            'action="/edit_user">'
+                            '<table>')
+    # This generates our shopping list form and writes it in the response
+    self.response.out.write(UserForm(instance=user))
+    self.response.out.write('</table>'
+                            '<input type="hidden" name="_id" value="%s">'
+                            '<input type="submit">'
+                            '</form></body></html>' % id)
+
+  def post(self):
+    id = int(self.request.get('_id'))
+    user = datastore.User.get(db.Key.from_path('User', id))
+    data = UserForm(data=self.request.POST, instance=user)
+    if data.is_valid():
+      # Save the data, and redirect to the view page
+      entity = data.save(commit=False)
+      entity.updated = datetime.datetime.utcnow()
+      entity.put()
+      self.redirect('/users')
+    else:
+      # Reprint the form
+      self.response.out.write('<html><body>'
+                              '<form method="POST" '
+                              'action="/edit_user">'
+                              '<table>')
+      self.response.out.write(data)
+      self.response.out.write('</table>'
+                              '<input type="hidden" name="_id" value="%s">'
+                              '<input type="submit">'
+                              '</form></body></html>' % id)
+
+
+class UsersListPage(webapp.RequestHandler):
+  """Lists all Users. In addition displays links for editing user info,
+     viewing user's friends and adding new users."""
+
+  def get(self):
+    users = datastore.User.all()
+    template_values = {
+        'users': users
+        }
+
+    path = os.path.join(os.path.dirname(__file__), 'templates', 'users.html')
+    self.response.out.write(template.render(path, template_values))
+
+
+class UserCredentialsForm(djangoforms.ModelForm):
+  """Represents django form for entering user's credentials."""
+
+  class Meta:
+    model = datastore.UserCredentials
+
+
+class UserCredentialsInsertPage(webapp.RequestHandler):
+  """Inserts user credentials. GET shows a blank form, POST processes it."""
+
+  def get(self):
+    self.response.out.write('<html><body>'
+                            '<form method="POST" '
+                            'action="/add_credentials">'
+                            '<table>')
+    # This generates our shopping list form and writes it in the response
+    self.response.out.write(UserCredentialsForm())
+    self.response.out.write('</table>'
+                            '<input type="submit">'
+                            '</form></body></html>')
+
+  def post(self):
+    data = UserCredentialsForm(data=self.request.POST)
+    if data.is_valid():
+      # Save the data, and redirect to the view page
+      entity = data.save(commit=False)
+      entity.put()
+      self.redirect('/users')
+    else:
+      # Reprint the form
+      self.response.out.write('<html><body>'
+                              '<form method="POST" '
+                              'action="/add_credentials">'
+                              '<table>')
+      self.response.out.write(data)
+      self.response.out.write('</table>'
+                              '<input type="submit">'
+                              '</form></body></html>')
+
+
+class UserFriendsForm(djangoforms.ModelForm):
+  """Represents django form for entering user's friends."""
+
+  class Meta:
+    model = datastore.UserFriends
+    exclude = ['deleted', 'username']
+
+
+class UserFriendsInsertPage(webapp.RequestHandler):
+  """Inserts user's new friends. GET shows a blank form, POST processes it."""
+
+  def get(self):
+    user = self.request.get('user')
+    self.response.out.write('<html><body>'
+                            '<form method="POST" '
+                            'action="/add_friend">'
+                            '<table>')
+    # This generates our shopping list form and writes it in the response
+    self.response.out.write(UserFriendsForm())
+    self.response.out.write('</table>'
+                            '<input type = hidden name = "user" value = "%s">'
+                            '<input type="submit">'
+                            '</form></body></html>' % user)
+
+  def post(self):
+    data = UserFriendsForm(data=self.request.POST)
+    if data.is_valid():
+      user = self.request.get('user')
+      # Save the data, and redirect to the view page
+      entity = data.save(commit=False)
+      entity.username = user
+      query = datastore.UserFriends.all()
+      query.filter('username = ', user)
+      query.filter('friend_handle = ', entity.friend_handle)
+      result = query.get()
+      if result:
+	result.deleted = False
+	result.updated = datetime.datetime.utcnow()
+	result.put()
+      else:
+        entity.deleted = False
+        entity.put()
+      self.redirect('/user_friends?user=' + user)
+    else:
+      # Reprint the form
+      self.response.out.write('<html><body>'
+                              '<form method="POST" '
+                              'action="/add_friend">'
+                              '<table>')
+      self.response.out.write(data)
+      self.response.out.write('</table>'
+                              '<input type="submit">'
+                              '</form></body></html>')
+
+
+class UserFriendsListPage(webapp.RequestHandler):
+  """Lists all friends for a user. In addition displays links for removing
+     friends and adding new friends."""
+
+  def get(self):
+    user = self.request.get('user')
+    query = datastore.UserFriends.all()
+    query.filter('deleted = ', False)
+    query.filter('username = ', user)
+    friends = query.fetch(50)
+    template_values = {
+        'friends': friends,
+        'user': user
+        }
+    path = os.path.join(os.path.dirname(__file__),
+                        'templates', 'view_friends.html')
+    self.response.out.write(template.render(path, template_values))
+
+
+class DeleteFriendPage(webapp.RequestHandler):
+  """Processes delete friend request."""
+
+  def get(self):
+    user = self.request.get('user')
+    friend = self.request.get('friend')
+    query = datastore.UserFriends.all()
+    query.filter('username =', user)
+    query.filter('friend_handle =', friend)
+    result = query.get()
+    result.deleted = True
+    result.updated = datetime.datetime.utcnow()
+    result.put()
+
+    self.redirect('/user_friends?user=' + user)
+
+
+def main():
+  application = webapp.WSGIApplication(
+      [('/add_user', UserInsertPage),
+       ('/users', UsersListPage),
+       ('/add_credentials', UserCredentialsInsertPage),
+       ('/add_friend', UserFriendsInsertPage),
+       ('/user_friends', UserFriendsListPage),
+       ('/delete_friend', DeleteFriendPage),
+       ('/edit_user', UserEditPage)
+      ],
+      debug=True)
+  wsgiref.handlers.CGIHandler().run(application)
+
+if __name__ == '__main__':
+  main()
\ No newline at end of file
diff --git a/samples/SampleSyncAdapter/samplesyncadapter_server/index.yaml b/samples/SampleSyncAdapter/samplesyncadapter_server/index.yaml
new file mode 100644
index 0000000..83ceea0
--- /dev/null
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/index.yaml
@@ -0,0 +1,14 @@
+indexes:
+
+# This index.yaml is automatically updated whenever the dev_appserver
+# detects that a new type of query is run.  If you want to manage the
+# index.yaml file manually, remove the above marker line (the line
+# saying "# AUTOGENERATED").  If you want to manage some indexes
+# manually, move them above the marker line.  The index.yaml file is
+# automatically uploaded to the admin console when you next deploy
+# your application using appcfg.py.
+
+- kind: UserFriends
+  properties:
+  - name: username
+  - name: updated
\ No newline at end of file
diff --git a/samples/SampleSyncAdapter/samplesyncadapter_server/main.py b/samples/SampleSyncAdapter/samplesyncadapter_server/main.py
new file mode 100644
index 0000000..2d7c5c7
--- /dev/null
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/main.py
@@ -0,0 +1,173 @@
+#!/usr/bin/python2.5
+
+# Copyright (C) 2010 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.
+
+"""Handlers for Sample SyncAdapter services.
+
+Contains several RequestHandler subclasses used to handle post operations.
+This script is designed to be run directly as a WSGI application.
+
+  Authenticate: Handles user requests for authentication.
+  FetchFriends: Handles user requests for friend list.
+  FriendData: Stores information about user's friends.
+"""
+
+import cgi
+from datetime import datetime
+from django.utils import simplejson
+from google.appengine.api import users
+from google.appengine.ext import db
+from google.appengine.ext import webapp
+from model import datastore
+import wsgiref.handlers
+
+
+class Authenticate(webapp.RequestHandler):
+  """Handles requests for login and authentication.
+
+  UpdateHandler only accepts post events. It expects each
+  request to include username and password fields. It returns authtoken
+  after successful authentication and "invalid credentials" error otherwise.
+  """
+
+  def post(self):
+    self.username = self.request.get('username')
+    self.password = self.request.get('password')
+    password = datastore.UserCredentials.get(self.username)
+    if password == self.password:
+      self.response.set_status(200, 'OK')
+      # return the password as AuthToken
+      self.response.out.write(password)
+    else:
+      self.response.set_status(401, 'Invalid Credentials')
+
+
+class FetchFriends(webapp.RequestHandler):
+  """Handles requests for fetching user's friendlist.
+
+  UpdateHandler only accepts post events. It expects each
+  request to include username and authtoken. If the authtoken is valid
+  it returns user's friend info in JSON format.It uses helper
+  class FriendData to fetch user's friendlist.
+  """
+
+  def post(self):
+    self.username = self.request.get('username')
+    self.password = self.request.get('password')
+    self.timestamp = None
+    timestamp = self.request.get('timestamp')
+    if timestamp:
+      self.timestamp = datetime.strptime(timestamp, '%Y/%m/%d %H:%M')
+    password = datastore.UserCredentials.get(self.username)
+    if password == self.password:
+      self.friend_list = []
+      friends = datastore.UserFriends.get_friends(self.username)
+      if friends:
+        for friend in friends:
+          friend_handle = getattr(friend, 'friend_handle')
+
+          if self.timestamp is None or getattr(friend, 'updated') > self.timestamp:
+            if (getattr(friend, 'deleted')) == True:
+              friend = {}
+              friend['u'] = friend_handle
+              friend['d'] = 'true'
+              friend['i'] = str(datastore.User.get_user_id(friend_handle))
+              self.friend_list.append(friend)
+            else:
+              FriendsData(self.friend_list, friend_handle)
+          else:
+            if datastore.User.get_user_last_updated(friend_handle) > self.timestamp:
+              FriendsData(self.friend_list, friend_handle)
+      self.response.set_status(200)
+      self.response.out.write(toJSON(self.friend_list))
+    else:
+      self.response.set_status(401, 'Invalid Credentials')
+
+class FetchStatus(webapp.RequestHandler):
+  """Handles requests fetching friend statuses.
+
+  UpdateHandler only accepts post events. It expects each
+  request to include username and authtoken. If the authtoken is valid
+  it returns status info in JSON format.
+  """
+
+  def post(self):
+    self.username = self.request.get('username')
+    self.password = self.request.get('password')
+    password = datastore.UserCredentials.get(self.username)
+    if password == self.password:
+      self.status_list = []
+      friends = datastore.UserFriends.get_friends(self.username)
+      if friends:
+        for friend in friends:
+          friend_handle = getattr(friend, 'friend_handle')
+          status_text = datastore.User.get_user_status(friend_handle)
+	  user_id = datastore.User.get_user_id(friend_handle)
+          status = {}
+          status['i'] = str(user_id)
+          status['s'] = status_text
+          self.status_list.append(status)
+      self.response.set_status(200)
+      self.response.out.write(toJSON(self.status_list))
+    else:
+      self.response.set_status(401, 'Invalid Credentials')
+
+  def toJSON(self):
+    """Dumps the data represented by the object to JSON for wire transfer."""
+    return simplejson.dumps(self.friend_list)
+
+
+def toJSON(object):
+  """Dumps the data represented by the object to JSON for wire transfer."""
+  return simplejson.dumps(object)
+
+class FriendsData(object):
+  """Holds data for user's friends.
+
+  This class knows how to serialize itself to JSON.
+  """
+  __FIELD_MAP = {
+      'handle': 'u',
+      'firstname': 'f',
+      'lastname': 'l',
+      'status': 's',
+      'phone_home': 'h',
+      'phone_office': 'o',
+      'phone_mobile': 'm',
+      'email': 'e',
+  }
+
+  def __init__(self, friend_list, username):
+    obj = datastore.User.get_user_info(username)
+    friend = {}
+    for obj_name, json_name in self.__FIELD_MAP.items():
+      if hasattr(obj, obj_name):
+        friend[json_name] = str(getattr(obj, obj_name))
+        friend['i'] = str(obj.key().id())
+    friend_list.append(friend)
+
+
+def main():
+  application = webapp.WSGIApplication(
+      [('/auth', Authenticate),
+       ('/login', Authenticate),
+       ('/fetch_friend_updates', FetchFriends),
+       ('/fetch_status', FetchStatus),
+      ],
+      debug=True)
+  wsgiref.handlers.CGIHandler().run(application)
+
+if __name__ == "__main__":
+  main()
\ No newline at end of file
diff --git a/samples/SampleSyncAdapter/samplesyncadapter_server/model/__init__.py b/samples/SampleSyncAdapter/samplesyncadapter_server/model/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/model/__init__.py
diff --git a/samples/SampleSyncAdapter/samplesyncadapter_server/model/datastore.py b/samples/SampleSyncAdapter/samplesyncadapter_server/model/datastore.py
new file mode 100644
index 0000000..71bd18a
--- /dev/null
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/model/datastore.py
@@ -0,0 +1,93 @@
+#!/usr/bin/python2.5
+
+# Copyright (C) 2010 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.
+
+"""Represents user's contact information, friends and credentials."""
+
+from google.appengine.ext import db
+
+
+class User(db.Model):
+  """Data model class to hold user objects."""
+
+  handle = db.StringProperty(required=True)
+  firstname = db.TextProperty()
+  lastname = db.TextProperty()
+  status = db.TextProperty()
+  phone_home = db.PhoneNumberProperty()
+  phone_office = db.PhoneNumberProperty()
+  phone_mobile = db.PhoneNumberProperty()
+  email = db.EmailProperty()
+  deleted = db.BooleanProperty()
+  updated = db.DateTimeProperty(auto_now_add=True)
+
+  @classmethod
+  def get_user_info(cls, username):
+    if username not in (None, ''):
+      query = cls.gql('WHERE handle = :1', username)
+      return query.get()
+    return None
+
+  @classmethod
+  def get_user_last_updated(cls, username):
+    if username not in (None, ''):
+      query = cls.gql('WHERE handle = :1', username)
+      return query.get().updated
+    return None
+
+  @classmethod
+  def get_user_id(cls, username):
+    if username not in (None, ''):
+      query = cls.gql('WHERE handle = :1', username)
+      return query.get().key().id()
+    return None
+
+  @classmethod
+  def get_user_status(cls, username):
+    if username not in (None, ''):
+      query = cls.gql('WHERE handle = :1', username)
+      return query.get().status
+    return None
+
+
+class UserCredentials(db.Model):
+  """Data model class to hold credentials for a Voiper user."""
+
+  username = db.StringProperty(required=True)
+  password = db.StringProperty()
+
+  @classmethod
+  def get(cls, username):
+    if username not in (None, ''):
+      query = cls.gql('WHERE username = :1', username)
+      return query.get().password
+    return None
+
+
+class UserFriends(db.Model):
+  """Data model class to hold user's friendlist info."""
+
+  username = db.StringProperty()
+  friend_handle = db.StringProperty(required=True)
+  updated = db.DateTimeProperty(auto_now_add=True)
+  deleted = db.BooleanProperty()
+
+  @classmethod
+  def get_friends(cls, username):
+    if username not in (None, ''):
+      query = cls.gql('WHERE username = :1', username)
+      friends = query.fetch(50)
+      return friends
+    return None
\ No newline at end of file
diff --git a/samples/SampleSyncAdapter/samplesyncadapter_server/templates/users.html b/samples/SampleSyncAdapter/samplesyncadapter_server/templates/users.html
new file mode 100644
index 0000000..044c352
--- /dev/null
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/templates/users.html
@@ -0,0 +1,19 @@
+<html>
+<body>
+<h1> Sample Sync Adapter </h1>
+
+<p>
+<h3> List of Users </h3>
+<table>
+
+{% for user in users %}
+  <tr><td>
+  <a
+  href="/edit_user?user={{ user.key.id}}">{{ user.firstname }}&nbsp; {{ user.lastname }} </a>
+  </td><td>&nbsp;&nbsp;<a href="/user_friends?user={{ user.handle }}">Friends</a> </td>
+  </tr>
+{% endfor %}
+</table>
+</p>
+
+<a href = "/add_user"> Insert More </a>
\ No newline at end of file
diff --git a/samples/SampleSyncAdapter/samplesyncadapter_server/templates/view_friends.html b/samples/SampleSyncAdapter/samplesyncadapter_server/templates/view_friends.html
new file mode 100644
index 0000000..d4ef892
--- /dev/null
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/templates/view_friends.html
@@ -0,0 +1,17 @@
+<html>
+<body>
+<h1> Sample Sync Adapter </h1>
+
+<p>
+
+{{user}}'s friends 
+<table>
+{% for friend in friends %}
+  <tr><td>
+  {{ friend.friend_handle }} </td><td> <a href="/delete_friend?user={{ user }}&friend={{friend.friend_handle}}">Remove</a>  
+  </td></tr>
+{% endfor %}
+</table>
+</p>
+
+<a href = "/add_friend?user={{user}}"> Add More </a>
\ No newline at end of file
diff --git a/samples/SampleSyncAdapter/src/com/example/android/samplesync/Constants.java b/samples/SampleSyncAdapter/src/com/example/android/samplesync/Constants.java
new file mode 100644
index 0000000..f233a5d
--- /dev/null
+++ b/samples/SampleSyncAdapter/src/com/example/android/samplesync/Constants.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2010 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.example.android.samplesync;
+
+public class Constants {
+
+    /**
+     * Account type string.
+     */
+    public static final String ACCOUNT_TYPE = "com.example.android.samplesync";
+
+    /**
+     * Authtoken type string.
+     */
+    public static final String AUTHTOKEN_TYPE =
+        "com.example.android.samplesync";
+
+}
diff --git a/samples/SampleSyncAdapter/src/com/example/android/samplesync/authenticator/AuthenticationService.java b/samples/SampleSyncAdapter/src/com/example/android/samplesync/authenticator/AuthenticationService.java
new file mode 100644
index 0000000..b8a903d
--- /dev/null
+++ b/samples/SampleSyncAdapter/src/com/example/android/samplesync/authenticator/AuthenticationService.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2010 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.example.android.samplesync.authenticator;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.util.Log;
+
+/**
+ * Service to handle Account authentication. It instantiates the authenticator
+ * and returns its IBinder.
+ */
+public class AuthenticationService extends Service {
+    private static final String TAG = "AuthenticationService";
+    private Authenticator mAuthenticator;
+
+    @Override
+    public void onCreate() {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "SampleSyncAdapter Authentication Service started.");
+        }
+        mAuthenticator = new Authenticator(this);
+    }
+
+    @Override
+    public void onDestroy() {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG, "SampleSyncAdapter Authentication Service stopped.");
+        }
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        if (Log.isLoggable(TAG, Log.VERBOSE)) {
+            Log.v(TAG,
+                "getBinder()...  returning the AccountAuthenticator binder for intent "
+                    + intent);
+        }
+        return mAuthenticator.getIBinder();
+    }
+}
diff --git a/samples/SampleSyncAdapter/src/com/example/android/samplesync/authenticator/Authenticator.java b/samples/SampleSyncAdapter/src/com/example/android/samplesync/authenticator/Authenticator.java
new file mode 100644
index 0000000..29613a9
--- /dev/null
+++ b/samples/SampleSyncAdapter/src/com/example/android/samplesync/authenticator/Authenticator.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2010 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.example.android.samplesync.authenticator;
+
+import android.accounts.AbstractAccountAuthenticator;
+import android.accounts.Account;
+import android.accounts.AccountAuthenticatorResponse;
+import android.accounts.AccountManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import com.example.android.samplesync.Constants;
+import com.example.android.samplesync.R;
+import com.example.android.samplesync.client.NetworkUtilities;
+
+/**
+ * This class is an implementation of AbstractAccountAuthenticator for
+ * authenticating accounts in the com.example.android.samplesync domain.
+ */
+class Authenticator extends AbstractAccountAuthenticator {
+    // Authentication Service context
+    private final Context mContext;
+
+    public Authenticator(Context context) {
+        super(context);
+        mContext = context;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Bundle addAccount(AccountAuthenticatorResponse response,
+        String accountType, String authTokenType, String[] requiredFeatures,
+        Bundle options) {
+        final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
+        intent.putExtra(AuthenticatorActivity.PARAM_AUTHTOKEN_TYPE,
+            authTokenType);
+        intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE,
+            response);
+        final Bundle bundle = new Bundle();
+        bundle.putParcelable(AccountManager.KEY_INTENT, intent);
+        return bundle;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Bundle confirmCredentials(AccountAuthenticatorResponse response,
+        Account account, Bundle options) {
+        if (options != null && options.containsKey(AccountManager.KEY_PASSWORD)) {
+            final String password =
+                options.getString(AccountManager.KEY_PASSWORD);
+            final boolean verified =
+                onlineConfirmPassword(account.name, password);
+            final Bundle result = new Bundle();
+            result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, verified);
+            return result;
+        }
+        // Launch AuthenticatorActivity to confirm credentials
+        final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
+        intent.putExtra(AuthenticatorActivity.PARAM_USERNAME, account.name);
+        intent.putExtra(AuthenticatorActivity.PARAM_CONFIRMCREDENTIALS, true);
+        intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE,
+            response);
+        final Bundle bundle = new Bundle();
+        bundle.putParcelable(AccountManager.KEY_INTENT, intent);
+        return bundle;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Bundle editProperties(AccountAuthenticatorResponse response,
+        String accountType) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Bundle getAuthToken(AccountAuthenticatorResponse response,
+        Account account, String authTokenType, Bundle loginOptions) {
+        if (!authTokenType.equals(Constants.AUTHTOKEN_TYPE)) {
+            final Bundle result = new Bundle();
+            result.putString(AccountManager.KEY_ERROR_MESSAGE,
+                "invalid authTokenType");
+            return result;
+        }
+        final AccountManager am = AccountManager.get(mContext);
+        final String password = am.getPassword(account);
+        if (password != null) {
+            final boolean verified =
+                onlineConfirmPassword(account.name, password);
+            if (verified) {
+                final Bundle result = new Bundle();
+                result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
+                result.putString(AccountManager.KEY_ACCOUNT_TYPE,
+                    Constants.ACCOUNT_TYPE);
+                result.putString(AccountManager.KEY_AUTHTOKEN, password);
+                return result;
+            }
+        }
+        // the password was missing or incorrect, return an Intent to an
+        // Activity that will prompt the user for the password.
+        final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
+        intent.putExtra(AuthenticatorActivity.PARAM_USERNAME, account.name);
+        intent.putExtra(AuthenticatorActivity.PARAM_AUTHTOKEN_TYPE,
+            authTokenType);
+        intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE,
+            response);
+        final Bundle bundle = new Bundle();
+        bundle.putParcelable(AccountManager.KEY_INTENT, intent);
+        return bundle;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String getAuthTokenLabel(String authTokenType) {
+        if (authTokenType.equals(Constants.AUTHTOKEN_TYPE)) {
+            return mContext.getString(R.string.label);
+        }
+        return null;
+
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Bundle hasFeatures(AccountAuthenticatorResponse response,
+        Account account, String[] features) {
+        final Bundle result = new Bundle();
+        result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
+        return result;
+    }
+
+    /**
+     * Validates user's password on the server
+     */
+    private boolean onlineConfirmPassword(String username, String password) {
+        return NetworkUtilities.authenticate(username, password,
+            null/* Handler */, null/* Context */);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Bundle updateCredentials(AccountAuthenticatorResponse response,
+        Account account, String authTokenType, Bundle loginOptions) {
+        final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
+        intent.putExtra(AuthenticatorActivity.PARAM_USERNAME, account.name);
+        intent.putExtra(AuthenticatorActivity.PARAM_AUTHTOKEN_TYPE,
+            authTokenType);
+        intent.putExtra(AuthenticatorActivity.PARAM_CONFIRMCREDENTIALS, false);
+        final Bundle bundle = new Bundle();
+        bundle.putParcelable(AccountManager.KEY_INTENT, intent);
+        return bundle;
+    }
+
+}
diff --git a/samples/SampleSyncAdapter/src/com/example/android/samplesync/authenticator/AuthenticatorActivity.java b/samples/SampleSyncAdapter/src/com/example/android/samplesync/authenticator/AuthenticatorActivity.java
new file mode 100644
index 0000000..779e894
--- /dev/null
+++ b/samples/SampleSyncAdapter/src/com/example/android/samplesync/authenticator/AuthenticatorActivity.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2010 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.example.android.samplesync.authenticator;
+
+import android.accounts.Account;
+import android.accounts.AccountAuthenticatorActivity;
+import android.accounts.AccountManager;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.ContentResolver;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.provider.ContactsContract;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.view.Window;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import com.example.android.samplesync.Constants;
+import com.example.android.samplesync.R;
+import com.example.android.samplesync.client.NetworkUtilities;
+
+/**
+ * Activity which displays login screen to the user.
+ */
+public class AuthenticatorActivity extends AccountAuthenticatorActivity {
+    public static final String PARAM_CONFIRMCREDENTIALS = "confirmCredentials";
+    public static final String PARAM_PASSWORD = "password";
+    public static final String PARAM_USERNAME = "username";
+    public static final String PARAM_AUTHTOKEN_TYPE = "authtokenType";
+
+    private static final String TAG = "AuthenticatorActivity";
+
+    private AccountManager mAccountManager;
+    private Thread mAuthThread;
+    private String mAuthtoken;
+    private String mAuthtokenType;
+
+    /**
+     * If set we are just checking that the user knows their credentials; this
+     * doesn't cause the user's password to be changed on the device.
+     */
+    private Boolean mConfirmCredentials = false;
+
+    /** for posting authentication attempts back to UI thread */
+    private final Handler mHandler = new Handler();
+    private TextView mMessage;
+    private String mPassword;
+    private EditText mPasswordEdit;
+
+    /** Was the original caller asking for an entirely new account? */
+    protected boolean mRequestNewAccount = false;
+
+    private String mUsername;
+    private EditText mUsernameEdit;
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onCreate(Bundle icicle) {
+        Log.i(TAG, "onCreate(" + icicle + ")");
+        super.onCreate(icicle);
+        mAccountManager = AccountManager.get(this);
+        Log.i(TAG, "loading data from Intent");
+        final Intent intent = getIntent();
+        mUsername = intent.getStringExtra(PARAM_USERNAME);
+        mAuthtokenType = intent.getStringExtra(PARAM_AUTHTOKEN_TYPE);
+        mRequestNewAccount = mUsername == null;
+        mConfirmCredentials =
+            intent.getBooleanExtra(PARAM_CONFIRMCREDENTIALS, false);
+
+        Log.i(TAG, "    request new: " + mRequestNewAccount);
+        requestWindowFeature(Window.FEATURE_LEFT_ICON);
+        setContentView(R.layout.login_activity);
+        getWindow().setFeatureDrawableResource(Window.FEATURE_LEFT_ICON,
+            android.R.drawable.ic_dialog_alert);
+
+        mMessage = (TextView) findViewById(R.id.message);
+        mUsernameEdit = (EditText) findViewById(R.id.username_edit);
+        mPasswordEdit = (EditText) findViewById(R.id.password_edit);
+
+        mUsernameEdit.setText(mUsername);
+        mMessage.setText(getMessage());
+    }
+
+    /*
+     * {@inheritDoc}
+     */
+    @Override
+    protected Dialog onCreateDialog(int id) {
+        final ProgressDialog dialog = new ProgressDialog(this);
+        dialog.setMessage(getText(R.string.ui_activity_authenticating));
+        dialog.setIndeterminate(true);
+        dialog.setCancelable(true);
+        dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
+            public void onCancel(DialogInterface dialog) {
+                Log.i(TAG, "dialog cancel has been invoked");
+                if (mAuthThread != null) {
+                    mAuthThread.interrupt();
+                    finish();
+                }
+            }
+        });
+        return dialog;
+    }
+
+    /**
+     * Handles onClick event on the Submit button. Sends username/password to
+     * the server for authentication.
+     * 
+     * @param view The Submit button for which this method is invoked
+     */
+    public void handleLogin(View view) {
+        if (mRequestNewAccount) {
+            mUsername = mUsernameEdit.getText().toString();
+        }
+        mPassword = mPasswordEdit.getText().toString();
+        if (TextUtils.isEmpty(mUsername) || TextUtils.isEmpty(mPassword)) {
+            mMessage.setText(getMessage());
+        } else {
+            showProgress();
+            // Start authenticating...
+            mAuthThread =
+                NetworkUtilities.attemptAuth(mUsername, mPassword, mHandler,
+                    AuthenticatorActivity.this);
+        }
+    }
+
+    /**
+     * Called when response is received from the server for confirm credentials
+     * request. See onAuthenticationResult(). Sets the
+     * AccountAuthenticatorResult which is sent back to the caller.
+     * 
+     * @param the confirmCredentials result.
+     */
+    protected void finishConfirmCredentials(boolean result) {
+        Log.i(TAG, "finishConfirmCredentials()");
+        final Account account = new Account(mUsername, Constants.ACCOUNT_TYPE);
+        mAccountManager.setPassword(account, mPassword);
+        final Intent intent = new Intent();
+        intent.putExtra(AccountManager.KEY_BOOLEAN_RESULT, result);
+        setAccountAuthenticatorResult(intent.getExtras());
+        setResult(RESULT_OK, intent);
+        finish();
+    }
+
+    /**
+     * 
+     * Called when response is received from the server for authentication
+     * request. See onAuthenticationResult(). Sets the
+     * AccountAuthenticatorResult which is sent back to the caller. Also sets
+     * the authToken in AccountManager for this account.
+     * 
+     * @param the confirmCredentials result.
+     */
+
+    protected void finishLogin() {
+        Log.i(TAG, "finishLogin()");
+        final Account account = new Account(mUsername, Constants.ACCOUNT_TYPE);
+
+        if (mRequestNewAccount) {
+            mAccountManager.addAccountExplicitly(account, mPassword, null);
+            // Set contacts sync for this account.
+            ContentResolver.setSyncAutomatically(account,
+                ContactsContract.AUTHORITY, true);
+        } else {
+            mAccountManager.setPassword(account, mPassword);
+        }
+        final Intent intent = new Intent();
+        mAuthtoken = mPassword;
+        intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, mUsername);
+        intent
+            .putExtra(AccountManager.KEY_ACCOUNT_TYPE, Constants.ACCOUNT_TYPE);
+        if (mAuthtokenType != null
+            && mAuthtokenType.equals(Constants.AUTHTOKEN_TYPE)) {
+            intent.putExtra(AccountManager.KEY_AUTHTOKEN, mAuthtoken);
+        }
+        setAccountAuthenticatorResult(intent.getExtras());
+        setResult(RESULT_OK, intent);
+        finish();
+    }
+
+    /**
+     * Hides the progress UI for a lengthy operation.
+     */
+    protected void hideProgress() {
+        dismissDialog(0);
+    }
+
+    /**
+     * Called when the authentication process completes (see attemptLogin()).
+     */
+    public void onAuthenticationResult(boolean result) {
+        Log.i(TAG, "onAuthenticationResult(" + result + ")");
+        // Hide the progress dialog
+        hideProgress();
+        if (result) {
+            if (!mConfirmCredentials) {
+                finishLogin();
+            } else {
+                finishConfirmCredentials(true);
+            }
+        } else {
+            Log.e(TAG, "onAuthenticationResult: failed to authenticate");
+            if (mRequestNewAccount) {
+                // "Please enter a valid username/password.
+                mMessage
+                    .setText(getText(R.string.login_activity_loginfail_text_both));
+            } else {
+                // "Please enter a valid password." (Used when the
+                // account is already in the database but the password
+                // doesn't work.)
+                mMessage
+                    .setText(getText(R.string.login_activity_loginfail_text_pwonly));
+            }
+        }
+    }
+
+    /**
+     * Returns the message to be displayed at the top of the login dialog box.
+     */
+    private CharSequence getMessage() {
+        getString(R.string.label);
+        if (TextUtils.isEmpty(mUsername)) {
+            // If no username, then we ask the user to log in using an
+            // appropriate service.
+            final CharSequence msg =
+                getText(R.string.login_activity_newaccount_text);
+            return msg;
+        }
+        if (TextUtils.isEmpty(mPassword)) {
+            // We have an account but no password
+            return getText(R.string.login_activity_loginfail_text_pwmissing);
+        }
+        return null;
+    }
+
+    /**
+     * Shows the progress UI for a lengthy operation.
+     */
+    protected void showProgress() {
+        showDialog(0);
+    }
+}
diff --git a/samples/SampleSyncAdapter/src/com/example/android/samplesync/client/NetworkUtilities.java b/samples/SampleSyncAdapter/src/com/example/android/samplesync/client/NetworkUtilities.java
new file mode 100644
index 0000000..9d2b666
--- /dev/null
+++ b/samples/SampleSyncAdapter/src/com/example/android/samplesync/client/NetworkUtilities.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2010 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.example.android.samplesync.client;
+
+import android.accounts.Account;
+import android.content.Context;
+import android.os.Handler;
+import android.util.Log;
+
+import com.example.android.samplesync.authenticator.AuthenticatorActivity;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.NameValuePair;
+import org.apache.http.ParseException;
+import org.apache.http.auth.AuthenticationException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.conn.params.ConnManagerParams;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.util.EntityUtils;
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.TimeZone;
+
+/**
+ * Provides utility methods for communicating with the server.
+ */
+public class NetworkUtilities {
+    private static final String TAG = "NetworkUtilities";
+    public static final String PARAM_USERNAME = "username";
+    public static final String PARAM_PASSWORD = "password";
+    public static final String PARAM_UPDATED = "timestamp";
+    public static final String USER_AGENT = "AuthenticationService/1.0";
+    public static final int REGISTRATION_TIMEOUT = 30 * 1000; // ms
+    public static final String BASE_URL =
+        "https://samplesyncadapter.appspot.com";
+    public static final String AUTH_URI = BASE_URL + "/auth";
+    public static final String FETCH_FRIEND_UPDATES_URI =
+        BASE_URL + "/fetch_friend_updates";
+    public static final String FETCH_STATUS_URI = BASE_URL + "/fetch_status";
+    private static HttpClient mHttpClient;
+
+    /**
+     * Configures the httpClient to connect to the URL provided.
+     */
+    public static void maybeCreateHttpClient() {
+        if (mHttpClient == null) {
+            mHttpClient = new DefaultHttpClient();
+            final HttpParams params = mHttpClient.getParams();
+            HttpConnectionParams.setConnectionTimeout(params,
+                REGISTRATION_TIMEOUT);
+            HttpConnectionParams.setSoTimeout(params, REGISTRATION_TIMEOUT);
+            ConnManagerParams.setTimeout(params, REGISTRATION_TIMEOUT);
+        }
+    }
+
+    /**
+     * Executes the network requests on a separate thread.
+     * 
+     * @param runnable The runnable instance containing network mOperations to
+     *        be executed.
+     */
+    public static Thread performOnBackgroundThread(final Runnable runnable) {
+        final Thread t = new Thread() {
+            @Override
+            public void run() {
+                try {
+                    runnable.run();
+                } finally {
+
+                }
+            }
+        };
+        t.start();
+        return t;
+    }
+
+    /**
+     * Connects to the Voiper server, authenticates the provided username and
+     * password.
+     * 
+     * @param username The user's username
+     * @param password The user's password
+     * @param handler The hander instance from the calling UI thread.
+     * @param context The context of the calling Activity.
+     * @return boolean The boolean result indicating whether the user was
+     *         successfully authenticated.
+     */
+    public static boolean authenticate(String username, String password,
+        Handler handler, final Context context) {
+        final HttpResponse resp;
+
+        final ArrayList<NameValuePair> params = new ArrayList<NameValuePair>();
+        params.add(new BasicNameValuePair(PARAM_USERNAME, username));
+        params.add(new BasicNameValuePair(PARAM_PASSWORD, password));
+        HttpEntity entity = null;
+        try {
+            entity = new UrlEncodedFormEntity(params);
+        } catch (final UnsupportedEncodingException e) {
+            // this should never happen.
+            throw new AssertionError(e);
+        }
+        final HttpPost post = new HttpPost(AUTH_URI);
+        post.addHeader(entity.getContentType());
+        post.setEntity(entity);
+        maybeCreateHttpClient();
+
+        try {
+            resp = mHttpClient.execute(post);
+            if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
+                if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                    Log.v(TAG, "Successful authentication");
+                }
+                sendResult(true, handler, context);
+                return true;
+            } else {
+                if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                    Log.v(TAG, "Error authenticating" + resp.getStatusLine());
+                }
+                sendResult(false, handler, context);
+                return false;
+            }
+        } catch (final IOException e) {
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "IOException when getting authtoken", e);
+            }
+            sendResult(false, handler, context);
+            return false;
+        } finally {
+            if (Log.isLoggable(TAG, Log.VERBOSE)) {
+                Log.v(TAG, "getAuthtoken completing");
+            }
+        }
+    }
+
+    /**
+     * Sends the authentication response from server back to the caller main UI
+     * thread through its handler.
+     * 
+     * @param result The boolean holding authentication result
+     * @param handler The main UI thread's handler instance.
+     * @param context The caller Activity's context.
+     */
+    private static void sendResult(final Boolean result, final Handler handler,
+        final Context context) {
+        if (handler == null || context == null) {
+            return;
+        }
+        handler.post(new Runnable() {
+            public void run() {
+                ((AuthenticatorActivity) context).onAuthenticationResult(result);
+            }
+        });
+    }
+
+    /**
+     * Attempts to authenticate the user credentials on the server.
+     * 
+     * @param username The user's username
+     * @param password The user's password to be authenticated
+     * @param handler The main UI thread's handler instance.
+     * @param context The caller Activity's context
+     * @return Thread The thread on which the network mOperations are executed.
+     */
+    public static Thread attemptAuth(final String username,
+        final String password, final Handler handler, final Context context) {
+        final Runnable runnable = new Runnable() {
+            public void run() {
+                authenticate(username, password, handler, context);
+            }
+        };
+        // run on background thread.
+        return NetworkUtilities.performOnBackgroundThread(runnable);
+    }
+
+    /**
+     * Fetches the list of friend data updates from the server
+     * 
+     * @param account The account being synced.
+     * @param authtoken The authtoken stored in AccountManager for this account
+     * @param lastUpdated The last time that sync was performed
+     * @return list The list of updates received from the server.
+     */
+    public static List<User> fetchFriendUpdates(Account account,
+        String authtoken, Date lastUpdated) throws JSONException,
+        ParseException, IOException, AuthenticationException {
+        final ArrayList<User> friendList = new ArrayList<User>();
+        final ArrayList<NameValuePair> params = new ArrayList<NameValuePair>();
+        params.add(new BasicNameValuePair(PARAM_USERNAME, account.name));
+        params.add(new BasicNameValuePair(PARAM_PASSWORD, authtoken));
+        if (lastUpdated != null) {
+            final SimpleDateFormat formatter =
+                new SimpleDateFormat("yyyy/MM/dd HH:mm");
+            formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
+            params.add(new BasicNameValuePair(PARAM_UPDATED, formatter
+                .format(lastUpdated)));
+        }
+        Log.i(TAG, params.toString());
+
+        HttpEntity entity = null;
+        entity = new UrlEncodedFormEntity(params);
+        final HttpPost post = new HttpPost(FETCH_FRIEND_UPDATES_URI);
+        post.addHeader(entity.getContentType());
+        post.setEntity(entity);
+        maybeCreateHttpClient();
+
+        final HttpResponse resp = mHttpClient.execute(post);
+        final String response = EntityUtils.toString(resp.getEntity());
+
+        if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
+            // Succesfully connected to the samplesyncadapter server and
+            // authenticated.
+            // Extract friends data in json format.
+            final JSONArray friends = new JSONArray(response);
+            Log.d(TAG, response);
+            for (int i = 0; i < friends.length(); i++) {
+                friendList.add(User.valueOf(friends.getJSONObject(i)));
+            }
+        } else {
+            if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
+                Log.e(TAG,
+                    "Authentication exception in fetching remote contacts");
+                throw new AuthenticationException();
+            } else {
+                Log.e(TAG, "Server error in fetching remote contacts: "
+                    + resp.getStatusLine());
+                throw new IOException();
+            }
+        }
+        return friendList;
+    }
+
+    /**
+     * Fetches status messages for the user's friends from the server
+     * 
+     * @param account The account being synced.
+     * @param authtoken The authtoken stored in the AccountManager for the
+     *        account
+     * @return list The list of status messages received from the server.
+     */
+    public static List<User.Status> fetchFriendStatuses(Account account,
+        String authtoken) throws JSONException, ParseException, IOException,
+        AuthenticationException {
+        final ArrayList<User.Status> statusList = new ArrayList<User.Status>();
+        final ArrayList<NameValuePair> params = new ArrayList<NameValuePair>();
+        params.add(new BasicNameValuePair(PARAM_USERNAME, account.name));
+        params.add(new BasicNameValuePair(PARAM_PASSWORD, authtoken));
+
+        HttpEntity entity = null;
+        entity = new UrlEncodedFormEntity(params);
+        final HttpPost post = new HttpPost(FETCH_STATUS_URI);
+        post.addHeader(entity.getContentType());
+        post.setEntity(entity);
+        maybeCreateHttpClient();
+
+        final HttpResponse resp = mHttpClient.execute(post);
+        final String response = EntityUtils.toString(resp.getEntity());
+
+        if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
+            // Succesfully connected to the samplesyncadapter server and
+            // authenticated.
+            // Extract friends data in json format.
+            final JSONArray statuses = new JSONArray(response);
+            for (int i = 0; i < statuses.length(); i++) {
+                statusList.add(User.Status.valueOf(statuses.getJSONObject(i)));
+            }
+        } else {
+            if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
+                Log.e(TAG,
+                    "Authentication exception in fetching friend status list");
+                throw new AuthenticationException();
+            } else {
+                Log.e(TAG, "Server error in fetching friend status list");
+                throw new IOException();
+            }
+        }
+        return statusList;
+    }
+
+}
diff --git a/samples/SampleSyncAdapter/src/com/example/android/samplesync/client/User.java b/samples/SampleSyncAdapter/src/com/example/android/samplesync/client/User.java
new file mode 100644
index 0000000..6ce9b3f
--- /dev/null
+++ b/samples/SampleSyncAdapter/src/com/example/android/samplesync/client/User.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2010 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.example.android.samplesync.client;
+
+import android.util.Log;
+
+import org.json.JSONObject;
+
+/**
+ * Represents a sample SyncAdapter user
+ */
+public class User {
+
+    private final String mUserName;
+    private final String mFirstName;
+    private final String mLastName;
+    private final String mCellPhone;
+    private final String mOfficePhone;
+    private final String mHomePhone;
+    private final String mEmail;
+    private final boolean mDeleted;
+    private final int mUserId;
+
+    public int getUserId() {
+        return mUserId;
+    }
+
+    public String getUserName() {
+        return mUserName;
+    }
+
+    public String getFirstName() {
+        return mFirstName;
+    }
+
+    public String getLastName() {
+        return mLastName;
+    }
+
+    public String getCellPhone() {
+        return mCellPhone;
+    }
+
+    public String getOfficePhone() {
+        return mOfficePhone;
+    }
+
+    public String getHomePhone() {
+        return mHomePhone;
+    }
+
+    public String getEmail() {
+        return mEmail;
+    }
+
+    public boolean isDeleted() {
+        return mDeleted;
+    }
+
+    public User(String name, String firstName, String lastName,
+        String cellPhone, String officePhone, String homePhone, String email,
+        Boolean deleted, Integer userId) {
+        mUserName = name;
+        mFirstName = firstName;
+        mLastName = lastName;
+        mCellPhone = cellPhone;
+        mOfficePhone = officePhone;
+        mHomePhone = homePhone;
+        mEmail = email;
+        mDeleted = deleted;
+        mUserId = userId;
+    }
+
+    /**
+     * Creates and returns an instance of the user from the provided JSON data.
+     * 
+     * @param user The JSONObject containing user data
+     * @return user The new instance of Voiper user created from the JSON data.
+     */
+    public static User valueOf(JSONObject user) {
+        try {
+            final String userName = user.getString("u");
+            final String firstName = user.has("f") ? user.getString("f") : null;
+            final String lastName = user.has("l") ? user.getString("l") : null;
+            final String cellPhone = user.has("m") ? user.getString("m") : null;
+            final String officePhone =
+                user.has("o") ? user.getString("o") : null;
+            final String homePhone = user.has("h") ? user.getString("h") : null;
+            final String email = user.has("e") ? user.getString("e") : null;
+            final boolean deleted =
+                user.has("d") ? user.getBoolean("d") : false;
+            final int userId = user.getInt("i");
+            return new User(userName, firstName, lastName, cellPhone,
+                officePhone, homePhone, email, deleted, userId);
+        } catch (final Exception ex) {
+            Log.i("User", "Error parsing JSON user object" + ex.toString());
+
+        }
+        return null;
+
+    }
+
+    /**
+     * Represents the User's status messages
+     * 
+     */
+    public static class Status {
+        private final Integer mUserId;
+        private final String mStatus;
+
+        public int getUserId() {
+            return mUserId;
+        }
+
+        public String getStatus() {
+            return mStatus;
+        }
+
+        public Status(Integer userId, String status) {
+            mUserId = userId;
+            mStatus = status;
+        }
+
+        public static User.Status valueOf(JSONObject userStatus) {
+            try {
+                final int userId = userStatus.getInt("i");
+                final String status = userStatus.getString("s");
+                return new User.Status(userId, status);
+            } catch (final Exception ex) {
+                Log.i("User.Status", "Error parsing JSON user object");
+            }
+            return null;
+        }
+    }
+
+}
diff --git a/samples/SampleSyncAdapter/src/com/example/android/samplesync/platform/BatchOperation.java b/samples/SampleSyncAdapter/src/com/example/android/samplesync/platform/BatchOperation.java
new file mode 100644
index 0000000..509d151
--- /dev/null
+++ b/samples/SampleSyncAdapter/src/com/example/android/samplesync/platform/BatchOperation.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2010 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.example.android.samplesync.platform;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * This class handles execution of batch mOperations on Contacts provider.
+ */
+public class BatchOperation {
+    private final String TAG = "BatchOperation";
+
+    private final ContentResolver mResolver;
+    // List for storing the batch mOperations
+    ArrayList<ContentProviderOperation> mOperations;
+
+    public BatchOperation(Context context, ContentResolver resolver) {
+        mResolver = resolver;
+        mOperations = new ArrayList<ContentProviderOperation>();
+    }
+
+    public int size() {
+        return mOperations.size();
+    }
+
+    public void add(ContentProviderOperation cpo) {
+        mOperations.add(cpo);
+    }
+
+    public void execute() {
+        if (mOperations.size() == 0) {
+            return;
+        }
+        // Apply the mOperations to the content provider
+        try {
+            mResolver.applyBatch(ContactsContract.AUTHORITY, mOperations);
+        } catch (final OperationApplicationException e1) {
+            Log.e(TAG, "storing contact data failed", e1);
+        } catch (final RemoteException e2) {
+            Log.e(TAG, "storing contact data failed", e2);
+        }
+        mOperations.clear();
+    }
+
+}
diff --git a/samples/SampleSyncAdapter/src/com/example/android/samplesync/platform/ContactManager.java b/samples/SampleSyncAdapter/src/com/example/android/samplesync/platform/ContactManager.java
new file mode 100644
index 0000000..4f71be0
--- /dev/null
+++ b/samples/SampleSyncAdapter/src/com/example/android/samplesync/platform/ContactManager.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright (C) 2010 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.example.android.samplesync.platform;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.StatusUpdates;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.util.Log;
+
+import com.example.android.samplesync.Constants;
+import com.example.android.samplesync.R;
+import com.example.android.samplesync.client.User;
+
+import java.util.List;
+
+/**
+ * Class for managing contacts sync related mOperations
+ */
+public class ContactManager {
+    /**
+     * Custom IM protocol used when storing status messages.
+     */
+    public static final String CUSTOM_IM_PROTOCOL = "SampleSyncAdapter";
+    private static final String TAG = "ContactManager";
+
+    /**
+     * Synchronize raw contacts
+     * 
+     * @param context The context of Authenticator Activity
+     * @param account The username for the account
+     * @param users The list of users
+     */
+    public static synchronized void syncContacts(Context context,
+        String account, List<User> users) {
+        long userId;
+        long rawContactId = 0;
+        final ContentResolver resolver = context.getContentResolver();
+        final BatchOperation batchOperation =
+            new BatchOperation(context, resolver);
+        Log.d(TAG, "In SyncContacts");
+        for (final User user : users) {
+            userId = user.getUserId();
+            // Check to see if the contact needs to be inserted or updated
+            rawContactId = lookupRawContact(resolver, userId);
+            if (rawContactId != 0) {
+                if (!user.isDeleted()) {
+                    // update contact
+                    updateContact(context, resolver, account, user,
+                        rawContactId, batchOperation);
+                } else {
+                    // delete contact
+                    deleteContact(context, rawContactId, batchOperation);
+                }
+            } else {
+                // add new contact
+                Log.d(TAG, "In addContact");
+                if (!user.isDeleted()) {
+                    addContact(context, account, user, batchOperation);
+                }
+            }
+            // A sync adapter should batch operations on multiple contacts,
+            // because it will make a dramatic performance difference.
+            if (batchOperation.size() >= 50) {
+                batchOperation.execute();
+            }
+        }
+        batchOperation.execute();
+    }
+
+    /**
+     * Add a list of status messages to the contacts provider.
+     * 
+     * @param context the context to use
+     * @param accountName the username of the logged in user
+     * @param statuses the list of statuses to store
+     */
+    public static void insertStatuses(Context context, String username,
+        List<User.Status> list) {
+        final ContentValues values = new ContentValues();
+        final ContentResolver resolver = context.getContentResolver();
+        final BatchOperation batchOperation =
+            new BatchOperation(context, resolver);
+        for (final User.Status status : list) {
+            // Look up the user's sample SyncAdapter data row
+            final long userId = status.getUserId();
+            final long profileId = lookupProfile(resolver, userId);
+
+            // Insert the activity into the stream
+            if (profileId > 0) {
+                values.put(StatusUpdates.DATA_ID, profileId);
+                values.put(StatusUpdates.STATUS, status.getStatus());
+                values.put(StatusUpdates.PROTOCOL, Im.PROTOCOL_CUSTOM);
+                values.put(StatusUpdates.CUSTOM_PROTOCOL, CUSTOM_IM_PROTOCOL);
+                values.put(StatusUpdates.IM_ACCOUNT, username);
+                values.put(StatusUpdates.IM_HANDLE, status.getUserId());
+                values.put(StatusUpdates.STATUS_RES_PACKAGE, context
+                    .getPackageName());
+                values.put(StatusUpdates.STATUS_ICON, R.drawable.icon);
+                values.put(StatusUpdates.STATUS_LABEL, R.string.label);
+
+                batchOperation
+                    .add(ContactOperations.newInsertCpo(
+                        StatusUpdates.CONTENT_URI, true).withValues(values)
+                        .build());
+                // A sync adapter should batch operations on multiple contacts,
+                // because it will make a dramatic performance difference.
+                if (batchOperation.size() >= 50) {
+                    batchOperation.execute();
+                }
+            }
+        }
+        batchOperation.execute();
+    }
+
+    /**
+     * Adds a single contact to the platform contacts provider.
+     * 
+     * @param context the Authenticator Activity context
+     * @param accountName the account the contact belongs to
+     * @param user the sample SyncAdapter User object
+     */
+    private static void addContact(Context context, String accountName,
+        User user, BatchOperation batchOperation) {
+        // Put the data in the contacts provider
+        final ContactOperations contactOp =
+            ContactOperations.createNewContact(context, user.getUserId(),
+                accountName, batchOperation);
+        contactOp.addName(user.getFirstName(), user.getLastName()).addEmail(
+            user.getEmail()).addPhone(user.getCellPhone(), Phone.TYPE_MOBILE)
+            .addPhone(user.getHomePhone(), Phone.TYPE_OTHER).addProfileAction(
+                user.getUserId());
+    }
+
+    /**
+     * Updates a single contact to the platform contacts provider.
+     * 
+     * @param context the Authenticator Activity context
+     * @param resolver the ContentResolver to use
+     * @param accountName the account the contact belongs to
+     * @param user the sample SyncAdapter contact object.
+     * @param rawContactId the unique Id for this rawContact in contacts
+     *        provider
+     */
+    private static void updateContact(Context context,
+        ContentResolver resolver, String accountName, User user,
+        long rawContactId, BatchOperation batchOperation) {
+        Uri uri;
+        String cellPhone = null;
+        String otherPhone = null;
+        String email = null;
+
+        final Cursor c =
+            resolver.query(Data.CONTENT_URI, DataQuery.PROJECTION,
+                DataQuery.SELECTION,
+                new String[] {String.valueOf(rawContactId)}, null);
+        final ContactOperations contactOp =
+            ContactOperations.updateExistingContact(context, rawContactId,
+                batchOperation);
+
+        try {
+            while (c.moveToNext()) {
+                final long id = c.getLong(DataQuery.COLUMN_ID);
+                final String mimeType = c.getString(DataQuery.COLUMN_MIMETYPE);
+                uri = ContentUris.withAppendedId(Data.CONTENT_URI, id);
+
+                if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) {
+                    final String lastName =
+                        c.getString(DataQuery.COLUMN_FAMILY_NAME);
+                    final String firstName =
+                        c.getString(DataQuery.COLUMN_GIVEN_NAME);
+                    contactOp.updateName(uri, firstName, lastName, user
+                        .getFirstName(), user.getLastName());
+                }
+
+                else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) {
+                    final int type = c.getInt(DataQuery.COLUMN_PHONE_TYPE);
+
+                    if (type == Phone.TYPE_MOBILE) {
+                        cellPhone = c.getString(DataQuery.COLUMN_PHONE_NUMBER);
+                        contactOp.updatePhone(cellPhone, user.getCellPhone(),
+                            uri);
+                    } else if (type == Phone.TYPE_OTHER) {
+                        otherPhone = c.getString(DataQuery.COLUMN_PHONE_NUMBER);
+                        contactOp.updatePhone(otherPhone, user.getHomePhone(),
+                            uri);
+                    }
+                }
+
+                else if (Data.MIMETYPE.equals(Email.CONTENT_ITEM_TYPE)) {
+                    email = c.getString(DataQuery.COLUMN_EMAIL_ADDRESS);
+                    contactOp.updateEmail(user.getEmail(), email, uri);
+
+                }
+            } // while
+        } finally {
+            c.close();
+        }
+
+        // Add the cell phone, if present and not updated above
+        if (cellPhone == null) {
+            contactOp.addPhone(user.getCellPhone(), Phone.TYPE_MOBILE);
+        }
+
+        // Add the other phone, if present and not updated above
+        if (otherPhone == null) {
+            contactOp.addPhone(user.getHomePhone(), Phone.TYPE_OTHER);
+        }
+
+        // Add the email address, if present and not updated above
+        if (email == null) {
+            contactOp.addEmail(user.getEmail());
+        }
+
+    }
+
+    /**
+     * Deletes a contact from the platform contacts provider.
+     * 
+     * @param context the Authenticator Activity context
+     * @param rawContactId the unique Id for this rawContact in contacts
+     *        provider
+     */
+    private static void deleteContact(Context context, long rawContactId,
+        BatchOperation batchOperation) {
+        batchOperation.add(ContactOperations.newDeleteCpo(
+            ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
+            true).build());
+    }
+
+    /**
+     * Returns the RawContact id for a sample SyncAdapter contact, or 0 if the
+     * sample SyncAdapter user isn't found.
+     * 
+     * @param context the Authenticator Activity context
+     * @param userId the sample SyncAdapter user ID to lookup
+     * @return the RawContact id, or 0 if not found
+     */
+    private static long lookupRawContact(ContentResolver resolver, long userId) {
+        long authorId = 0;
+        final Cursor c =
+            resolver.query(RawContacts.CONTENT_URI, UserIdQuery.PROJECTION,
+                UserIdQuery.SELECTION, new String[] {String.valueOf(userId)},
+                null);
+        try {
+            if (c.moveToFirst()) {
+                authorId = c.getLong(UserIdQuery.COLUMN_ID);
+            }
+        } finally {
+            if (c != null) {
+                c.close();
+            }
+        }
+        return authorId;
+    }
+
+    /**
+     * Returns the Data id for a sample SyncAdapter contact's profile row, or 0
+     * if the sample SyncAdapter user isn't found.
+     * 
+     * @param resolver a content resolver
+     * @param userId the sample SyncAdapter user ID to lookup
+     * @return the profile Data row id, or 0 if not found
+     */
+    private static long lookupProfile(ContentResolver resolver, long userId) {
+        long profileId = 0;
+        final Cursor c =
+            resolver.query(Data.CONTENT_URI, ProfileQuery.PROJECTION,
+                ProfileQuery.SELECTION, new String[] {String.valueOf(userId)},
+                null);
+        try {
+            if (c != null && c.moveToFirst()) {
+                profileId = c.getLong(ProfileQuery.COLUMN_ID);
+            }
+        } finally {
+            if (c != null) {
+                c.close();
+            }
+        }
+        return profileId;
+    }
+
+    /**
+     * Constants for a query to find a contact given a sample SyncAdapter user
+     * ID.
+     */
+    private interface ProfileQuery {
+        public final static String[] PROJECTION = new String[] {Data._ID};
+
+        public final static int COLUMN_ID = 0;
+
+        public static final String SELECTION =
+            Data.MIMETYPE + "='" + SampleSyncAdapterColumns.MIME_PROFILE
+                + "' AND " + SampleSyncAdapterColumns.DATA_PID + "=?";
+    }
+    /**
+     * Constants for a query to find a contact given a sample SyncAdapter user
+     * ID.
+     */
+    private interface UserIdQuery {
+        public final static String[] PROJECTION =
+            new String[] {RawContacts._ID};
+
+        public final static int COLUMN_ID = 0;
+
+        public static final String SELECTION =
+            RawContacts.ACCOUNT_TYPE + "='" + Constants.ACCOUNT_TYPE + "' AND "
+                + RawContacts.SOURCE_ID + "=?";
+    }
+
+    /**
+     * Constants for a query to get contact data for a given rawContactId
+     */
+    private interface DataQuery {
+        public static final String[] PROJECTION =
+            new String[] {Data._ID, Data.MIMETYPE, Data.DATA1, Data.DATA2,
+                Data.DATA3,};
+
+        public static final int COLUMN_ID = 0;
+        public static final int COLUMN_MIMETYPE = 1;
+        public static final int COLUMN_DATA1 = 2;
+        public static final int COLUMN_DATA2 = 3;
+        public static final int COLUMN_DATA3 = 4;
+        public static final int COLUMN_PHONE_NUMBER = COLUMN_DATA1;
+        public static final int COLUMN_PHONE_TYPE = COLUMN_DATA2;
+        public static final int COLUMN_EMAIL_ADDRESS = COLUMN_DATA1;
+        public static final int COLUMN_EMAIL_TYPE = COLUMN_DATA2;
+        public static final int COLUMN_GIVEN_NAME = COLUMN_DATA2;
+        public static final int COLUMN_FAMILY_NAME = COLUMN_DATA3;
+
+        public static final String SELECTION = Data.RAW_CONTACT_ID + "=?";
+    }
+}
diff --git a/samples/SampleSyncAdapter/src/com/example/android/samplesync/platform/ContactOperations.java b/samples/SampleSyncAdapter/src/com/example/android/samplesync/platform/ContactOperations.java
new file mode 100644
index 0000000..9e47f70
--- /dev/null
+++ b/samples/SampleSyncAdapter/src/com/example/android/samplesync/platform/ContactOperations.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2010 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.example.android.samplesync.platform;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.example.android.samplesync.Constants;
+import com.example.android.samplesync.R;
+
+/**
+ * Helper class for storing data in the platform content providers.
+ */
+public class ContactOperations {
+
+    private final ContentValues mValues;
+    private ContentProviderOperation.Builder mBuilder;
+    private final BatchOperation mBatchOperation;
+    private final Context mContext;
+    private boolean mYield;
+    private long mRawContactId;
+    private int mBackReference;
+    private boolean mIsNewContact;
+
+    /**
+     * Returns an instance of ContactOperations instance for adding new contact
+     * to the platform contacts provider.
+     * 
+     * @param context the Authenticator Activity context
+     * @param userId the userId of the sample SyncAdapter user object
+     * @param accountName the username of the current login
+     * @return instance of ContactOperations
+     */
+    public static ContactOperations createNewContact(Context context,
+        int userId, String accountName, BatchOperation batchOperation) {
+        return new ContactOperations(context, userId, accountName,
+            batchOperation);
+    }
+
+    /**
+     * Returns an instance of ContactOperations for updating existing contact in
+     * the platform contacts provider.
+     * 
+     * @param context the Authenticator Activity context
+     * @param rawContactId the unique Id of the existing rawContact
+     * @return instance of ContactOperations
+     */
+    public static ContactOperations updateExistingContact(Context context,
+        long rawContactId, BatchOperation batchOperation) {
+        return new ContactOperations(context, rawContactId, batchOperation);
+    }
+
+    public ContactOperations(Context context, BatchOperation batchOperation) {
+        mValues = new ContentValues();
+        mYield = true;
+        mContext = context;
+        mBatchOperation = batchOperation;
+    }
+
+    public ContactOperations(Context context, int userId, String accountName,
+        BatchOperation batchOperation) {
+        this(context, batchOperation);
+        mBackReference = mBatchOperation.size();
+        mIsNewContact = true;
+        mValues.put(RawContacts.SOURCE_ID, userId);
+        mValues.put(RawContacts.ACCOUNT_TYPE, Constants.ACCOUNT_TYPE);
+        mValues.put(RawContacts.ACCOUNT_NAME, accountName);
+        mBuilder =
+            newInsertCpo(RawContacts.CONTENT_URI, true).withValues(mValues);
+        mBatchOperation.add(mBuilder.build());
+    }
+
+    public ContactOperations(Context context, long rawContactId,
+        BatchOperation batchOperation) {
+        this(context, batchOperation);
+        mIsNewContact = false;
+        mRawContactId = rawContactId;
+    }
+
+    /**
+     * Adds a contact name
+     * 
+     * @param name Name of contact
+     * @param nameType type of name: family name, given name, etc.
+     * @return instance of ContactOperations
+     */
+    public ContactOperations addName(String firstName, String lastName) {
+        mValues.clear();
+        if (!TextUtils.isEmpty(firstName)) {
+            mValues.put(StructuredName.GIVEN_NAME, firstName);
+            mValues.put(StructuredName.MIMETYPE,
+                StructuredName.CONTENT_ITEM_TYPE);
+        }
+        if (!TextUtils.isEmpty(lastName)) {
+            mValues.put(StructuredName.FAMILY_NAME, lastName);
+            mValues.put(StructuredName.MIMETYPE,
+                StructuredName.CONTENT_ITEM_TYPE);
+        }
+        if (mValues.size() > 0) {
+            addInsertOp();
+        }
+        return this;
+    }
+
+    /**
+     * Adds an email
+     * 
+     * @param new email for user
+     * @return instance of ContactOperations
+     */
+    public ContactOperations addEmail(String email) {
+        mValues.clear();
+        if (!TextUtils.isEmpty(email)) {
+            mValues.put(Email.DATA, email);
+            mValues.put(Email.TYPE, Email.TYPE_OTHER);
+            mValues.put(Email.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+            addInsertOp();
+        }
+        return this;
+    }
+
+    /**
+     * Adds a phone number
+     * 
+     * @param phone new phone number for the contact
+     * @param phoneType the type: cell, home, etc.
+     * @return instance of ContactOperations
+     */
+    public ContactOperations addPhone(String phone, int phoneType) {
+        mValues.clear();
+        if (!TextUtils.isEmpty(phone)) {
+            mValues.put(Phone.NUMBER, phone);
+            mValues.put(Phone.TYPE, phoneType);
+            mValues.put(Phone.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+            addInsertOp();
+        }
+        return this;
+    }
+
+    /**
+     * Adds a profile action
+     * 
+     * @param userId the userId of the sample SyncAdapter user object
+     * @return instance of ContactOperations
+     */
+    public ContactOperations addProfileAction(long userId) {
+        mValues.clear();
+        if (userId != 0) {
+            mValues.put(SampleSyncAdapterColumns.DATA_PID, userId);
+            mValues.put(SampleSyncAdapterColumns.DATA_SUMMARY, mContext
+                .getString(R.string.profile_action));
+            mValues.put(SampleSyncAdapterColumns.DATA_DETAIL, mContext
+                .getString(R.string.view_profile));
+            mValues.put(Data.MIMETYPE, SampleSyncAdapterColumns.MIME_PROFILE);
+            addInsertOp();
+        }
+        return this;
+    }
+
+    /**
+     * Updates contact's email
+     * 
+     * @param email email id of the sample SyncAdapter user
+     * @param uri Uri for the existing raw contact to be updated
+     * @return instance of ContactOperations
+     */
+    public ContactOperations updateEmail(String email, String existingEmail,
+        Uri uri) {
+        if (!TextUtils.equals(existingEmail, email)) {
+            mValues.clear();
+            mValues.put(Email.DATA, email);
+            addUpdateOp(uri);
+        }
+        return this;
+    }
+
+    /**
+     * Updates contact's name
+     * 
+     * @param name Name of contact
+     * @param existingName Name of contact stored in provider
+     * @param nameType type of name: family name, given name, etc.
+     * @param uri Uri for the existing raw contact to be updated
+     * @return instance of ContactOperations
+     */
+    public ContactOperations updateName(Uri uri, String existingFirstName,
+        String existingLastName, String firstName, String lastName) {
+        Log.i("ContactOperations", "ef=" + existingFirstName + "el="
+            + existingLastName + "f=" + firstName + "l=" + lastName);
+        mValues.clear();
+        if (!TextUtils.equals(existingFirstName, firstName)) {
+            mValues.put(StructuredName.GIVEN_NAME, firstName);
+        }
+        if (!TextUtils.equals(existingLastName, lastName)) {
+            mValues.put(StructuredName.FAMILY_NAME, lastName);
+        }
+        if (mValues.size() > 0) {
+            addUpdateOp(uri);
+        }
+        return this;
+    }
+
+    /**
+     * Updates contact's phone
+     * 
+     * @param existingNumber phone number stored in contacts provider
+     * @param phone new phone number for the contact
+     * @param uri Uri for the existing raw contact to be updated
+     * @return instance of ContactOperations
+     */
+    public ContactOperations updatePhone(String existingNumber, String phone,
+        Uri uri) {
+        if (!TextUtils.equals(phone, existingNumber)) {
+            mValues.clear();
+            mValues.put(Phone.NUMBER, phone);
+            addUpdateOp(uri);
+        }
+        return this;
+    }
+
+    /**
+     * Updates contact's profile action
+     * 
+     * @param userId sample SyncAdapter user id
+     * @param uri Uri for the existing raw contact to be updated
+     * @return instance of ContactOperations
+     */
+    public ContactOperations updateProfileAction(Integer userId, Uri uri) {
+        mValues.clear();
+        mValues.put(SampleSyncAdapterColumns.DATA_PID, userId);
+        addUpdateOp(uri);
+        return this;
+    }
+
+    /**
+     * Adds an insert operation into the batch
+     */
+    private void addInsertOp() {
+        if (!mIsNewContact) {
+            mValues.put(Phone.RAW_CONTACT_ID, mRawContactId);
+        }
+        mBuilder =
+            newInsertCpo(addCallerIsSyncAdapterParameter(Data.CONTENT_URI),
+                mYield);
+        mBuilder.withValues(mValues);
+        if (mIsNewContact) {
+            mBuilder
+                .withValueBackReference(Data.RAW_CONTACT_ID, mBackReference);
+        }
+        mYield = false;
+        mBatchOperation.add(mBuilder.build());
+    }
+
+    /**
+     * Adds an update operation into the batch
+     */
+    private void addUpdateOp(Uri uri) {
+        mBuilder = newUpdateCpo(uri, mYield).withValues(mValues);
+        mYield = false;
+        mBatchOperation.add(mBuilder.build());
+    }
+
+    public static ContentProviderOperation.Builder newInsertCpo(Uri uri,
+        boolean yield) {
+        return ContentProviderOperation.newInsert(
+            addCallerIsSyncAdapterParameter(uri)).withYieldAllowed(yield);
+    }
+
+    public static ContentProviderOperation.Builder newUpdateCpo(Uri uri,
+        boolean yield) {
+        return ContentProviderOperation.newUpdate(
+            addCallerIsSyncAdapterParameter(uri)).withYieldAllowed(yield);
+    }
+
+    public static ContentProviderOperation.Builder newDeleteCpo(Uri uri,
+        boolean yield) {
+        return ContentProviderOperation.newDelete(
+            addCallerIsSyncAdapterParameter(uri)).withYieldAllowed(yield);
+
+    }
+
+    private static Uri addCallerIsSyncAdapterParameter(Uri uri) {
+        return uri.buildUpon().appendQueryParameter(
+            ContactsContract.CALLER_IS_SYNCADAPTER, "true").build();
+    }
+
+}
diff --git a/samples/SampleSyncAdapter/src/com/example/android/samplesync/platform/SampleSyncAdapterColumns.java b/samples/SampleSyncAdapter/src/com/example/android/samplesync/platform/SampleSyncAdapterColumns.java
new file mode 100644
index 0000000..bc02325
--- /dev/null
+++ b/samples/SampleSyncAdapter/src/com/example/android/samplesync/platform/SampleSyncAdapterColumns.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2010 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.example.android.samplesync.platform;
+
+import android.provider.ContactsContract.Data;
+
+/*
+ * The standard columns representing contact's info from social apps.
+ */
+public interface SampleSyncAdapterColumns {
+    /**
+     * MIME-type used when storing a profile {@link Data} entry.
+     */
+    public static final String MIME_PROFILE =
+        "vnd.android.cursor.item/vnd.samplesyncadapter.profile";
+
+    public static final String DATA_PID = Data.DATA1;
+    public static final String DATA_SUMMARY = Data.DATA2;
+    public static final String DATA_DETAIL = Data.DATA3;
+
+}
diff --git a/samples/SampleSyncAdapter/src/com/example/android/samplesync/syncadapter/SyncAdapter.java b/samples/SampleSyncAdapter/src/com/example/android/samplesync/syncadapter/SyncAdapter.java
new file mode 100644
index 0000000..07525aa
--- /dev/null
+++ b/samples/SampleSyncAdapter/src/com/example/android/samplesync/syncadapter/SyncAdapter.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2010 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.example.android.samplesync.syncadapter;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+import android.content.AbstractThreadedSyncAdapter;
+import android.content.ContentProviderClient;
+import android.content.Context;
+import android.content.SyncResult;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.example.android.samplesync.Constants;
+import com.example.android.samplesync.client.NetworkUtilities;
+import com.example.android.samplesync.client.User;
+import com.example.android.samplesync.client.User.Status;
+import com.example.android.samplesync.platform.ContactManager;
+
+import org.apache.http.ParseException;
+import org.apache.http.auth.AuthenticationException;
+import org.json.JSONException;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * SyncAdapter implementation for syncing sample SyncAdapter contacts to the
+ * platform ContactOperations provider.
+ */
+public class SyncAdapter extends AbstractThreadedSyncAdapter {
+    private static final String TAG = "SyncAdapter";
+
+    private final AccountManager mAccountManager;
+    private final Context mContext;
+
+    private Date mLastUpdated;
+
+    public SyncAdapter(Context context, boolean autoInitialize) {
+        super(context, autoInitialize);
+        mContext = context;
+        mAccountManager = AccountManager.get(context);
+    }
+
+    @Override
+    public void onPerformSync(Account account, Bundle extras, String authority,
+        ContentProviderClient provider, SyncResult syncResult) {
+        List<User> users;
+        List<Status> statuses;
+        String authtoken = null;
+         try {
+             // use the account manager to request the credentials
+             authtoken =
+                mAccountManager.blockingGetAuthToken(account,
+                    Constants.AUTHTOKEN_TYPE, true /* notifyAuthFailure */);
+             // fetch updates from the sample service over the cloud
+             users =
+                NetworkUtilities.fetchFriendUpdates(account, authtoken,
+                    mLastUpdated);
+            // update the last synced date.
+            mLastUpdated = new Date();
+            // update platform contacts.
+            Log.d(TAG, "Calling contactManager's sync contacts");
+            ContactManager.syncContacts(mContext, account.name, users);
+            // fetch and update status messages for all the synced users.
+            statuses = NetworkUtilities.fetchFriendStatuses(account, authtoken);
+            ContactManager.insertStatuses(mContext, account.name, statuses);
+        } catch (final AuthenticatorException e) {
+            syncResult.stats.numParseExceptions++;
+            Log.e(TAG, "AuthenticatorException", e);
+        } catch (final OperationCanceledException e) {
+            Log.e(TAG, "OperationCanceledExcetpion", e);
+        } catch (final IOException e) {
+            Log.e(TAG, "IOException", e);
+            syncResult.stats.numIoExceptions++;
+        } catch (final AuthenticationException e) {
+            mAccountManager.invalidateAuthToken(Constants.ACCOUNT_TYPE,
+                authtoken);
+            syncResult.stats.numAuthExceptions++;
+            Log.e(TAG, "AuthenticationException", e);
+        } catch (final ParseException e) {
+            syncResult.stats.numParseExceptions++;
+            Log.e(TAG, "ParseException", e);
+        } catch (final JSONException e) {
+            syncResult.stats.numParseExceptions++;
+            Log.e(TAG, "JSONException", e);
+        }
+    }
+}
diff --git a/samples/SampleSyncAdapter/src/com/example/android/samplesync/syncadapter/SyncService.java b/samples/SampleSyncAdapter/src/com/example/android/samplesync/syncadapter/SyncService.java
new file mode 100644
index 0000000..256f91d
--- /dev/null
+++ b/samples/SampleSyncAdapter/src/com/example/android/samplesync/syncadapter/SyncService.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2010 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.example.android.samplesync.syncadapter;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+/**
+ * Service to handle Account sync. This is invoked with an intent with action
+ * ACTION_AUTHENTICATOR_INTENT. It instantiates the syncadapter and returns its
+ * IBinder.
+ */
+public class SyncService extends Service {
+    private static final Object sSyncAdapterLock = new Object();
+    private static SyncAdapter sSyncAdapter = null;
+
+    /*
+     * {@inheritDoc}
+     */
+    @Override
+    public void onCreate() {
+        synchronized (sSyncAdapterLock) {
+            if (sSyncAdapter == null) {
+                sSyncAdapter = new SyncAdapter(getApplicationContext(), true);
+            }
+        }
+    }
+
+    /*
+     * {@inheritDoc}
+     */
+    @Override
+    public IBinder onBind(Intent intent) {
+        return sSyncAdapter.getSyncAdapterBinder();
+    }
+}