New version of SampleSyncAdapter sample code that allows local editing

The changes made were
pretty sweeping. The biggest addition was to allow on-device contact
creation/editing, and supporting 2-way sync to the sample server that
runs in Google App Engine.

The client-side sample code also includes examples of how to support
the user of AuthTokens (instead of always sending username/password
to the server), how to change a contact's picture, and how to set
IM-style status messages for each contact.

I also greatly simplified the server code so that instead of mimicking
both an addressbook and an IM-style status update system for multiple
users, it really just simulates an addressbook for a single user. The
server code also includes a cron job that (once a week) blows away the
contact database, so that it's relatively self-cleaning.

Change-Id: I017f1d3f9320a02fe05a20f1613846963107145e
diff --git a/samples/SampleSyncAdapter/Android.mk b/samples/SampleSyncAdapter/Android.mk
index a27a68f..0f87c17 100644
--- a/samples/SampleSyncAdapter/Android.mk
+++ b/samples/SampleSyncAdapter/Android.mk
@@ -6,10 +6,12 @@
 # Only compile source java files in this apk.
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
-LOCAL_PACKAGE_NAME := Voiper
+LOCAL_PACKAGE_NAME := SampleSyncAdapter
 
 LOCAL_SDK_VERSION := current
 
+LOCAL_DX_FLAGS=--target-api=11
+
 include $(BUILD_PACKAGE)
 
 # Use the folloing include to make our test apk.
diff --git a/samples/SampleSyncAdapter/AndroidManifest.xml b/samples/SampleSyncAdapter/AndroidManifest.xml
index 202ed0e..fd53a16 100644
--- a/samples/SampleSyncAdapter/AndroidManifest.xml
+++ b/samples/SampleSyncAdapter/AndroidManifest.xml
@@ -46,7 +46,7 @@
     <uses-permission
         android:name="android.permission.WRITE_SYNC_SETTINGS" />
 
-    <uses-sdk android:minSdkVersion="5" />
+    <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="11"/>
         
     <application
         android:icon="@drawable/icon"
@@ -82,11 +82,36 @@
             android:label="@string/ui_activity_title"
             android:theme="@android:style/Theme.Dialog"
             android:excludeFromRecents="true"
+            android:configChanges="orientation"
             >
             <!--
                 No intent-filter here! This activity is only ever launched by
                 someone who explicitly knows the class name
             -->
         </activity>
+
+        <activity
+            android:name=".editor.ContactEditorActivity"
+            android:theme="@style/ContactEditTheme"
+            android:windowSoftInputMode="adjustResize">
+            <intent-filter>
+                <action
+                    android:name="android.intent.action.INSERT" />
+                <data
+                    android:mimeType="vnd.android.cursor.item/contact" />
+            </intent-filter>
+
+            <!--
+                Note that the editor gets a raw contact URI, but is expected to call
+                setResult with the corresponding aggregate contact URI, not raw contact
+                URI.
+            -->
+            <intent-filter>
+                <action
+                    android:name="android.intent.action.EDIT" />
+                <data
+                    android:mimeType="vnd.android.cursor.item/raw_contact" />
+            </intent-filter>
+        </activity>
     </application>
 </manifest>
diff --git a/samples/SampleSyncAdapter/_index.html b/samples/SampleSyncAdapter/_index.html
index 4191ba5..603083a 100644
--- a/samples/SampleSyncAdapter/_index.html
+++ b/samples/SampleSyncAdapter/_index.html
@@ -2,7 +2,8 @@
 cloud-based service and synchronize its 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>
+adapter). It also demonstrates how to provide users the ability to create
+and edit synchronized contacts using a custom editor.</p>
 
 <p> The <a
 href="../../../reference/android/accounts/AccountManager.html">account
@@ -26,7 +27,7 @@
 issues a sync operation for that sync adapter. </p>
 
 <p> The cloud-based service for this sample application is running at: </p>
-<p style="margin-left:2em;">http://samplesyncadapter.appspot.com/users</p>
+<p style="margin-left:2em;">http://samplesyncadapter2.appspot.com/</p>
 
 <p>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 |
diff --git a/samples/SampleSyncAdapter/res/drawable/border.xml b/samples/SampleSyncAdapter/res/drawable/border.xml
new file mode 100644
index 0000000..ab71f2c
--- /dev/null
+++ b/samples/SampleSyncAdapter/res/drawable/border.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+  <solid android:color="@color/EditPanelBackgroundColor" />
+  <stroke android:width="2dip" android:color="@color/EditPanelBorderColor" />
+  <padding android:left="5dip" android:top="5dip" android:right="5dip" android:bottom="5dip" />
+</shape>
\ No newline at end of file
diff --git a/samples/SampleSyncAdapter/res/drawable/done_menu_icon.png b/samples/SampleSyncAdapter/res/drawable/done_menu_icon.png
new file mode 100644
index 0000000..3468bbd
--- /dev/null
+++ b/samples/SampleSyncAdapter/res/drawable/done_menu_icon.png
Binary files differ
diff --git a/samples/SampleSyncAdapter/res/layout-xlarge/editor.xml b/samples/SampleSyncAdapter/res/layout-xlarge/editor.xml
new file mode 100644
index 0000000..3b7d97b
--- /dev/null
+++ b/samples/SampleSyncAdapter/res/layout-xlarge/editor.xml
@@ -0,0 +1,43 @@
+<?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:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:gravity="center_horizontal">
+    <LinearLayout
+      xmlns:android="http://schemas.android.com/apk/res/android"
+      android:layout_width="600dip"
+      android:layout_height="match_parent"
+      android:orientation="vertical"
+      android:background="@drawable/border">
+      <include layout="@layout/editor_header" />
+      <ScrollView
+          android:layout_width="match_parent"
+          android:layout_height="0dip"
+          android:paddingTop="20dip"
+          android:paddingRight="20dip"
+          android:paddingBottom="20dip"
+          android:paddingLeft="20dip"
+          android:layout_weight="1">
+          <include layout="@layout/editor_fields" />
+      </ScrollView>
+    </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/samples/SampleSyncAdapter/res/layout-xlarge/editor_header.xml b/samples/SampleSyncAdapter/res/layout-xlarge/editor_header.xml
new file mode 100644
index 0000000..648e6f9
--- /dev/null
+++ b/samples/SampleSyncAdapter/res/layout-xlarge/editor_header.xml
@@ -0,0 +1,58 @@
+<?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.
+-->
+
+
+<!-- Account info header -->
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_height="64dip"
+    android:layout_width="match_parent"
+    android:background="?android:attr/selectableItemBackground">
+
+    <ImageView
+        android:id="@+id/header_account_icon"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginLeft="7dip"
+        android:layout_marginRight="7dip"
+        android:layout_centerVertical="true"
+        android:layout_alignParentRight="true"
+        android:src="@drawable/icon" />
+
+    <TextView
+        android:id="@+id/header_account_type"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_toLeftOf="@+id/header_account_icon"
+        android:layout_alignTop="@id/header_account_icon"
+        android:layout_marginTop="-4dip"
+        android:textSize="24sp"
+        android:textColor="?android:attr/textColorPrimary"
+        android:singleLine="true"
+        android:text="@string/header_account_type" />
+
+    <TextView
+        android:id="@+id/header_account_name"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_toLeftOf="@+id/header_account_icon"
+        android:layout_alignBottom="@+id/header_account_icon"
+        android:layout_marginBottom="2dip"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:textColor="?android:attr/textColorPrimary"
+        android:singleLine="true" />
+
+</RelativeLayout>
diff --git a/samples/SampleSyncAdapter/res/layout/editor.xml b/samples/SampleSyncAdapter/res/layout/editor.xml
new file mode 100644
index 0000000..a0c36d2
--- /dev/null
+++ b/samples/SampleSyncAdapter/res/layout/editor.xml
@@ -0,0 +1,36 @@
+<?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:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <ScrollView
+        android:layout_width="match_parent"
+        android:layout_height="0dip"
+        android:paddingTop="20dip"
+        android:paddingRight="20dip"
+        android:paddingBottom="20dip"
+        android:paddingLeft="20dip"
+        android:layout_weight="1">
+        <include layout="@layout/editor_fields" />
+    </ScrollView>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/samples/SampleSyncAdapter/res/layout/editor_fields.xml b/samples/SampleSyncAdapter/res/layout/editor_fields.xml
new file mode 100644
index 0000000..31d0128
--- /dev/null
+++ b/samples/SampleSyncAdapter/res/layout/editor_fields.xml
@@ -0,0 +1,127 @@
+<?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.
+ */
+-->
+
+<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  android:layout_width="fill_parent"
+  android:layout_height="fill_parent"
+  android:stretchColumns="2">
+
+  <TableRow>
+    <TextView
+      android:layout_column="1"
+      android:textStyle="bold"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:text="@string/label_name"
+      android:padding="3dip" />
+    <EditText
+      android:layout_column="2"
+      android:id="@+id/editor_name"
+      android:singleLine="true"
+      android:inputType="textPersonName"
+      android:layout_width="fill_parent"
+      android:layout_height="wrap_content"
+      android:minWidth="250dip"
+      android:scrollHorizontally="true"
+      android:capitalize="none"
+      android:textSize="@dimen/contact_name_text_size"
+      android:gravity="fill_horizontal"
+      android:autoText="false" />
+  </TableRow>
+  <TableRow>
+    <TextView
+      android:layout_column="1"
+      android:textStyle="bold"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:text="@string/label_phone_home"
+      android:padding="3dip" />
+    <EditText
+      android:id="@+id/editor_phone_home"
+      android:singleLine="true"
+      android:inputType="phone"
+      android:layout_width="fill_parent"
+      android:layout_height="wrap_content"
+      android:minWidth="250dip"
+      android:scrollHorizontally="true"
+      android:capitalize="none"
+      android:gravity="fill_horizontal"
+      android:autoText="false" />
+  </TableRow>
+  <TableRow>
+    <TextView
+      android:layout_column="1"
+      android:textStyle="bold"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:text="@string/label_phone_mobile"
+      android:padding="3dip" />
+    <EditText
+      android:id="@+id/editor_phone_mobile"
+      android:singleLine="true"
+      android:inputType="phone"
+      android:layout_width="fill_parent"
+      android:layout_height="wrap_content"
+      android:minWidth="250dip"
+      android:scrollHorizontally="true"
+      android:capitalize="none"
+      android:gravity="fill_horizontal"
+      android:autoText="false" />
+  </TableRow>
+  <TableRow>
+    <TextView
+      android:layout_column="1"
+      android:textStyle="bold"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:text="@string/label_phone_work"
+      android:padding="3dip" />
+    <EditText
+      android:id="@+id/editor_phone_work"
+      android:singleLine="true"
+      android:phoneNumber="true"
+      android:autoText="true"
+      android:layout_width="fill_parent"
+      android:layout_height="wrap_content"
+      android:minWidth="250dip"
+      android:scrollHorizontally="true"
+      android:capitalize="none"
+      android:gravity="fill_horizontal" />
+  </TableRow>
+  <TableRow>
+    <TextView
+      android:layout_column="1"
+      android:textStyle="bold"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:text="@string/label_email"
+      android:padding="3dip" />
+    <EditText
+      android:id="@+id/editor_email"
+      android:singleLine="true"
+      android:inputType="textEmailAddress"
+      android:layout_width="fill_parent"
+      android:layout_height="wrap_content"
+      android:minWidth="250dip"
+      android:scrollHorizontally="true"
+      android:capitalize="none"
+      android:gravity="fill_horizontal"
+      android:autoText="false" />
+  </TableRow>
+</TableLayout>
diff --git a/samples/SampleSyncAdapter/res/menu/edit.xml b/samples/SampleSyncAdapter/res/menu/edit.xml
new file mode 100644
index 0000000..1227584
--- /dev/null
+++ b/samples/SampleSyncAdapter/res/menu/edit.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/menu_done"
+        android:alphabeticShortcut="\n"
+        android:icon="@drawable/done_menu_icon"
+        android:title="@string/menu_done"
+        android:showAsAction="always|withText" />
+
+    <item
+        android:id="@+id/menu_cancel"
+        android:alphabeticShortcut="q"
+        android:title="@string/menu_cancel"
+        android:showAsAction="always|withText" />
+</menu>
diff --git a/samples/SampleSyncAdapter/res/values-xlarge/dimens.xml b/samples/SampleSyncAdapter/res/values-xlarge/dimens.xml
new file mode 100644
index 0000000..f9d74a7
--- /dev/null
+++ b/samples/SampleSyncAdapter/res/values-xlarge/dimens.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2011, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+<resources>
+
+    <!-- Font size used for the contact name in the editor -->
+    <dimen name="contact_name_text_size">26sp</dimen>
+
+</resources>
diff --git a/samples/SampleSyncAdapter/res/values/dimens.xml b/samples/SampleSyncAdapter/res/values/dimens.xml
new file mode 100644
index 0000000..7518c07
--- /dev/null
+++ b/samples/SampleSyncAdapter/res/values/dimens.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2011, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<resources>
+
+    <!-- Font size used for the contact name in the editor -->
+    <dimen name="contact_name_text_size">18sp</dimen>
+
+</resources>
diff --git a/samples/SampleSyncAdapter/res/values/strings.xml b/samples/SampleSyncAdapter/res/values/strings.xml
index 8139d65..3b5ac58 100644
--- a/samples/SampleSyncAdapter/res/values/strings.xml
+++ b/samples/SampleSyncAdapter/res/values/strings.xml
@@ -20,7 +20,7 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <!-- Label for this package -->
     <string
-        name="label">SamplesyncAdapter</string>
+        name="label">Sample SyncAdapter</string>
 
     <!-- Permission label -->
     <string
@@ -90,4 +90,16 @@
         name="profile_action">Sample profile</string>
     <string
         name="view_profile">View Profile</string>
+
+    <string name="header_account_type">SampleSync contact</string>
+
+    <string name="label_name">Name</string>
+    <string name="label_phone_home">Home Phone</string>
+    <string name="label_phone_mobile">Mobile Phone</string>
+    <string name="label_phone_work">Work Phone</string>
+    <string name="label_email">Email</string>
+    <string
+        name="menu_done">Done</string>
+    <string
+        name="menu_cancel">Cancel</string>
 </resources>
\ No newline at end of file
diff --git a/samples/SampleSyncAdapter/res/values/styles.xml b/samples/SampleSyncAdapter/res/values/styles.xml
new file mode 100644
index 0000000..074613e
--- /dev/null
+++ b/samples/SampleSyncAdapter/res/values/styles.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+    <!--
+        These styles will only be used in Honeycomb and later because
+        Android doesn't support third-party contact editing in pre-
+        Honeycomb versions.
+    -->
+    <color name="EditPanelBackgroundColor">#ffffff</color>
+    <color name="EditPanelBorderColor">#cccccc</color>
+    <style name="ContactEditTheme" parent="android:Theme.Holo.Light">
+    </style>
+</resources>
diff --git a/samples/SampleSyncAdapter/res/xml-v11/contacts.xml b/samples/SampleSyncAdapter/res/xml-v11/contacts.xml
new file mode 100644
index 0000000..62dfa80
--- /dev/null
+++ b/samples/SampleSyncAdapter/res/xml-v11/contacts.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2011, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<ContactsAccountType
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    editContactActivity="com.example.android.samplesync.editor.ContactEditorActivity"
+    createContactActivity="com.example.android.samplesync.editor.ContactEditorActivity"
+>
+
+    <ContactsDataKind
+        android:mimeType="vnd.android.cursor.item/vnd.samplesyncadapter.profile"
+        android:icon="@drawable/icon"
+        android:summaryColumn="data2"
+        android:detailColumn="data3"
+        android:detailSocialSummary="true" />
+
+</ContactsAccountType>
diff --git a/samples/SampleSyncAdapter/res/xml-v11/syncadapter.xml b/samples/SampleSyncAdapter/res/xml-v11/syncadapter.xml
new file mode 100644
index 0000000..dac8ade
--- /dev/null
+++ b/samples/SampleSyncAdapter/res/xml-v11/syncadapter.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2011, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<!--
+  The attributes in this XML file provide configuration information
+  for the SampleSyncAdapter.
+
+  See xml/syncadapter.xml for greater details, but this version of
+  the file specifies that uploading (and thus editing) is supported.
+-->
+
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+    android:contentAuthority="com.android.contacts"
+    android:accountType="com.example.android.samplesync"
+    android:supportsUploading="true"
+    android:userVisible="true"
+/>
diff --git a/samples/SampleSyncAdapter/res/xml/contacts.xml b/samples/SampleSyncAdapter/res/xml/contacts.xml
index 1ff9c05..b46257d 100644
--- a/samples/SampleSyncAdapter/res/xml/contacts.xml
+++ b/samples/SampleSyncAdapter/res/xml/contacts.xml
@@ -17,7 +17,11 @@
  */
 -->
 
-<ContactsSource xmlns:android="http://schemas.android.com/apk/res/android">
+<ContactsSource
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    editContactActivity="com.example.android.samplesync.editor.ContactEditorActivity"
+    createContactActivity="com.example.android.samplesync.editor.ContactEditorActivity"
+>
 
     <ContactsDataKind
         android:mimeType="vnd.android.cursor.item/vnd.samplesyncadapter.profile"
diff --git a/samples/SampleSyncAdapter/res/xml/syncadapter.xml b/samples/SampleSyncAdapter/res/xml/syncadapter.xml
index 1f75947..3053a24 100644
--- a/samples/SampleSyncAdapter/res/xml/syncadapter.xml
+++ b/samples/SampleSyncAdapter/res/xml/syncadapter.xml
@@ -17,11 +17,21 @@
  */
 -->
 
-<!-- The attributes in this XML file provide configuration information -->
-<!-- for the SyncAdapter. -->
+<!--
+  The attributes in this XML file provide configuration information
+  for the SampleSyncAdapter.
+
+  We have two versions of this file - one here, and one in the
+  xml-v11 directory (Honeycomb and beyond). This one specifies that
+  the syncadapter does not support uploading (and thus the contacts
+  associated with this syncadapter are not editable).  The SDK 11
+  version of the file specifies that the adapter DOES support
+  uploading, so the contacts on SDK 11 and greater are editable.
+-->
 
 <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"
+    android:userVisible="true"
 />
diff --git a/samples/SampleSyncAdapter/samplesyncadapter_server/app.yaml b/samples/SampleSyncAdapter/samplesyncadapter_server/app.yaml
index 109eff3..8526177 100644
--- a/samples/SampleSyncAdapter/samplesyncadapter_server/app.yaml
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/app.yaml
@@ -1,44 +1,59 @@
-application: samplesyncadapter
+# 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.
+
+application: samplesyncadapter2
 version: 1
 runtime: python
 api_version: 1
 
 handlers:
+
+#
+# Define a handler for our static files (css, images, etc)
+#
+- url: /static
+  static_dir: static
+
+#
+# Route all "web services" requests to the main.py file
+#
 - url: /auth
-  script: main.py
+  script: web_services.py
 
-- url: /login
-  script: main.py
+- url: /sync
+  script: web_services.py
 
-- url: /fetch_friend_updates
-  script: main.py
-
-- url: /fetch_status
-  script: main.py
-
-- url: /add_user
+- url: /reset_database
+  script: web_services.py
+  
+#
+# Route all page requests to the dashboard.py file
+#
+- url: /
   script: dashboard.py
 
-- url: /edit_user
+- url: /add_contact
   script: dashboard.py
 
-- url: /users
+- url: /edit_contact
   script: dashboard.py
 
-- url: /delete_friend
+- url: /delete_contact
   script: dashboard.py
 
-- url: /edit_user
+- url: /avatar
   script: dashboard.py
 
-- url: /add_credentials
-  script: dashboard.py
-
-- url: /user_credentials
-  script: dashboard.py
-
-- url: /add_friend
-  script: dashboard.py
-
-- url: /user_friends
+- url: /edit_avatar
   script: dashboard.py
diff --git a/samples/SampleSyncAdapter/samplesyncadapter_server/cron.yaml b/samples/SampleSyncAdapter/samplesyncadapter_server/cron.yaml
new file mode 100644
index 0000000..1a0badb
--- /dev/null
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/cron.yaml
@@ -0,0 +1,24 @@
+# Copyright (C) 2011 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+cron:
+#
+# Create a weekly cron job that cleans up the SampleSyncAdapter server database.
+# We remove all existing contacts from the db, and create three initial
+# contacts: Romeo, Juliet, and Tybalt.
+#
+- description: weekly cleanup job
+  url: /reset_database
+  schedule: every sunday 00:00
+  timezone: America/Los_Angeles
\ No newline at end of file
diff --git a/samples/SampleSyncAdapter/samplesyncadapter_server/dashboard.py b/samples/SampleSyncAdapter/samplesyncadapter_server/dashboard.py
index c986f7e..b9bac5e 100644
--- a/samples/SampleSyncAdapter/samplesyncadapter_server/dashboard.py
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/dashboard.py
@@ -1,21 +1,23 @@
 #!/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."""
+"""
+Defines Django forms for inserting/updating/viewing contact data
+to/from SampleSyncAdapter datastore.
+"""
 
 import cgi
 import datetime
@@ -26,248 +28,181 @@
 from google.appengine.ext.webapp import template
 from google.appengine.ext.db import djangoforms
 from model import datastore
+from google.appengine.api import images
 
 import wsgiref.handlers
 
+class BaseRequestHandler(webapp.RequestHandler):
+    """
+    Base class for our page-based request handlers that contains
+    some helper functions we use in most pages.
+    """
 
-class UserForm(djangoforms.ModelForm):
-  """Represents django form for entering user info."""
+    """
+    Return a form (potentially partially filled-in) to
+    the user.
+    """
+    def send_form(self, title, action, contactId, handle, content_obj):
+        if (contactId >= 0):
+            idInfo = '<input type="hidden" name="_id" value="%s">'
+        else:
+            idInfo = ''
 
-  class Meta:
-    model = datastore.User
+        template_values = {
+                'title': title,
+                'header': title,
+                'action': action,
+                'contactId': contactId,
+                'handle': handle,
+                'has_contactId': (contactId >= 0),
+                'has_handle': (handle != None),
+                'form_data_rows': str(content_obj)
+                }
+
+        path = os.path.join(os.path.dirname(__file__), 'templates', 'simple_form.html')
+        self.response.out.write(template.render(path, template_values))
+
+class ContactForm(djangoforms.ModelForm):
+    """Represents django form for entering contact info."""
+
+    class Meta:
+        model = datastore.Contact
 
 
-class UserInsertPage(webapp.RequestHandler):
-  """Inserts new users. GET presents a blank form. POST processes it."""
+class ContactInsertPage(BaseRequestHandler):
+    """
+    Processes requests to add a new contact. GET presents an empty
+    contact form for the user to fill in.  POST saves the new contact
+    with the POSTed information.
+    """
 
-  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 get(self):
+        self.send_form('Add Contact', '/add_contact', -1, None, ContactForm())
 
-  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>')
+    def post(self):
+        data = ContactForm(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('/')
+        else:
+            # Reprint the form
+            self.send_form('Add Contact', '/add_contact', -1, None, data)
 
 
-class UserEditPage(webapp.RequestHandler):
-  """Edits users. GET presents a form prefilled with user info
-     from datastore. POST processes it."""
+class ContactEditPage(BaseRequestHandler):
+    """
+    Process requests to edit a contact's information.  GET presents a form
+    with the current contact information filled in. POST saves new information
+    into the contact record.
+    """
 
-  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 get(self):
+        id = int(self.request.get('id'))
+        contact = datastore.Contact.get(db.Key.from_path('Contact', id))
+        self.send_form('Edit Contact', '/edit_contact', id, contact.handle, 
+                       ContactForm(instance=contact))
 
-  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)
+    def post(self):
+        id = int(self.request.get('id'))
+        contact = datastore.Contact.get(db.Key.from_path('Contact', id))
+        data = ContactForm(data=self.request.POST, instance=contact)
+        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('/')
+        else:
+            # Reprint the form
+            self.send_form('Edit Contact', '/edit_contact', id, contact.handle, data)
 
+class ContactDeletePage(BaseRequestHandler):
+    """Processes delete contact request."""
 
-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):
+        id = int(self.request.get('id'))
+        contact = datastore.Contact.get(db.Key.from_path('Contact', id))
+        contact.deleted = True
+        contact.updated = datetime.datetime.utcnow()
+        contact.put()
 
-  def get(self):
-    users = datastore.User.all()
-    template_values = {
-        'users': users
-        }
+        self.redirect('/')
 
-    path = os.path.join(os.path.dirname(__file__), 'templates', 'users.html')
-    self.response.out.write(template.render(path, template_values))
+class AvatarEditPage(webapp.RequestHandler):
+    """
+    Processes requests to edit contact's avatar. GET is used to fetch
+    a page that displays the contact's current avatar and allows the user 
+    to specify a file containing a new avatar image.  POST is used to
+    submit the form which will change the contact's avatar.
+    """
 
+    def get(self):
+        id = int(self.request.get('id'))
+        contact = datastore.Contact.get(db.Key.from_path('Contact', id))
+        template_values = {
+                'avatar': contact.avatar,
+                'contactId': id
+                }
+        
+        path = os.path.join(os.path.dirname(__file__), 'templates', 'edit_avatar.html')
+        self.response.out.write(template.render(path, template_values))
 
-class UserCredentialsForm(djangoforms.ModelForm):
-  """Represents django form for entering user's credentials."""
+    def post(self):
+        id = int(self.request.get('id'))
+        contact = datastore.Contact.get(db.Key.from_path('Contact', id))
+        #avatar = images.resize(self.request.get("avatar"), 128, 128)
+        avatar = self.request.get("avatar")
+        contact.avatar = db.Blob(avatar)
+        contact.updated = datetime.datetime.utcnow()
+        contact.put()
+        self.redirect('/')
 
-  class Meta:
-    model = datastore.UserCredentials
+class AvatarViewPage(BaseRequestHandler):
+    """
+    Processes request to view contact's avatar. This is different from
+    the GET AvatarEditPage request in that this doesn't return a page -
+    it just returns the raw image itself.
+    """
 
+    def get(self):
+        id = int(self.request.get('id'))
+        contact = datastore.Contact.get(db.Key.from_path('Contact', id))
+        if (contact.avatar):
+            self.response.headers['Content-Type'] = "image/png"
+            self.response.out.write(contact.avatar)
+        else:
+            self.redirect(self.request.host_url + '/static/img/default_avatar.gif')
 
-class UserCredentialsInsertPage(webapp.RequestHandler):
-  """Inserts user credentials. GET shows a blank form, POST processes it."""
+class ContactsListPage(webapp.RequestHandler):
+    """
+    Display a page that lists all the contacts associated with
+    the specifies user account.
+    """
 
-  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 get(self):
+        contacts = datastore.Contact.all()
+        template_values = {
+                'contacts': contacts,
+                'username': 'user'
+                }
 
-  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)
+        path = os.path.join(os.path.dirname(__file__), 'templates', 'contacts.html')
+        self.response.out.write(template.render(path, template_values))
 
 
 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)
+    application = webapp.WSGIApplication(
+        [('/', ContactsListPage),
+         ('/add_contact', ContactInsertPage),
+         ('/edit_contact', ContactEditPage),
+         ('/delete_contact', ContactDeletePage),
+         ('/avatar', AvatarViewPage),
+         ('/edit_avatar', AvatarEditPage)
+        ],
+        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
index 83ceea0..6f02db5 100644
--- a/samples/SampleSyncAdapter/samplesyncadapter_server/index.yaml
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/index.yaml
@@ -1,14 +1,15 @@
-indexes:
+# 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.
 
-# 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
+# AUTOGENERATED
diff --git a/samples/SampleSyncAdapter/samplesyncadapter_server/main.py b/samples/SampleSyncAdapter/samplesyncadapter_server/main.py
deleted file mode 100644
index 2d7c5c7..0000000
--- a/samples/SampleSyncAdapter/samplesyncadapter_server/main.py
+++ /dev/null
@@ -1,173 +0,0 @@
-#!/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/datastore.py b/samples/SampleSyncAdapter/samplesyncadapter_server/model/datastore.py
index 71bd18a..1f91633 100644
--- a/samples/SampleSyncAdapter/samplesyncadapter_server/model/datastore.py
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/model/datastore.py
@@ -14,80 +14,51 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
-"""Represents user's contact information, friends and credentials."""
+"""Represents user's contact information"""
 
 from google.appengine.ext import db
 
 
-class User(db.Model):
+class Contact(db.Model):
   """Data model class to hold user objects."""
 
   handle = db.StringProperty(required=True)
-  firstname = db.TextProperty()
-  lastname = db.TextProperty()
-  status = db.TextProperty()
+  firstname = db.StringProperty()
+  lastname = db.StringProperty()
   phone_home = db.PhoneNumberProperty()
   phone_office = db.PhoneNumberProperty()
   phone_mobile = db.PhoneNumberProperty()
   email = db.EmailProperty()
+  status = db.TextProperty()
+  avatar = db.BlobProperty()
   deleted = db.BooleanProperty()
   updated = db.DateTimeProperty(auto_now_add=True)
 
   @classmethod
-  def get_user_info(cls, username):
+  def get_contact_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):
+  def get_contact_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):
+  def get_contact_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):
+  def get_contact_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/static/css/main.css b/samples/SampleSyncAdapter/samplesyncadapter_server/static/css/main.css
new file mode 100644
index 0000000..eab304d
--- /dev/null
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/static/css/main.css
@@ -0,0 +1,77 @@
+/**
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+*/
+
+html,body {
+    height: 100%;
+}
+
+body {
+    padding: 20px;
+    margin: 0;
+    background-color: #fff;
+    font-family: Verdana, Arial, Helvetica, sans-serif;
+    text-align: left;
+}
+
+a {
+    color: #0033cc;
+}
+
+h1 {
+    font-family: Arial;
+    font-weight: normal;
+    border-bottom: solid 1px #ccc;
+    margin: 0 0 20px 0;
+    padding: 0 0 5px 0;
+}
+
+h3 {
+    font-family: Arial;
+    font-weight: bold;
+    line-height: 2em;
+}
+
+table {
+    border-collapse: collapse;
+    margin-bottom: 20px;
+}
+
+th,td {
+    padding: 5px 8px;
+    text-align: left;
+}
+
+td.center {
+    text-align: center;
+}
+
+.deleted td {
+    text-decoration: line-through;
+}
+
+.data th {
+    font-weight: normal;
+    border-bottom: solid 1px #000;
+}
+
+.data td {
+    border-bottom: solid 1px #eee;
+}
+
+.form th {
+    font-weight: normal;
+    text-align: right;
+}
\ No newline at end of file
diff --git a/samples/SampleSyncAdapter/samplesyncadapter_server/static/img/default_avatar.gif b/samples/SampleSyncAdapter/samplesyncadapter_server/static/img/default_avatar.gif
new file mode 100644
index 0000000..5089e95
--- /dev/null
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/static/img/default_avatar.gif
Binary files differ
diff --git a/samples/SampleSyncAdapter/samplesyncadapter_server/templates/contacts.html b/samples/SampleSyncAdapter/samplesyncadapter_server/templates/contacts.html
new file mode 100644
index 0000000..b668d33
--- /dev/null
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/templates/contacts.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
+<!--
+ * 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.
+-->
+    <head>
+        <title>SampleSync: Contacts for '{{ username }}'</title>
+        <link type="text/css" rel="stylesheet" href="/static/css/main.css" media="screen" />
+    </head>
+    <body>
+        <h1>SampleSync: Contacts for '{{ username }}'</h1>
+        <table class="data" cellpadding="0" cellspacing="0">
+        <tr>
+            <th>&nbsp;</th>
+            <th>Id</th>
+            <th>Name</th>
+            <th>Email</th>
+            <th>Home</th>
+            <th>Office</th>
+            <th>Mobile</th>
+            <th>Status</th>
+        </tr>
+        {% for contact in contacts %}
+        <tr {% if contact.deleted %} class="deleted" {% endif %}>
+            <td class="center">
+            <a href="/edit_avatar?id={{ contact.key.id }}"><img src="/avatar?id={{ contact.key.id }}" height="25" width="25" /></a>
+            </td>
+            <td><a href="/edit_contact?id={{ contact.key.id }}">{{ contact.key.id }}</a></td>
+            <td><a href="/edit_contact?id={{ contact.key.id }}">{{ contact.firstname }} {{ contact.lastname }}</a></td>
+            <td>{{ contact.email }}</td>
+            <td>{{ contact.phone_home }}</td>
+            <td>{{ contact.phone_office }}</td>
+            <td>{{ contact.phone_mobile }}</td>
+            <td><span style="whitespace: no-wrap;">{{ contact.status }}</span></td>
+        </tr>
+        {% endfor %}
+        </table>
+
+        <a href = "/add_contact">Add Contact</a>
+    </body>
+</html>
\ No newline at end of file
diff --git a/samples/SampleSyncAdapter/samplesyncadapter_server/templates/edit_avatar.html b/samples/SampleSyncAdapter/samplesyncadapter_server/templates/edit_avatar.html
new file mode 100644
index 0000000..49cd3d3
--- /dev/null
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/templates/edit_avatar.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
+<!--
+ * 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.
+-->
+    <head>
+        <title>SampleSync: Edit Picture</title>
+        <link type="text/css" rel="stylesheet" href="/static/css/main.css" media="screen" />
+    </head>
+    <body>
+        <h1>SampleSync: Edit Picture</h1>
+        <form method="POST" action="/edit_avatar" enctype="multipart/form-data">
+            <h3>Current Avatar:</h3>
+            <blockquote>
+                {% if avatar %}
+                <img src="/avatar?id={{ contactId }}" />
+                {% else %}
+                <i>You haven't added a picture for this friend...</i>
+                {% endif %}
+            </blockquote>
+            <h3>New Avatar:</h3>
+            <p>Please select a file containing the image you'd like to use for this friend</p>
+            <input type="file" name="avatar" />
+            <p>&nbsp;</p>
+            <input type="submit" name="Save" value="Save Changes" />
+            <input type="button" name="Cancel" value="Cancel" onclick="document.location='/';return false;" />
+            <input type="hidden" name="id" value="{{ contactId }}" />
+        </form>
+    </body>
+</html>
\ No newline at end of file
diff --git a/samples/SampleSyncAdapter/samplesyncadapter_server/templates/simple_form.html b/samples/SampleSyncAdapter/samplesyncadapter_server/templates/simple_form.html
new file mode 100644
index 0000000..1d72819
--- /dev/null
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/templates/simple_form.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
+<!--
+ * 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.
+-->
+    <head>
+        <title>SampleSync: {{ title }}</title>
+        <link type="text/css" rel="stylesheet" href="/static/css/main.css" media="screen" />
+    </head>
+    <body>
+        <h1>SampleSync: {{ header }}</h1>
+        <form method="POST" action="{{ action }}">
+            <table class="form" cellpadding="0" cellspacing="0">
+              {{ form_data_rows }}
+            </table>
+            <input type="submit" name="Save" value="Save Changes" />
+            <input type="button" name="Cancel" value="Cancel" onclick="document.location='/';return false;" />
+            {% if has_contactId %}
+            <input type="hidden" name="id" value="{{ contactId }}" />
+            {% endif %}
+            {% if has_handle %}
+            <input type="hidden" name="username" value="{{ handle }}" />
+            {% endif %}
+        </form>
+    </body>
+</html>
diff --git a/samples/SampleSyncAdapter/samplesyncadapter_server/templates/users.html b/samples/SampleSyncAdapter/samplesyncadapter_server/templates/users.html
deleted file mode 100644
index 044c352..0000000
--- a/samples/SampleSyncAdapter/samplesyncadapter_server/templates/users.html
+++ /dev/null
@@ -1,19 +0,0 @@
-<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
deleted file mode 100644
index d4ef892..0000000
--- a/samples/SampleSyncAdapter/samplesyncadapter_server/templates/view_friends.html
+++ /dev/null
@@ -1,17 +0,0 @@
-<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/samplesyncadapter_server/web_services.py b/samples/SampleSyncAdapter/samplesyncadapter_server/web_services.py
new file mode 100644
index 0000000..3028add
--- /dev/null
+++ b/samples/SampleSyncAdapter/samplesyncadapter_server/web_services.py
@@ -0,0 +1,400 @@
+#!/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.
+
+"""
+
+import cgi
+import logging
+import time as _time
+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 BaseWebServiceHandler(webapp.RequestHandler):
+    """
+    Base class for our web services. We put some common helper
+    functions here.
+    """
+
+    """
+    Since we're only simulating a single user account, declare our
+    hard-coded credentials here, so that they're easy to see/find.
+    We actually accept any and all usernames that start with this
+    hard-coded values. So if ACCT_USER_NAME is 'user', then we'll
+    accept 'user', 'user75', 'userbuddy', etc, all as legal account
+    usernames.
+    """
+    ACCT_USER_NAME  = 'user'
+    ACCT_PASSWORD   = 'test'
+    ACCT_AUTH_TOKEN = 'xyzzy'
+
+    DATE_TIME_FORMAT = '%Y/%m/%d %H:%M'
+
+    """
+    Process a request to authenticate a client.  We assume that the username
+    and password will be included in the request. If successful, we'll return
+    an authtoken as the only content.  If auth fails, we'll send an "invalid
+    credentials" error.
+    We return a boolean indicating whether we were successful (true) or not (false).
+    In the event that this call fails, we will setup the response, so callers just
+    need to RETURN in the error case.
+    """
+    def authenticate(self):
+        self.username = self.request.get('username')
+        self.password = self.request.get('password')
+
+        logging.info('Authenticatng username: ' + self.username)
+
+        if ((self.username != None) and
+                (self.username.startswith(BaseWebServiceHandler.ACCT_USER_NAME)) and
+                (self.password == BaseWebServiceHandler.ACCT_PASSWORD)):
+            # Authentication was successful - return our hard-coded
+            # auth-token as the only response.
+            self.response.set_status(200, 'OK')
+            self.response.out.write(BaseWebServiceHandler.ACCT_AUTH_TOKEN)
+            return True
+        else:
+            # Authentication failed. Return the standard HTTP auth failure
+            # response to let the client know.
+            self.response.set_status(401, 'Invalid Credentials')
+            return False
+
+    """
+    Validate the credentials of the client for a web service request.
+    The request should include username/password parameters that correspond
+    to our hard-coded single account values.
+    We return a boolean indicating whether we were successful (true) or not (false).
+    In the event that this call fails, we will setup the response, so callers just
+    need to RETURN in the error case.
+    """
+    def validate(self):
+        self.username = self.request.get('username')
+        self.authtoken = self.request.get('authtoken')
+
+        logging.info('Validating username: ' + self.username)
+
+        if ((self.username != None) and
+                (self.username.startswith(BaseWebServiceHandler.ACCT_USER_NAME)) and
+                (self.authtoken == BaseWebServiceHandler.ACCT_AUTH_TOKEN)):
+            return True
+        else:
+            self.response.set_status(401, 'Invalid Credentials')
+            return False
+
+
+class Authenticate(BaseWebServiceHandler):
+    """
+    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.authenticate()
+
+    def get(self):
+        """Used for debugging in a browser..."""
+        self.post()
+
+
+class SyncContacts(BaseWebServiceHandler):
+    """Handles requests for fetching user's contacts.
+
+    UpdateHandler only accepts post events. It expects each
+    request to include username and authtoken. If the authtoken is valid
+    it returns user's contact info in JSON format.
+    """
+
+    def get(self):
+        """Used for debugging in a browser..."""
+        self.post()
+
+    def post(self):
+        logging.info('*** Starting contact sync ***')
+        if (not self.validate()):
+            return
+
+        updated_contacts = []
+
+        # Process any client-side changes sent up in the request.
+        # Any new contacts that were added are included in the
+        # updated_contacts list, so that we return them to the
+        # client. That way, the client can see the serverId of
+        # the newly added contact.
+        client_buffer = self.request.get('contacts')
+        if ((client_buffer != None) and (client_buffer != '')):
+            self.process_client_changes(client_buffer, updated_contacts)
+
+        # Add any contacts that have been updated on the server-side
+        # since the last sync by this client.
+        client_state = self.request.get('syncstate')
+        self.get_updated_contacts(client_state, updated_contacts)
+
+        logging.info('Returning ' + str(len(updated_contacts)) + ' contact records')
+
+        # Return the list of updated contacts to the client
+        self.response.set_status(200)
+        self.response.out.write(toJSON(updated_contacts))
+
+    def get_updated_contacts(self, client_state, updated_contacts):
+        logging.info('* Processing server changes')
+        timestamp = None
+
+        base_url = self.request.host_url
+
+        # The client sends the last high-water-mark that they successfully
+        # sync'd to in the syncstate parameter.  It's opaque to them, but
+        # its actually a seconds-in-unix-epoch timestamp that we use
+        # as a baseline.
+        if client_state:
+            logging.info('Client sync state: ' + client_state)
+            timestamp = datetime.utcfromtimestamp(float(client_state))
+
+        # Keep track of the update/delete counts, so we can log it
+        # below.  Makes debugging easier...
+        update_count = 0
+        delete_count = 0
+
+        contacts = datastore.Contact.all()
+        if contacts:
+            # Find the high-water mark for the most recently updated friend.
+            # We'll return this as the syncstate (x) value for all the friends
+            # we return from this function.
+            high_water_date = datetime.min
+            for contact in contacts:
+                if (contact.updated > high_water_date):
+                    high_water_date = contact.updated
+            high_water_mark = str(long(_time.mktime(high_water_date.utctimetuple())) + 1)
+            logging.info('New sync state: ' + high_water_mark)
+
+            # Now build the updated_contacts containing all the friends that have been
+            # changed since the last sync
+            for contact in contacts:
+                # If our list of contacts we're returning already contains this
+                # contact (for example, it's a contact just uploaded from the client)
+                # then don't bother processing it any further...
+                if (self.list_contains_contact(updated_contacts, contact)):
+                    continue
+
+                handle = contact.handle
+
+                if timestamp is None or contact.updated > timestamp:
+                    if contact.deleted == True:
+                        delete_count = delete_count + 1
+                        DeletedContactData(updated_contacts, handle, high_water_mark)
+                    else:
+                        update_count = update_count + 1
+                        UpdatedContactData(updated_contacts, handle, None, base_url, high_water_mark)
+
+        logging.info('Server-side updates: ' + str(update_count))
+        logging.info('Server-side deletes: ' + str(delete_count))
+
+    def process_client_changes(self, contacts_buffer, updated_contacts):
+        logging.info('* Processing client changes: ' + self.username)
+
+        base_url = self.request.host_url
+
+        # Build an array of generic objects containing contact data,
+        # using the Django built-in JSON parser
+        logging.info('Uploaded contacts buffer: ' + contacts_buffer)
+        json_list = simplejson.loads(contacts_buffer)
+        logging.info('Client-side updates: ' + str(len(json_list)))
+
+        # Keep track of the number of new contacts the client sent to us,
+        # so that we can log it below.
+        new_contact_count = 0
+
+        for jcontact in json_list:
+            new_contact = False
+            id = self.safe_attr(jcontact, 'i')
+            if (id != None):
+                logging.info('Updating contact: ' + str(id))
+                contact = datastore.Contact.get(db.Key.from_path('Contact', id))
+            else:
+                logging.info('Creating new contact record')
+                new_contact = True
+                contact = datastore.Contact(handle='temp')
+
+            # If the 'change' for this contact is that they were deleted
+            # on the client-side, all we want to do is set the deleted
+            # flag here, and we're done.
+            if (self.safe_attr(jcontact, 'd') == True):
+                contact.deleted = True
+                contact.put()
+                logging.info('Deleted contact: ' + contact.handle)
+                continue
+
+            contact.firstname = self.safe_attr(jcontact, 'f')
+            contact.lastname = self.safe_attr(jcontact, 'l')
+            contact.phone_home = self.safe_attr(jcontact, 'h')
+            contact.phone_office = self.safe_attr(jcontact, 'o')
+            contact.phone_mobile = self.safe_attr(jcontact, 'm')
+            contact.email = self.safe_attr(jcontact, 'e')
+            contact.deleted = (self.safe_attr(jcontact, 'd') == 'true')
+            if (new_contact):
+                # New record - add them to db...
+                new_contact_count = new_contact_count + 1
+                contact.handle = contact.firstname + '_' + contact.lastname
+                logging.info('Created new contact handle: ' + contact.handle)
+            contact.put()
+            logging.info('Saved contact: ' + contact.handle)
+
+            # We don't save off the client_id value (thus we add it after
+            # the "put"), but we want it to be in the JSON object we
+            # serialize out, so that the client can match this contact
+            # up with the client version.
+            client_id = self.safe_attr(jcontact, 'c')
+
+            # Create a high-water-mark for sync-state from the 'updated' time
+            # for this contact, so we return the correct value to the client.
+            high_water = str(long(_time.mktime(contact.updated.utctimetuple())) + 1)
+
+            # Add new contacts to our updated_contacts, so that we return them
+            # to the client (so the client gets the serverId for the
+            # added contact)
+            if (new_contact):
+                UpdatedContactData(updated_contacts, contact.handle, client_id, base_url,
+                        high_water)
+
+        logging.info('Client-side adds: ' + str(new_contact_count))
+
+    def list_contains_contact(self, contact_list, contact):
+        if (contact is None):
+            return False
+        contact_id = str(contact.key().id())
+        for next in contact_list:
+            if ((next != None) and (next['i'] == contact_id)):
+                return True
+        return False
+
+    def safe_attr(self, obj, attr_name):
+        if attr_name in obj:
+            return obj[attr_name]
+        return None
+
+class ResetDatabase(BaseWebServiceHandler):
+    """
+    Handles cron request to reset the contact database.
+
+    We have a weekly cron task that resets the database back to a
+    few contacts, so that it doesn't grow to an absurd size.
+    """
+
+    def get(self):
+        # Delete all the existing contacts from the database
+        contacts = datastore.Contact.all()
+        for contact in contacts:
+            contact.delete()
+
+        # Now create three sample contacts
+        contact1 = datastore.Contact(handle = 'juliet',
+                firstname = 'Juliet',
+                lastname = 'Capulet',
+                phone_mobile = '(650) 555-1000',
+                phone_home = '(650) 555-1001',
+                status = 'Wherefore art thou Romeo?')
+        contact1.put()
+
+        contact2 = datastore.Contact(handle = 'romeo',
+                firstname = 'Romeo',
+                lastname = 'Montague',
+                phone_mobile = '(650) 555-2000',
+                phone_home = '(650) 555-2001',
+                status = 'I dream\'d a dream to-night')
+        contact2.put()
+
+        contact3 = datastore.Contact(handle = 'tybalt',
+                firstname = 'Tybalt',
+                lastname = 'Capulet',
+                phone_mobile = '(650) 555-3000',
+                phone_home = '(650) 555-3001',
+                status = 'Have at thee, coward')
+        contact3.put()
+
+
+
+
+def toJSON(object):
+    """Dumps the data represented by the object to JSON for wire transfer."""
+    return simplejson.dumps(object)
+
+class UpdatedContactData(object):
+    """Holds data for user's contacts.
+
+    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',
+        'client_id': 'c'
+    }
+
+    def __init__(self, contact_list, username, client_id, host_url, high_water_mark):
+        obj = datastore.Contact.get_contact_info(username)
+        contact = {}
+        for obj_name, json_name in self.__FIELD_MAP.items():
+            if hasattr(obj, obj_name):
+              v = getattr(obj, obj_name)
+              if (v != None):
+                  contact[json_name] = str(v)
+              else:
+                  contact[json_name] = None
+        contact['i'] = str(obj.key().id())
+        contact['a'] = host_url + "/avatar?id=" + str(obj.key().id())
+        contact['x'] = high_water_mark
+        if (client_id != None):
+            contact['c'] = str(client_id)
+        contact_list.append(contact)
+
+class DeletedContactData(object):
+    def __init__(self, contact_list, username, high_water_mark):
+        obj = datastore.Contact.get_contact_info(username)
+        contact = {}
+        contact['d'] = 'true'
+        contact['i'] = str(obj.key().id())
+        contact['x'] = high_water_mark
+        contact_list.append(contact)
+
+def main():
+    application = webapp.WSGIApplication(
+            [('/auth', Authenticate),
+             ('/sync', SyncContacts),
+             ('/reset_database', ResetDatabase),
+            ],
+            debug=True)
+    wsgiref.handlers.CGIHandler().run(application)
+
+if __name__ == "__main__":
+    main()
diff --git a/samples/SampleSyncAdapter/src/com/example/android/samplesync/Constants.java b/samples/SampleSyncAdapter/src/com/example/android/samplesync/Constants.java
index 49f92bf..bc09da2 100644
--- a/samples/SampleSyncAdapter/src/com/example/android/samplesync/Constants.java
+++ b/samples/SampleSyncAdapter/src/com/example/android/samplesync/Constants.java
@@ -1,12 +1,12 @@
 /*
  * 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
diff --git a/samples/SampleSyncAdapter/src/com/example/android/samplesync/authenticator/AuthenticationService.java b/samples/SampleSyncAdapter/src/com/example/android/samplesync/authenticator/AuthenticationService.java
index 2c163be..9fc72eb 100644
--- a/samples/SampleSyncAdapter/src/com/example/android/samplesync/authenticator/AuthenticationService.java
+++ b/samples/SampleSyncAdapter/src/com/example/android/samplesync/authenticator/AuthenticationService.java
@@ -1,18 +1,19 @@
 /*
  * 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;
@@ -49,7 +50,7 @@
     public IBinder onBind(Intent intent) {
         if (Log.isLoggable(TAG, Log.VERBOSE)) {
             Log.v(TAG, "getBinder()...  returning the AccountAuthenticator binder for intent "
-                + 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
index 0c79c5e..2eb6ecd 100644
--- a/samples/SampleSyncAdapter/src/com/example/android/samplesync/authenticator/Authenticator.java
+++ b/samples/SampleSyncAdapter/src/com/example/android/samplesync/authenticator/Authenticator.java
@@ -1,38 +1,57 @@
 /*
  * 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 com.example.android.samplesync.Constants;
+import com.example.android.samplesync.client.NetworkUtilities;
+
 import android.accounts.AbstractAccountAuthenticator;
 import android.accounts.Account;
 import android.accounts.AccountAuthenticatorResponse;
 import android.accounts.AccountManager;
+import android.accounts.NetworkErrorException;
 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;
+import android.text.TextUtils;
+import android.util.Log;
 
 /**
  * This class is an implementation of AbstractAccountAuthenticator for
- * authenticating accounts in the com.example.android.samplesync domain.
+ * authenticating accounts in the com.example.android.samplesync domain. The
+ * interesting thing that this class demonstrates is the use of authTokens as
+ * part of the authentication process. In the account setup UI, the user enters
+ * their username and password. But for our subsequent calls off to the service
+ * for syncing, we want to use an authtoken instead - so we're not continually
+ * sending the password over the wire. getAuthToken() will be called when
+ * SyncAdapter calls AccountManager.blockingGetAuthToken(). When we get called,
+ * we need to return the appropriate authToken for the specified account. If we
+ * already have an authToken stored in the account, we return that authToken. If
+ * we don't, but we do have a username and password, then we'll attempt to talk
+ * to the sample service to fetch an authToken. If that fails (or we didn't have
+ * a username/password), then we need to prompt the user - so we create an
+ * AuthenticatorActivity intent and return that. That will display the dialog
+ * that prompts the user for their login information.
  */
 class Authenticator extends AbstractAccountAuthenticator {
 
+    /** The tag used to log to adb console. **/
+    private static final String TAG = "Authenticator";
+
     // Authentication Service context
     private final Context mContext;
 
@@ -43,10 +62,9 @@
 
     @Override
     public Bundle addAccount(AccountAuthenticatorResponse response, String accountType,
-        String authTokenType, String[] requiredFeatures, Bundle options) {
-
+            String authTokenType, String[] requiredFeatures, Bundle options) {
+        Log.v(TAG, "addAccount()");
         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);
@@ -54,54 +72,49 @@
     }
 
     @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_CONFIRM_CREDENTIALS, true);
-        intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
-        final Bundle bundle = new Bundle();
-        bundle.putParcelable(AccountManager.KEY_INTENT, intent);
-        return bundle;
+    public Bundle confirmCredentials(
+            AccountAuthenticatorResponse response, Account account, Bundle options) {
+        Log.v(TAG, "confirmCredentials()");
+        return null;
     }
 
     @Override
     public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
+        Log.v(TAG, "editProperties()");
         throw new UnsupportedOperationException();
     }
 
     @Override
     public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
-        String authTokenType, Bundle loginOptions) {
+            String authTokenType, Bundle loginOptions) throws NetworkErrorException {
+        Log.v(TAG, "getAuthToken()");
 
+        // If the caller requested an authToken type we don't support, then
+        // return an error
         if (!authTokenType.equals(Constants.AUTHTOKEN_TYPE)) {
             final Bundle result = new Bundle();
             result.putString(AccountManager.KEY_ERROR_MESSAGE, "invalid authTokenType");
             return result;
         }
+
+        // Extract the username and password from the Account Manager, and ask
+        // the server for an appropriate AuthToken.
         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 String authToken = NetworkUtilities.authenticate(account.name, password);
+            if (!TextUtils.isEmpty(authToken)) {
                 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);
+                result.putString(AccountManager.KEY_AUTHTOKEN, authToken);
                 return result;
             }
         }
-        // the password was missing or incorrect, return an Intent to an
-        // Activity that will prompt the user for the password.
+
+        // If we get here, then we couldn't access the user's password - so we
+        // need to re-prompt them for their credentials. We do that by creating
+        // an intent to display our AuthenticatorActivity panel.
         final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
         intent.putExtra(AuthenticatorActivity.PARAM_USERNAME, account.name);
         intent.putExtra(AuthenticatorActivity.PARAM_AUTHTOKEN_TYPE, authTokenType);
@@ -113,39 +126,27 @@
 
     @Override
     public String getAuthTokenLabel(String authTokenType) {
-        if (Constants.AUTHTOKEN_TYPE.equals(authTokenType)) {
-            return mContext.getString(R.string.label);
-        }
+        // null means we don't support multiple authToken types
+        Log.v(TAG, "getAuthTokenLabel()");
         return null;
     }
 
     @Override
-    public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account,
-        String[] features) {
-
+    public Bundle hasFeatures(
+            AccountAuthenticatorResponse response, Account account, String[] features) {
+        // This call is used to query whether the Authenticator supports
+        // specific features. We don't expect to get called, so we always
+        // return false (no) for any queries.
+        Log.v(TAG, "hasFeatures()");
         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 */);
-    }
-
     @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_CONFIRM_CREDENTIALS, false);
-        final Bundle bundle = new Bundle();
-        bundle.putParcelable(AccountManager.KEY_INTENT, intent);
-        return bundle;
+            String authTokenType, Bundle loginOptions) {
+        Log.v(TAG, "updateCredentials()");
+        return null;
     }
 }
diff --git a/samples/SampleSyncAdapter/src/com/example/android/samplesync/authenticator/AuthenticatorActivity.java b/samples/SampleSyncAdapter/src/com/example/android/samplesync/authenticator/AuthenticatorActivity.java
index 4e1ee2a..2a3c0fc 100644
--- a/samples/SampleSyncAdapter/src/com/example/android/samplesync/authenticator/AuthenticatorActivity.java
+++ b/samples/SampleSyncAdapter/src/com/example/android/samplesync/authenticator/AuthenticatorActivity.java
@@ -1,20 +1,25 @@
 /*
  * 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 com.example.android.samplesync.Constants;
+import com.example.android.samplesync.R;
+import com.example.android.samplesync.client.NetworkUtilities;
+
 import android.accounts.Account;
 import android.accounts.AccountAuthenticatorActivity;
 import android.accounts.AccountManager;
@@ -23,6 +28,7 @@
 import android.content.ContentResolver;
 import android.content.DialogInterface;
 import android.content.Intent;
+import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.Handler;
 import android.provider.ContactsContract;
@@ -33,41 +39,36 @@
 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 {
-
-    /** The Intent flag to confirm credentials. **/
+    /** The Intent flag to confirm credentials. */
     public static final String PARAM_CONFIRM_CREDENTIALS = "confirmCredentials";
 
-    /** The Intent extra to store password. **/
+    /** The Intent extra to store password. */
     public static final String PARAM_PASSWORD = "password";
 
-    /** The Intent extra to store username. **/
+    /** The Intent extra to store username. */
     public static final String PARAM_USERNAME = "username";
 
-    /** The Intent extra to store authtoken type. **/
+    /** The Intent extra to store username. */
     public static final String PARAM_AUTHTOKEN_TYPE = "authtokenType";
 
-    /** The tag used to log to adb console. **/
+    /** The tag used to log to adb console. */
     private static final String TAG = "AuthenticatorActivity";
-
     private AccountManager mAccountManager;
 
-    private Thread mAuthThread;
+    /** Keep track of the login task so can cancel it if requested */
+    private UserLoginTask mAuthTask = null;
 
-    private String mAuthtoken;
-
-    private String mAuthtokenType;
+    /** Keep track of the progress dialog so we can dismiss it */
+    private ProgressDialog mProgressDialog = null;
 
     /**
      * 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.
+     * doesn't cause the user's password or authToken to be changed on the
+     * device.
      */
     private Boolean mConfirmCredentials = false;
 
@@ -99,14 +100,13 @@
         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_CONFIRM_CREDENTIALS, 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);
+        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);
@@ -118,27 +118,31 @@
      * {@inheritDoc}
      */
     @Override
-    protected Dialog onCreateDialog(int id) {
+    protected Dialog onCreateDialog(int id, Bundle args) {
         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();
+                Log.i(TAG, "user cancelling authentication");
+                if (mAuthTask != null) {
+                    mAuthTask.cancel(true);
                 }
             }
         });
+        // We save off the progress dialog in a field so that we can dismiss
+        // it later. We can't just call dismissDialog(0) because the system
+        // can lose track of our dialog if there's an orientation change.
+        mProgressDialog = dialog;
         return dialog;
     }
 
     /**
      * Handles onClick event on the Submit button. Sends username/password to
-     * the server for authentication.
-     * 
+     * the server for authentication. The button is configured to call
+     * handleLogin() in the layout XML.
+     *
      * @param view The Submit button for which this method is invoked
      */
     public void handleLogin(View view) {
@@ -149,11 +153,11 @@
         if (TextUtils.isEmpty(mUsername) || TextUtils.isEmpty(mPassword)) {
             mMessage.setText(getMessage());
         } else {
+            // Show a progress dialog, and kick off a background task to perform
+            // the user login attempt.
             showProgress();
-            // Start authenticating...
-            mAuthThread =
-                NetworkUtilities.attemptAuth(mUsername, mPassword, mHandler,
-                    AuthenticatorActivity.this);
+            mAuthTask = new UserLoginTask();
+            mAuthTask.execute();
         }
     }
 
@@ -161,8 +165,8 @@
      * 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.
+     *
+     * @param result the confirmCredentials result.
      */
     private void finishConfirmCredentials(boolean result) {
         Log.i(TAG, "finishConfirmCredentials()");
@@ -178,12 +182,13 @@
     /**
      * 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.
+     * AccountAuthenticatorResult which is sent back to the caller. We store the
+     * authToken that's returned from the server as the 'password' for this
+     * account - so we're never storing the user's actual password locally.
+     *
+     * @param result the confirmCredentials result.
      */
-    private void finishLogin() {
+    private void finishLogin(String authToken) {
 
         Log.i(TAG, "finishLogin()");
         final Account account = new Account(mUsername, Constants.ACCOUNT_TYPE);
@@ -195,37 +200,35 @@
             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.
-     */
-    private void hideProgress() {
-        dismissDialog(0);
-    }
-
-    /**
      * Called when the authentication process completes (see attemptLogin()).
+     *
+     * @param authToken the authentication token returned by the server, or NULL if
+     *            authentication failed.
      */
-    public void onAuthenticationResult(boolean result) {
+    public void onAuthenticationResult(String authToken) {
 
-        Log.i(TAG, "onAuthenticationResult(" + result + ")");
+        boolean success = ((authToken != null) && (authToken.length() > 0));
+        Log.i(TAG, "onAuthenticationResult(" + success + ")");
+
+        // Our task is complete, so clear it out
+        mAuthTask = null;
+
         // Hide the progress dialog
         hideProgress();
-        if (result) {
+
+        if (success) {
             if (!mConfirmCredentials) {
-                finishLogin();
+                finishLogin(authToken);
             } else {
-                finishConfirmCredentials(true);
+                finishConfirmCredentials(success);
             }
         } else {
             Log.e(TAG, "onAuthenticationResult: failed to authenticate");
@@ -241,6 +244,16 @@
         }
     }
 
+    public void onAuthenticationCancel() {
+        Log.i(TAG, "onAuthenticationCancel()");
+
+        // Our task is complete, so clear it out
+        mAuthTask = null;
+
+        // Hide the progress dialog
+        hideProgress();
+    }
+
     /**
      * Returns the message to be displayed at the top of the login dialog box.
      */
@@ -265,4 +278,49 @@
     private void showProgress() {
         showDialog(0);
     }
+
+    /**
+     * Hides the progress UI for a lengthy operation.
+     */
+    private void hideProgress() {
+        if (mProgressDialog != null) {
+            mProgressDialog.dismiss();
+            mProgressDialog = null;
+        }
+    }
+
+    /**
+     * Represents an asynchronous task used to authenticate a user against the
+     * SampleSync Service
+     */
+    public class UserLoginTask extends AsyncTask<Void, Void, String> {
+
+        @Override
+        protected String doInBackground(Void... params) {
+            // We do the actual work of authenticating the user
+            // in the NetworkUtilities class.
+            try {
+                return NetworkUtilities.authenticate(mUsername, mPassword);
+            } catch (Exception ex) {
+                Log.e(TAG, "UserLoginTask.doInBackground: failed to authenticate");
+                Log.i(TAG, ex.toString());
+                return null;
+            }
+        }
+
+        @Override
+        protected void onPostExecute(final String authToken) {
+            // On a successful authentication, call back into the Activity to
+            // communicate the authToken (or null for an error).
+            onAuthenticationResult(authToken);
+        }
+
+        @Override
+        protected void onCancelled() {
+            // If the action was canceled (by the user clicking the cancel
+            // button in the progress dialog), then call back into the
+            // activity to let it know.
+            onAuthenticationCancel();
+        }
+    }
 }
diff --git a/samples/SampleSyncAdapter/src/com/example/android/samplesync/client/NetworkUtilities.java b/samples/SampleSyncAdapter/src/com/example/android/samplesync/client/NetworkUtilities.java
index 7824a4d..bebcd72 100644
--- a/samples/SampleSyncAdapter/src/com/example/android/samplesync/client/NetworkUtilities.java
+++ b/samples/SampleSyncAdapter/src/com/example/android/samplesync/client/NetworkUtilities.java
@@ -1,23 +1,27 @@
 /*
  * 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.graphics.Bitmap;
+import android.graphics.BitmapFactory;
 import android.os.Handler;
+import android.text.TextUtils;
 import android.util.Log;
 
 import com.example.android.samplesync.authenticator.AuthenticatorActivity;
@@ -37,11 +41,19 @@
 import org.apache.http.params.HttpConnectionParams;
 import org.apache.http.params.HttpParams;
 import org.apache.http.util.EntityUtils;
+import org.json.JSONObject;
 import org.json.JSONArray;
 import org.json.JSONException;
 
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Date;
@@ -52,31 +64,26 @@
  * Provides utility methods for communicating with the server.
  */
 final public class NetworkUtilities {
-
-    /** The tag used to log to adb console. **/
+    /** The tag used to log to adb console. */
     private static final String TAG = "NetworkUtilities";
-
-    /** The Intent extra to store password. **/
-    public static final String PARAM_PASSWORD = "password";
-
-    /** The Intent extra to store username. **/
+    /** POST parameter name for the user's account name */
     public static final String PARAM_USERNAME = "username";
-
-    public static final String PARAM_UPDATED = "timestamp";
-
-    public static final String USER_AGENT = "AuthenticationService/1.0";
-
-    public static final int REGISTRATION_TIMEOUT_MS = 30 * 1000; // ms
-
-    public static final String BASE_URL = "https://samplesyncadapter.appspot.com";
-
+    /** POST parameter name for the user's password */
+    public static final String PARAM_PASSWORD = "password";
+    /** POST parameter name for the user's authentication token */
+    public static final String PARAM_AUTH_TOKEN = "authtoken";
+    /** POST parameter name for the client's last-known sync state */
+    public static final String PARAM_SYNC_STATE = "syncstate";
+    /** POST parameter name for the sending client-edited contact info */
+    public static final String PARAM_CONTACTS_DATA = "contacts";
+    /** Timeout (in ms) we specify for each http request */
+    public static final int HTTP_REQUEST_TIMEOUT_MS = 30 * 1000;
+    /** Base URL for the v2 Sample Sync Service */
+    public static final String BASE_URL = "https://samplesyncadapter2.appspot.com";
+    /** URI for authentication service */
     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;
+    /** URI for sync service */
+    public static final String SYNC_CONTACTS_URI = BASE_URL + "/sync";
 
     private NetworkUtilities() {
     }
@@ -84,224 +91,188 @@
     /**
      * 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_MS);
-            HttpConnectionParams.setSoTimeout(params, REGISTRATION_TIMEOUT_MS);
-            ConnManagerParams.setTimeout(params, REGISTRATION_TIMEOUT_MS);
-        }
+    public static HttpClient getHttpClient() {
+        HttpClient httpClient = new DefaultHttpClient();
+        final HttpParams params = httpClient.getParams();
+        HttpConnectionParams.setConnectionTimeout(params, HTTP_REQUEST_TIMEOUT_MS);
+        HttpConnectionParams.setSoTimeout(params, HTTP_REQUEST_TIMEOUT_MS);
+        ConnManagerParams.setTimeout(params, HTTP_REQUEST_TIMEOUT_MS);
+        return httpClient;
     }
 
     /**
-     * Executes the network requests on a separate thread.
-     * 
-     * @param runnable The runnable instance containing network mOperations to
-     *        be executed.
+     * Connects to the SampleSync test server, authenticates the provided
+     * username and password.
+     *
+     * @param username The server account username
+     * @param password The server account password
+     * @return String The authentication token returned by the server (or null)
      */
-    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) {
+    public static String authenticate(String username, String password) {
 
         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;
+        final HttpEntity entity;
         try {
             entity = new UrlEncodedFormEntity(params);
         } catch (final UnsupportedEncodingException e) {
             // this should never happen.
-            throw new AssertionError(e);
+            throw new IllegalStateException(e);
         }
+        Log.i(TAG, "Authenticating to: " + AUTH_URI);
         final HttpPost post = new HttpPost(AUTH_URI);
         post.addHeader(entity.getContentType());
         post.setEntity(entity);
-        maybeCreateHttpClient();
         try {
-            resp = mHttpClient.execute(post);
+            resp = getHttpClient().execute(post);
+            String authToken = null;
             if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
-                if (Log.isLoggable(TAG, Log.VERBOSE)) {
-                    Log.v(TAG, "Successful authentication");
+                InputStream istream = (resp.getEntity() != null) ? resp.getEntity().getContent()
+                        : null;
+                if (istream != null) {
+                    BufferedReader ireader = new BufferedReader(new InputStreamReader(istream));
+                    authToken = ireader.readLine().trim();
                 }
-                sendResult(true, handler, context);
-                return true;
+            }
+            if ((authToken != null) && (authToken.length() > 0)) {
+                Log.v(TAG, "Successful authentication");
+                return authToken;
             } else {
-                if (Log.isLoggable(TAG, Log.VERBOSE)) {
-                    Log.v(TAG, "Error authenticating" + resp.getStatusLine());
-                }
-                sendResult(false, handler, context);
-                return false;
+                Log.e(TAG, "Error authenticating" + resp.getStatusLine());
+                return null;
             }
         } catch (final IOException e) {
-            if (Log.isLoggable(TAG, Log.VERBOSE)) {
-                Log.v(TAG, "IOException when getting authtoken", e);
-            }
-            sendResult(false, handler, context);
-            return false;
+            Log.e(TAG, "IOException when getting authtoken", e);
+            return null;
         } finally {
-            if (Log.isLoggable(TAG, Log.VERBOSE)) {
-                Log.v(TAG, "getAuthtoken completing");
-            }
+            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.
+     * Perform 2-way sync with the server-side contacts. We send a request that
+     * includes all the locally-dirty contacts so that the server can process
+     * those changes, and we receive (and return) a list of contacts that were
+     * updated on the server-side that need to be updated locally.
+     *
+     * @param account The account being synced
+     * @param authtoken The authtoken stored in the AccountManager for this
+     *            account
+     * @param serverSyncState A token returned from the server on the last sync
+     * @param dirtyContacts A list of the contacts to send to the server
+     * @return A list of contacts that we need to update locally
      */
-    private static void sendResult(final Boolean result, final Handler handler,
-        final Context context) {
-        if (handler == null || context == null) {
-            return;
+    public static List<RawContact> syncContacts(
+            Account account, String authtoken, long serverSyncState, List<RawContact> dirtyContacts)
+            throws JSONException, ParseException, IOException, AuthenticationException {
+        // Convert our list of User objects into a list of JSONObject
+        List<JSONObject> jsonContacts = new ArrayList<JSONObject>();
+        for (RawContact rawContact : dirtyContacts) {
+            jsonContacts.add(rawContact.toJSONObject());
         }
-        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) {
+        // Create a special JSONArray of our JSON contacts
+        JSONArray buffer = new JSONArray(jsonContacts);
 
-        final Runnable runnable = new Runnable() {
-            public void run() {
-                authenticate(username, password, handler, context);
-            }
-        };
-        // run on background thread.
-        return NetworkUtilities.performOnBackgroundThread(runnable);
-    }
+        // Create an array that will hold the server-side contacts
+        // that have been changed (returned by the server).
+        final ArrayList<RawContact> serverDirtyList = new ArrayList<RawContact>();
 
-    /**
-     * 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>();
+        // Prepare our POST data
         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)));
+        params.add(new BasicNameValuePair(PARAM_AUTH_TOKEN, authtoken));
+        params.add(new BasicNameValuePair(PARAM_CONTACTS_DATA, buffer.toString()));
+        if (serverSyncState > 0) {
+            params.add(new BasicNameValuePair(PARAM_SYNC_STATE, Long.toString(serverSyncState)));
         }
         Log.i(TAG, params.toString());
-        HttpEntity entity = null;
-        entity = new UrlEncodedFormEntity(params);
-        final HttpPost post = new HttpPost(FETCH_FRIEND_UPDATES_URI);
+        HttpEntity entity = new UrlEncodedFormEntity(params);
+
+        // Send the updated friends data to the server
+        Log.i(TAG, "Syncing to: " + SYNC_CONTACTS_URI);
+        final HttpPost post = new HttpPost(SYNC_CONTACTS_URI);
         post.addHeader(entity.getContentType());
         post.setEntity(entity);
-        maybeCreateHttpClient();
-        final HttpResponse resp = mHttpClient.execute(post);
+        final HttpResponse resp = getHttpClient().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);
+            // Our request to the server was successful - so we assume
+            // that they accepted all the changes we sent up, and
+            // that the response includes the contacts that we need
+            // to update on our side...
+            final JSONArray serverContacts = new JSONArray(response);
             Log.d(TAG, response);
-            for (int i = 0; i < friends.length(); i++) {
-                friendList.add(User.valueOf(friends.getJSONObject(i)));
+            for (int i = 0; i < serverContacts.length(); i++) {
+                RawContact rawContact = RawContact.valueOf(serverContacts.getJSONObject(i));
+                if (rawContact != null) {
+                    serverDirtyList.add(rawContact);
+                }
             }
         } else {
             if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
-                Log.e(TAG, "Authentication exception in fetching remote contacts");
+                Log.e(TAG, "Authentication exception in sending dirty contacts");
                 throw new AuthenticationException();
             } else {
-                Log.e(TAG, "Server error in fetching remote contacts: " + resp.getStatusLine());
+                Log.e(TAG, "Server error in sending dirty contacts: " + resp.getStatusLine());
                 throw new IOException();
             }
         }
-        return friendList;
+
+        return serverDirtyList;
     }
 
     /**
-     * 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.
+     * Download the avatar image from the server.
+     *
+     * @param avatarUrl the URL pointing to the avatar image
+     * @return a byte array with the raw JPEG avatar image
      */
-    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();
-            }
+    public static byte[] downloadAvatar(final String avatarUrl) {
+        // If there is no avatar, we're done
+        if (TextUtils.isEmpty(avatarUrl)) {
+            return null;
         }
-        return statusList;
+
+        try {
+            Log.i(TAG, "Downloading avatar: " + avatarUrl);
+            // Request the avatar image from the server, and create a bitmap
+            // object from the stream we get back.
+            URL url = new URL(avatarUrl);
+            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+            connection.connect();
+            try {
+                final BitmapFactory.Options options = new BitmapFactory.Options();
+                final Bitmap avatar = BitmapFactory.decodeStream(connection.getInputStream(),
+                        null, options);
+
+                // Take the image we received from the server, whatever format it
+                // happens to be in, and convert it to a JPEG image. Note: we're
+                // not resizing the avatar - we assume that the image we get from
+                // the server is a reasonable size...
+                Log.i(TAG, "Converting avatar to JPEG");
+                ByteArrayOutputStream convertStream = new ByteArrayOutputStream(
+                        avatar.getWidth() * avatar.getHeight() * 4);
+                avatar.compress(Bitmap.CompressFormat.JPEG, 95, convertStream);
+                convertStream.flush();
+                convertStream.close();
+                // On pre-Honeycomb systems, it's important to call recycle on bitmaps
+                avatar.recycle();
+                return convertStream.toByteArray();
+            } finally {
+                connection.disconnect();
+            }
+        } catch (MalformedURLException muex) {
+            // A bad URL - nothing we can really do about it here...
+            Log.e(TAG, "Malformed avatar URL: " + avatarUrl);
+        } catch (IOException ioex) {
+            // If we're unable to download the avatar, it's a bummer but not the
+            // end of the world. We'll try to get it next time we sync.
+            Log.e(TAG, "Failed to download user avatar: " + avatarUrl);
+        }
+        return null;
     }
+
 }
diff --git a/samples/SampleSyncAdapter/src/com/example/android/samplesync/client/RawContact.java b/samples/SampleSyncAdapter/src/com/example/android/samplesync/client/RawContact.java
new file mode 100644
index 0000000..6aa73d9
--- /dev/null
+++ b/samples/SampleSyncAdapter/src/com/example/android/samplesync/client/RawContact.java
@@ -0,0 +1,260 @@
+/*
+ * 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.text.TextUtils;
+import android.util.Log;
+
+import org.json.JSONObject;
+import org.json.JSONException;
+
+import java.lang.StringBuilder;
+
+/**
+ * Represents a low-level contacts RawContact - or at least
+ * the fields of the RawContact that we care about.
+ */
+final public class RawContact {
+
+    /** The tag used to log to adb console. **/
+    private static final String TAG = "RawContact";
+
+    private final String mUserName;
+
+    private final String mFullName;
+
+    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 String mStatus;
+
+    private final String mAvatarUrl;
+
+    private final boolean mDeleted;
+
+    private final boolean mDirty;
+
+    private final long mServerContactId;
+
+    private final long mRawContactId;
+
+    private final long mSyncState;
+
+    public long getServerContactId() {
+        return mServerContactId;
+    }
+
+    public long getRawContactId() {
+        return mRawContactId;
+    }
+
+    public String getUserName() {
+        return mUserName;
+    }
+
+    public String getFirstName() {
+        return mFirstName;
+    }
+
+    public String getLastName() {
+        return mLastName;
+    }
+
+    public String getFullName() {
+        return mFullName;
+    }
+
+    public String getCellPhone() {
+        return mCellPhone;
+    }
+
+    public String getOfficePhone() {
+        return mOfficePhone;
+    }
+
+    public String getHomePhone() {
+        return mHomePhone;
+    }
+
+    public String getEmail() {
+        return mEmail;
+    }
+
+    public String getStatus() {
+        return mStatus;
+    }
+
+    public String getAvatarUrl() {
+        return mAvatarUrl;
+    }
+
+    public boolean isDeleted() {
+        return mDeleted;
+    }
+
+    public boolean isDirty() {
+        return mDirty;
+    }
+
+    public long getSyncState() {
+        return mSyncState;
+    }
+
+    public String getBestName() {
+        if (!TextUtils.isEmpty(mFullName)) {
+            return mFullName;
+        } else if (TextUtils.isEmpty(mFirstName)) {
+            return mLastName;
+        } else {
+            return mFirstName;
+        }
+    }
+
+    /**
+     * Convert the RawContact object into a JSON string.  From the
+     * JSONString interface.
+     * @return a JSON string representation of the object
+     */
+    public JSONObject toJSONObject() {
+        JSONObject json = new JSONObject();
+
+        try {
+            if (!TextUtils.isEmpty(mFirstName)) {
+                json.put("f", mFirstName);
+            }
+            if (!TextUtils.isEmpty(mLastName)) {
+                json.put("l", mLastName);
+            }
+            if (!TextUtils.isEmpty(mCellPhone)) {
+                json.put("m", mCellPhone);
+            }
+            if (!TextUtils.isEmpty(mOfficePhone)) {
+                json.put("o", mOfficePhone);
+            }
+            if (!TextUtils.isEmpty(mHomePhone)) {
+                json.put("h", mHomePhone);
+            }
+            if (!TextUtils.isEmpty(mEmail)) {
+                json.put("e", mEmail);
+            }
+            if (mServerContactId > 0) {
+                json.put("i", mServerContactId);
+            }
+            if (mRawContactId > 0) {
+                json.put("c", mRawContactId);
+            }
+            if (mDeleted) {
+                json.put("d", mDeleted);
+            }
+        } catch (final Exception ex) {
+            Log.i(TAG, "Error converting RawContact to JSONObject" + ex.toString());
+        }
+
+        return json;
+    }
+
+    public RawContact(String name, String fullName, String firstName, String lastName,
+            String cellPhone, String officePhone, String homePhone, String email,
+            String status, String avatarUrl, boolean deleted, long serverContactId,
+            long rawContactId, long syncState, boolean dirty) {
+        mUserName = name;
+        mFullName = fullName;
+        mFirstName = firstName;
+        mLastName = lastName;
+        mCellPhone = cellPhone;
+        mOfficePhone = officePhone;
+        mHomePhone = homePhone;
+        mEmail = email;
+        mStatus = status;
+        mAvatarUrl = avatarUrl;
+        mDeleted = deleted;
+        mServerContactId = serverContactId;
+        mRawContactId = rawContactId;
+        mSyncState = syncState;
+        mDirty = dirty;
+    }
+
+    /**
+     * Creates and returns an instance of the RawContact from the provided JSON data.
+     *
+     * @param user The JSONObject containing user data
+     * @return user The new instance of Sample RawContact created from the JSON data.
+     */
+    public static RawContact valueOf(JSONObject contact) {
+
+        try {
+            final String userName = !contact.isNull("u") ? contact.getString("u") : null;
+            final int serverContactId = !contact.isNull("i") ? contact.getInt("i") : -1;
+            // If we didn't get either a username or serverId for the contact, then
+            // we can't do anything with it locally...
+            if ((userName == null) && (serverContactId <= 0)) {
+                throw new JSONException("JSON contact missing required 'u' or 'i' fields");
+            }
+
+            final int rawContactId = !contact.isNull("c") ? contact.getInt("c") : -1;
+            final String firstName = !contact.isNull("f")  ? contact.getString("f") : null;
+            final String lastName = !contact.isNull("l") ? contact.getString("l") : null;
+            final String cellPhone = !contact.isNull("m") ? contact.getString("m") : null;
+            final String officePhone = !contact.isNull("o") ? contact.getString("o") : null;
+            final String homePhone = !contact.isNull("h") ? contact.getString("h") : null;
+            final String email = !contact.isNull("e") ? contact.getString("e") : null;
+            final String status = !contact.isNull("s") ? contact.getString("s") : null;
+            final String avatarUrl = !contact.isNull("a") ? contact.getString("a") : null;
+            final boolean deleted = !contact.isNull("d") ? contact.getBoolean("d") : false;
+            final long syncState = !contact.isNull("x") ? contact.getLong("x") : 0;
+            return new RawContact(userName, null, firstName, lastName, cellPhone,
+                    officePhone, homePhone, email, status, avatarUrl, deleted,
+                    serverContactId, rawContactId, syncState, false);
+        } catch (final Exception ex) {
+            Log.i(TAG, "Error parsing JSON contact object" + ex.toString());
+        }
+        return null;
+    }
+
+    /**
+     * Creates and returns RawContact instance from all the supplied parameters.
+     */
+    public static RawContact create(String fullName, String firstName, String lastName,
+            String cellPhone, String officePhone, String homePhone,
+            String email, String status, boolean deleted, long rawContactId,
+            long serverContactId) {
+        return new RawContact(null, fullName, firstName, lastName, cellPhone, officePhone,
+                homePhone, email, status, null, deleted, serverContactId, rawContactId,
+                -1, true);
+    }
+
+    /**
+     * Creates and returns a User instance that represents a deleted user.
+     * Since the user is deleted, all we need are the client/server IDs.
+     * @param clientUserId The client-side ID for the contact
+     * @param serverUserId The server-side ID for the contact
+     * @return a minimal User object representing the deleted contact.
+     */
+    public static RawContact createDeletedContact(long rawContactId, long serverContactId)
+    {
+        return new RawContact(null, null, null, null, null, null, null,
+                null, null, null, true, serverContactId, rawContactId, -1, true);
+    }
+}
diff --git a/samples/SampleSyncAdapter/src/com/example/android/samplesync/client/User.java b/samples/SampleSyncAdapter/src/com/example/android/samplesync/client/User.java
deleted file mode 100644
index 217a383..0000000
--- a/samples/SampleSyncAdapter/src/com/example/android/samplesync/client/User.java
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * 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
- */
-final 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;
-    }
-
-    private 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
-     * 
-     */
-    final 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/editor/ContactEditorActivity.java b/samples/SampleSyncAdapter/src/com/example/android/samplesync/editor/ContactEditorActivity.java
new file mode 100644
index 0000000..2e30be7
--- /dev/null
+++ b/samples/SampleSyncAdapter/src/com/example/android/samplesync/editor/ContactEditorActivity.java
@@ -0,0 +1,369 @@
+/*
+ * 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.editor;
+
+import com.example.android.samplesync.R;
+import com.example.android.samplesync.client.RawContact;
+import com.example.android.samplesync.platform.BatchOperation;
+import com.example.android.samplesync.platform.ContactManager;
+import com.example.android.samplesync.platform.ContactManager.EditorQuery;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.widget.EditText;
+import android.widget.TextView;
+
+/**
+ * Implements a sample editor for a contact that belongs to a remote contact service.
+ * The editor can be invoked for an existing SampleSyncAdapter contact, or it can
+ * be used to create a brand new SampleSyncAdapter contact. We look at the Intent
+ * object to figure out whether this is a "new" or "edit" operation.
+ */
+public class ContactEditorActivity extends Activity {
+    private static final String TAG = "SampleSyncAdapter";
+
+    // Keep track of whether we're inserting a new contact or editing an
+    // existing contact.
+    private boolean mIsInsert;
+
+    // The name of the external account we're syncing this contact to.
+    private String mAccountName;
+
+    // For existing contacts, this is the URI to the contact data.
+    private Uri mRawContactUri;
+
+    // The raw clientId for this contact
+    private long mRawContactId;
+
+    // Make sure we only attempt to save the contact once if the
+    // user presses the "done" button multiple times...
+    private boolean mSaveInProgress = false;
+
+    // Keep track of the controls used to edit contact values, so we can get/set
+    // those values easily.
+    private EditText mNameEditText;
+    private EditText mHomePhoneEditText;
+    private EditText mMobilePhoneEditText;
+    private EditText mWorkPhoneEditText;
+    private EditText mEmailEditText;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.editor);
+
+        mNameEditText = (EditText)findViewById(R.id.editor_name);
+        mHomePhoneEditText = (EditText)findViewById(R.id.editor_phone_home);
+        mMobilePhoneEditText = (EditText)findViewById(R.id.editor_phone_mobile);
+        mWorkPhoneEditText = (EditText)findViewById(R.id.editor_phone_work);
+        mEmailEditText = (EditText)findViewById(R.id.editor_email);
+
+        // Figure out whether we're creating a new contact (ACTION_INSERT) or editing
+        // an existing contact.
+        Intent intent = getIntent();
+        String action = intent.getAction();
+        if (Intent.ACTION_INSERT.equals(action)) {
+            // We're inserting a new contact, so save off the external account name
+            // which should have been added to the intent we were passed.
+            mIsInsert = true;
+            String accountName = intent.getStringExtra(RawContacts.ACCOUNT_NAME);
+            if (accountName == null) {
+                Log.e(TAG, "Account name is required");
+                finish();
+            }
+            setAccountName(accountName);
+        } else {
+            // We're editing an existing contact. Load in the data from the contact
+            // so that the user can edit it.
+            mIsInsert = false;
+            mRawContactUri = intent.getData();
+            if (mRawContactUri == null) {
+                Log.e(TAG, "Raw contact URI is required");
+                finish();
+            }
+            startLoadRawContactEntity();
+        }
+    }
+
+    @Override
+    public void onBackPressed() {
+        // This method will have been called if the user presses the "Back" button
+        // in the ActionBar.  We treat that the same way as the "Done" button in
+        // the ActionBar.
+        save();
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        // This method gets called so that we can place items in the main Options menu -
+        // for example, the ActionBar items.  We add our menus from the res/menu/edit.xml
+        // file.
+        MenuInflater inflater = getMenuInflater();
+        inflater.inflate(R.menu.edit, menu);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case android.R.id.home:
+            case R.id.menu_done:
+                // The user pressed the "Home" button or our "Done" button - both
+                // in the ActionBar.  In both cases, we want to save the contact
+                // and exit.
+                save();
+                return true;
+            case R.id.menu_cancel:
+                // The user pressed the Cancel menu item in the ActionBar.
+                // Close the editor without saving any changes.
+                finish();
+                return true;
+        }
+        return false;
+    }
+
+    /**
+     * Create an AsyncTask to load the contact from the Contacts data provider
+     */
+    private void startLoadRawContactEntity() {
+        Uri uri = Uri.withAppendedPath(mRawContactUri, RawContacts.Entity.CONTENT_DIRECTORY);
+        new LoadRawContactTask().execute(uri);
+    }
+
+    /**
+     * Called by the LoadRawContactTask when the contact information has been
+     * successfully loaded from the Contacts data provider.
+     */
+    public void onRawContactEntityLoaded(Cursor cursor) {
+        while (cursor.moveToNext()) {
+            String mimetype = cursor.getString(EditorQuery.COLUMN_MIMETYPE);
+            if (StructuredName.CONTENT_ITEM_TYPE.equals(mimetype)) {
+                setAccountName(cursor.getString(EditorQuery.COLUMN_ACCOUNT_NAME));
+                mRawContactId = cursor.getLong(EditorQuery.COLUMN_RAW_CONTACT_ID);
+                mNameEditText.setText(cursor.getString(EditorQuery.COLUMN_FULL_NAME));
+            } else if (Phone.CONTENT_ITEM_TYPE.equals(mimetype)) {
+                final int type = cursor.getInt(EditorQuery.COLUMN_PHONE_TYPE);
+                if (type == Phone.TYPE_HOME) {
+                    mHomePhoneEditText.setText(cursor.getString(EditorQuery.COLUMN_PHONE_NUMBER));
+                } else if (type == Phone.TYPE_MOBILE) {
+                    mMobilePhoneEditText.setText(cursor.getString(EditorQuery.COLUMN_PHONE_NUMBER));
+                } else if (type == Phone.TYPE_WORK) {
+                    mWorkPhoneEditText.setText(cursor.getString(EditorQuery.COLUMN_PHONE_NUMBER));
+                }
+            } else if (Email.CONTENT_ITEM_TYPE.equals(mimetype)) {
+                mEmailEditText.setText(cursor.getString(EditorQuery.COLUMN_DATA1));
+            }
+        }
+    }
+
+    /**
+     * Save the updated contact data. We actually take two different actions
+     * depending on whether we are creating a new contact or editing an
+     * existing contact.
+     */
+    public void save() {
+        // If we're already saving this contact, don't kick-off yet
+        // another save - the user probably just pressed the "Done"
+        // button multiple times...
+        if (mSaveInProgress) {
+            return;
+        }
+
+        mSaveInProgress = true;
+        if (mIsInsert) {
+            saveNewContact();
+        } else {
+            saveChanges();
+        }
+    }
+
+    /**
+     * Save off the external contacts provider account name. We show the account name
+     * in the header section of the edit panel, and we also need it later when we
+     * save off a brand new contact.
+     */
+    private void setAccountName(String accountName) {
+        mAccountName = accountName;
+        if (accountName != null) {
+            TextView accountNameLabel = (TextView)findViewById(R.id.header_account_name);
+            if (accountNameLabel != null) {
+                accountNameLabel.setText(accountName);
+            }
+        }
+    }
+
+    /**
+     * Save a new contact using the Contacts content provider. The actual insertion
+     * is performed in an AsyncTask.
+     */
+    @SuppressWarnings("unchecked")
+    private void saveNewContact() {
+        new InsertContactTask().execute(buildRawContact());
+    }
+
+    /**
+     * Save changes to an existing contact.  The actual update is performed in
+     * an AsyncTask.
+     */
+    @SuppressWarnings("unchecked")
+    private void saveChanges() {
+        new UpdateContactTask().execute(buildRawContact());
+    }
+
+    /**
+     * Build a RawContact object from the data in the user-editable form
+     * @return a new RawContact object representing the edited user
+     */
+    private RawContact buildRawContact() {
+        return RawContact.create(mNameEditText.getText().toString(),
+                null,
+                null,
+                mMobilePhoneEditText.getText().toString(),
+                mWorkPhoneEditText.getText().toString(),
+                mHomePhoneEditText.getText().toString(),
+                mEmailEditText.getText().toString(),
+                null,
+                false,
+                mRawContactId,
+                -1);
+    }
+
+    /**
+     * Called after a contact is saved - both for edited contacts and new contacts.
+     * We set the final result of the activity to be "ok", and then close the activity
+     * by calling finish().
+     */
+    public void onContactSaved(Uri result) {
+        if (result != null) {
+            Intent intent = new Intent();
+            intent.setData(result);
+            setResult(RESULT_OK, intent);
+            finish();
+        }
+        mSaveInProgress = false;
+    }
+
+    /**
+     * Represents an asynchronous task used to load a contact from
+     * the Contacts content provider.
+     *
+     */
+    public class LoadRawContactTask extends AsyncTask<Uri, Void, Cursor> {
+
+        @Override
+        protected Cursor doInBackground(Uri... params) {
+            // Our background task is to load the contact from the Contacts provider
+            return getContentResolver().query(params[0], EditorQuery.PROJECTION, null, null, null);
+        }
+
+        @Override
+        protected void onPostExecute(Cursor cursor) {
+            // After we've successfully loaded the contact, call back into
+            // the ContactEditorActivity so we can update the UI
+            try {
+                if (cursor != null) {
+                    onRawContactEntityLoaded(cursor);
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+    }
+
+    /**
+     * Represents an asynchronous task used to save a new contact
+     * into the contacts database.
+     */
+    public class InsertContactTask extends AsyncTask<RawContact, Void, Uri> {
+
+        @Override
+        protected Uri doInBackground(RawContact... params) {
+            try {
+                final RawContact rawContact = params[0];
+                final Context context = getApplicationContext();
+                final ContentResolver resolver = getContentResolver();
+                final BatchOperation batchOperation = new BatchOperation(context, resolver);
+                ContactManager.addContact(context, mAccountName, rawContact, false, batchOperation);
+                Uri rawContactUri = batchOperation.execute();
+
+                // Convert the raw contact URI to a contact URI
+                if (rawContactUri != null) {
+                    return RawContacts.getContactLookupUri(resolver, rawContactUri);
+                } else {
+                    Log.e(TAG, "Could not save new contact");
+                    return null;
+                }
+            } catch (Exception e) {
+                Log.e(TAG, "An error occurred while saving new contact", e);
+            }
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(Uri result) {
+            // Tell the UI that the contact has been successfully saved
+            onContactSaved(result);
+        }
+    }
+
+
+    /**
+     * Represents an asynchronous task used to save an updated contact
+     * into the contacts database.
+     */
+    public class UpdateContactTask extends AsyncTask<RawContact, Void, Uri> {
+
+        @Override
+        protected Uri doInBackground(RawContact... params) {
+            try {
+                final RawContact rawContact = params[0];
+                final Context context = getApplicationContext();
+                final ContentResolver resolver = getContentResolver();
+                final BatchOperation batchOperation = new BatchOperation(context, resolver);
+                ContactManager.updateContact(context, resolver, rawContact, false, false, false,
+                        false, rawContact.getRawContactId(), batchOperation);
+                batchOperation.execute();
+
+                // Convert the raw contact URI to a contact URI
+                return RawContacts.getContactLookupUri(resolver, mRawContactUri);
+            } catch (Exception e) {
+                Log.e(TAG, "Could not save changes", e);
+            }
+            return null;
+        }
+
+        @Override
+        protected void onPostExecute(Uri result) {
+            // Tell the UI that the contact has been successfully saved
+            onContactSaved(result);
+        }
+    }
+}
diff --git a/samples/SampleSyncAdapter/src/com/example/android/samplesync/platform/BatchOperation.java b/samples/SampleSyncAdapter/src/com/example/android/samplesync/platform/BatchOperation.java
index 0be3daa..3a9c879 100644
--- a/samples/SampleSyncAdapter/src/com/example/android/samplesync/platform/BatchOperation.java
+++ b/samples/SampleSyncAdapter/src/com/example/android/samplesync/platform/BatchOperation.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -16,9 +16,11 @@
 package com.example.android.samplesync.platform;
 
 import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.OperationApplicationException;
+import android.net.Uri;
 import android.os.RemoteException;
 import android.provider.ContactsContract;
 import android.util.Log;
@@ -50,19 +52,24 @@
         mOperations.add(cpo);
     }
 
-    public void execute() {
+    public Uri execute() {
+        Uri result = null;
 
         if (mOperations.size() == 0) {
-            return;
+            return result;
         }
         // Apply the mOperations to the content provider
         try {
-            mResolver.applyBatch(ContactsContract.AUTHORITY, mOperations);
+            ContentProviderResult[] results = mResolver.applyBatch(ContactsContract.AUTHORITY,
+                    mOperations);
+            if ((results != null) && (results.length > 0))
+                result = results[0].uri;
         } 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();
+        return result;
     }
 }
diff --git a/samples/SampleSyncAdapter/src/com/example/android/samplesync/platform/ContactManager.java b/samples/SampleSyncAdapter/src/com/example/android/samplesync/platform/ContactManager.java
index 218b165..2335d38 100644
--- a/samples/SampleSyncAdapter/src/com/example/android/samplesync/platform/ContactManager.java
+++ b/samples/SampleSyncAdapter/src/com/example/android/samplesync/platform/ContactManager.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -15,25 +15,30 @@
  */
 package com.example.android.samplesync.platform;
 
+import com.example.android.samplesync.Constants;
+import com.example.android.samplesync.R;
+import com.example.android.samplesync.client.RawContact;
+
+import android.accounts.Account;
 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;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.Im;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.Settings;
+import android.provider.ContactsContract.StatusUpdates;
 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.ArrayList;
 import java.util.List;
 
 /**
@@ -49,81 +54,172 @@
     private static final String TAG = "ContactManager";
 
     /**
-     * Synchronize raw contacts
-     * 
+     * Take a list of updated contacts and apply those changes to the
+     * contacts database. Typically this list of contacts would have been
+     * returned from the server, and we want to apply those changes locally.
+     *
      * @param context The context of Authenticator Activity
      * @param account The username for the account
-     * @param users The list of users
+     * @param rawContacts The list of contacts to update
+     * @param lastSyncMarker The previous server sync-state
+     * @return the server syncState that should be used in our next
+     * sync request.
      */
-    public static synchronized void syncContacts(Context context, String account, List<User> users) {
+    public static synchronized long updateContacts(Context context, String account,
+            List<RawContact> rawContacts, long lastSyncMarker) {
 
-        long userId;
-        long rawContactId = 0;
+        long currentSyncMarker = lastSyncMarker;
         final ContentResolver resolver = context.getContentResolver();
         final BatchOperation batchOperation = new BatchOperation(context, resolver);
+        final List<RawContact> newUsers = new ArrayList<RawContact>();
+
         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);
+        for (final RawContact rawContact : rawContacts) {
+            // The server returns a syncState (x) value with each contact record.
+            // The syncState is sequential, so higher values represent more recent
+            // changes than lower values. We keep track of the highest value we
+            // see, and consider that a "high water mark" for the changes we've
+            // received from the server.  That way, on our next sync, we can just
+            // ask for changes that have occurred since that most-recent change.
+            if (rawContact.getSyncState() > currentSyncMarker) {
+                currentSyncMarker = rawContact.getSyncState();
+            }
+
+            // If the server returned a clientId for this user, then it's likely
+            // that the user was added here, and was just pushed to the server
+            // for the first time. In that case, we need to update the main
+            // row for this contact so that the RawContacts.SOURCE_ID value
+            // contains the correct serverId.
+            final long rawContactId;
+            final boolean updateServerId;
+            if (rawContact.getRawContactId() > 0) {
+                rawContactId = rawContact.getRawContactId();
+                updateServerId = true;
+            } else {
+                long serverContactId = rawContact.getServerContactId();
+                rawContactId = lookupRawContact(resolver, serverContactId);
+                updateServerId = false;
+            }
             if (rawContactId != 0) {
-                if (!user.isDeleted()) {
-                    // update contact
-                    updateContact(context, resolver, account, user, rawContactId, batchOperation);
+                if (!rawContact.isDeleted()) {
+                    updateContact(context, resolver, rawContact, updateServerId,
+                            true, true, true, 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);
+                if (!rawContact.isDeleted()) {
+                    newUsers.add(rawContact);
+                    addContact(context, account, rawContact, true, batchOperation);
                 }
             }
             // A sync adapter should batch operations on multiple contacts,
             // because it will make a dramatic performance difference.
+            // (UI updates, etc)
             if (batchOperation.size() >= 50) {
                 batchOperation.execute();
             }
         }
         batchOperation.execute();
+
+        return currentSyncMarker;
     }
 
     /**
-     * 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
+     * Return a list of the local contacts that have been marked as
+     * "dirty", and need syncing to the SampleSync server.
+     *
+     * @param context The context of Authenticator Activity
+     * @param account The account that we're interested in syncing
+     * @return a list of Users that are considered "dirty"
      */
-    public static void insertStatuses(Context context, String username, List<User.Status> list) {
+    public static List<RawContact> getDirtyContacts(Context context, Account account) {
+        Log.i(TAG, "*** Looking for local dirty contacts");
+        List<RawContact> dirtyContacts = new ArrayList<RawContact>();
 
-        final ContentValues values = new ContentValues();
+        final ContentResolver resolver = context.getContentResolver();
+        final Cursor c = resolver.query(DirtyQuery.CONTENT_URI,
+                DirtyQuery.PROJECTION,
+                DirtyQuery.SELECTION,
+                new String[] {account.name},
+                null);
+        try {
+            while (c.moveToNext()) {
+                final long rawContactId = c.getLong(DirtyQuery.COLUMN_RAW_CONTACT_ID);
+                final long serverContactId = c.getLong(DirtyQuery.COLUMN_SERVER_ID);
+                final boolean isDirty = "1".equals(c.getString(DirtyQuery.COLUMN_DIRTY));
+                final boolean isDeleted = "1".equals(c.getString(DirtyQuery.COLUMN_DELETED));
+
+                // The system actually keeps track of a change version number for
+                // each contact. It may be something you're interested in for your
+                // client-server sync protocol. We're not using it in this example,
+                // other than to log it.
+                final long version = c.getLong(DirtyQuery.COLUMN_VERSION);
+
+                Log.i(TAG, "Dirty Contact: " + Long.toString(rawContactId));
+                Log.i(TAG, "Contact Version: " + Long.toString(version));
+
+                if (isDeleted) {
+                    Log.i(TAG, "Contact is marked for deletion");
+                    RawContact rawContact = RawContact.createDeletedContact(rawContactId,
+                            serverContactId);
+                    dirtyContacts.add(rawContact);
+                } else if (isDirty) {
+                    RawContact rawContact = getRawContact(context, rawContactId);
+                    Log.i(TAG, "Contact Name: " + rawContact.getBestName());
+                    dirtyContacts.add(rawContact);
+                }
+            }
+
+        } finally {
+            if (c != null) {
+                c.close();
+            }
+        }
+        return dirtyContacts;
+    }
+
+    /**
+     * Update the status messages for a list of users.  This is typically called
+     * for contacts we've just added to the system, since we can't monkey with
+     * the contact's status until they have a profileId.
+     *
+     * @param context The context of Authenticator Activity
+     * @param rawContacts The list of users we want to update
+     */
+    public static void updateStatusMessages(Context context, List<RawContact> rawContacts) {
         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();
-                }
+        for (RawContact rawContact : rawContacts) {
+            updateContactStatus(context, rawContact, batchOperation);
+        }
+        batchOperation.execute();
+    }
+
+    /**
+     * After we've finished up a sync operation, we want to clean up the sync-state
+     * so that we're ready for the next time.  This involves clearing out the 'dirty'
+     * flag on the synced contacts - but we also have to finish the DELETE operation
+     * on deleted contacts.  When the user initially deletes them on the client, they're
+     * marked for deletion - but they're not actually deleted until we delete them
+     * again, and include the ContactsContract.CALLER_IS_SYNCADAPTER parameter to
+     * tell the contacts provider that we're really ready to let go of this contact.
+     *
+     * @param context The context of Authenticator Activity
+     * @param dirtyContacts The list of contacts that we're cleaning up
+     */
+    public static void clearSyncFlags(Context context, List<RawContact> dirtyContacts) {
+        Log.i(TAG, "*** Clearing Sync-related Flags");
+        final ContentResolver resolver = context.getContentResolver();
+        final BatchOperation batchOperation = new BatchOperation(context, resolver);
+        for (RawContact rawContact : dirtyContacts) {
+            if (rawContact.isDeleted()) {
+                Log.i(TAG, "Deleting contact: " + Long.toString(rawContact.getRawContactId()));
+                deleteContact(context, rawContact.getRawContactId(), batchOperation);
+            } else if (rawContact.isDirty()) {
+                Log.i(TAG, "Clearing dirty flag for: " + rawContact.getBestName());
+                clearDirtyFlag(context, rawContact.getRawContactId(), batchOperation);
             }
         }
         batchOperation.execute();
@@ -131,89 +227,312 @@
 
     /**
      * Adds a single contact to the platform contacts provider.
-     * 
+     * This can be used to respond to a new contact found as part
+     * of sync information returned from the server, or because a
+     * user added a new contact.
+     *
      * @param context the Authenticator Activity context
      * @param accountName the account the contact belongs to
-     * @param user the sample SyncAdapter User object
+     * @param rawContact the sample SyncAdapter User object
+     * @param inSync is the add part of a client-server sync?
+     * @param batchOperation allow us to batch together multiple operations
+     *        into a single provider call
      */
-    private static void addContact(Context context, String accountName, User user,
-        BatchOperation batchOperation) {
+    public static void addContact(Context context, String accountName, RawContact rawContact,
+        boolean inSync, 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());
+        final ContactOperations contactOp = ContactOperations.createNewContact(
+                context, rawContact.getServerContactId(), accountName, inSync, batchOperation);
+
+        contactOp.addName(rawContact.getFullName(), rawContact.getFirstName(),
+                rawContact.getLastName())
+                .addEmail(rawContact.getEmail())
+                .addPhone(rawContact.getCellPhone(), Phone.TYPE_MOBILE)
+                .addPhone(rawContact.getHomePhone(), Phone.TYPE_HOME)
+                .addPhone(rawContact.getOfficePhone(), Phone.TYPE_WORK)
+                .addAvatar(rawContact.getAvatarUrl());
+
+        // If we have a serverId, then go ahead and create our status profile.
+        // Otherwise skip it - and we'll create it after we sync-up to the
+        // server later on.
+        if (rawContact.getServerContactId() > 0) {
+            contactOp.addProfileAction(rawContact.getServerContactId());
+        }
     }
 
     /**
      * Updates a single contact to the platform contacts provider.
-     * 
+     * This method can be used to update a contact from a sync
+     * operation or as a result of a user editing a contact
+     * record.
+     *
+     * This operation is actually relatively complex.  We query
+     * the database to find all the rows of info that already
+     * exist for this Contact. For rows that exist (and thus we're
+     * modifying existing fields), we create an update operation
+     * to change that field.  But for fields we're adding, we create
+     * "add" operations to create new rows for those fields.
+     *
      * @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 rawContact the sample SyncAdapter contact object
+     * @param updateStatus should we update this user's status
+     * @param updateAvatar should we update this user's avatar image
+     * @param inSync is the update part of a client-server sync?
      * @param rawContactId the unique Id for this rawContact in contacts
      *        provider
+     * @param batchOperation allow us to batch together multiple operations
+     *        into a single provider call
      */
-    private static void updateContact(Context context, ContentResolver resolver,
-        String accountName, User user, long rawContactId, BatchOperation batchOperation) {
+    public static void updateContact(Context context, ContentResolver resolver,
+        RawContact rawContact, boolean updateServerId, boolean updateStatus, boolean updateAvatar,
+        boolean inSync, long rawContactId, BatchOperation batchOperation) {
 
-        Uri uri;
-        String cellPhone = null;
-        String otherPhone = null;
-        String email = null;
+        boolean existingCellPhone = false;
+        boolean existingHomePhone = false;
+        boolean existingWorkPhone = false;
+        boolean existingEmail = false;
+        boolean existingAvatar = false;
+
         final Cursor c =
-            resolver.query(Data.CONTENT_URI, DataQuery.PROJECTION, DataQuery.SELECTION,
+                resolver.query(DataQuery.CONTENT_URI, DataQuery.PROJECTION, DataQuery.SELECTION,
                 new String[] {String.valueOf(rawContactId)}, null);
         final ContactOperations contactOp =
-            ContactOperations.updateExistingContact(context, rawContactId, batchOperation);
+                ContactOperations.updateExistingContact(context, rawContactId,
+                inSync, batchOperation);
         try {
+            // Iterate over the existing rows of data, and update each one
+            // with the information we received from the server.
             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);
+                final Uri 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());
+                    contactOp.updateName(uri,
+                            c.getString(DataQuery.COLUMN_GIVEN_NAME),
+                            c.getString(DataQuery.COLUMN_FAMILY_NAME),
+                            c.getString(DataQuery.COLUMN_FULL_NAME),
+                            rawContact.getFirstName(),
+                            rawContact.getLastName(),
+                            rawContact.getFullName());
                 } 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);
+                        existingCellPhone = true;
+                        contactOp.updatePhone(c.getString(DataQuery.COLUMN_PHONE_NUMBER),
+                                rawContact.getCellPhone(), uri);
+                    } else if (type == Phone.TYPE_HOME) {
+                        existingHomePhone = true;
+                        contactOp.updatePhone(c.getString(DataQuery.COLUMN_PHONE_NUMBER),
+                                rawContact.getHomePhone(), uri);
+                    } else if (type == Phone.TYPE_WORK) {
+                        existingWorkPhone = true;
+                        contactOp.updatePhone(c.getString(DataQuery.COLUMN_PHONE_NUMBER),
+                                rawContact.getOfficePhone(), uri);
                     }
-                } else if (Data.MIMETYPE.equals(Email.CONTENT_ITEM_TYPE)) {
-                    email = c.getString(DataQuery.COLUMN_EMAIL_ADDRESS);
-                    contactOp.updateEmail(user.getEmail(), email, uri);
+                } else if (mimeType.equals(Email.CONTENT_ITEM_TYPE)) {
+                    existingEmail = true;
+                    contactOp.updateEmail(rawContact.getEmail(),
+                            c.getString(DataQuery.COLUMN_EMAIL_ADDRESS), uri);
+                } else if (mimeType.equals(Photo.CONTENT_ITEM_TYPE)) {
+                    existingAvatar = true;
+                    contactOp.updateAvatar(rawContact.getAvatarUrl(), 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);
+        if (!existingCellPhone) {
+            contactOp.addPhone(rawContact.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 home phone, if present and not updated above
+        if (!existingHomePhone) {
+            contactOp.addPhone(rawContact.getHomePhone(), Phone.TYPE_HOME);
+        }
+
+        // Add the work phone, if present and not updated above
+        if (!existingWorkPhone) {
+            contactOp.addPhone(rawContact.getOfficePhone(), Phone.TYPE_WORK);
         }
         // Add the email address, if present and not updated above
-        if (email == null) {
-            contactOp.addEmail(user.getEmail());
+        if (!existingEmail) {
+            contactOp.addEmail(rawContact.getEmail());
+        }
+        // Add the avatar if we didn't update the existing avatar
+        if (!existingAvatar) {
+            contactOp.addAvatar(rawContact.getAvatarUrl());
+        }
+
+        // If we need to update the serverId of the contact record, take
+        // care of that.  This will happen if the contact is created on the
+        // client, and then synced to the server. When we get the updated
+        // record back from the server, we can set the SOURCE_ID property
+        // on the contact, so we can (in the future) lookup contacts by
+        // the serverId.
+        if (updateServerId) {
+            Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+            contactOp.updateServerId(rawContact.getServerContactId(), uri);
+        }
+
+        // If we don't have a status profile, then create one.  This could
+        // happen for contacts that were created on the client - we don't
+        // create the status profile until after the first sync...
+        final long serverId = rawContact.getServerContactId();
+        final long profileId = lookupProfile(resolver, serverId);
+        if (profileId <= 0) {
+            contactOp.addProfileAction(serverId);
         }
     }
 
     /**
-     * Deletes a contact from the platform contacts provider.
-     * 
+     * When we first add a sync adapter to the system, the contacts from that
+     * sync adapter will be hidden unless they're merged/grouped with an existing
+     * contact.  But typically we want to actually show those contacts, so we
+     * need to mess with the Settings table to get them to show up.
+     *
+     * @param context the Authenticator Activity context
+     * @param account the Account who's visibility we're changing
+     * @param visible true if we want the contacts visible, false for hidden
+     */
+    public static void setAccountContactsVisibility(Context context, Account account,
+            boolean visible) {
+        ContentValues values = new ContentValues();
+        values.put(RawContacts.ACCOUNT_NAME, account.name);
+        values.put(RawContacts.ACCOUNT_TYPE, Constants.ACCOUNT_TYPE);
+        values.put(Settings.UNGROUPED_VISIBLE, visible ? 1 : 0);
+
+        context.getContentResolver().insert(Settings.CONTENT_URI, values);
+    }
+
+    /**
+     * Return a User object with data extracted from a contact stored
+     * in the local contacts database.
+     *
+     * Because a contact is actually stored over several rows in the
+     * database, our query will return those multiple rows of information.
+     * We then iterate over the rows and build the User structure from
+     * what we find.
+     *
+     * @param context the Authenticator Activity context
+     * @param rawContactId the unique ID for the local contact
+     * @return a User object containing info on that contact
+     */
+    private static RawContact getRawContact(Context context, long rawContactId) {
+        String firstName = null;
+        String lastName = null;
+        String fullName = null;
+        String cellPhone = null;
+        String homePhone = null;
+        String workPhone = null;
+        String email = null;
+        long serverId = -1;
+
+        final ContentResolver resolver = context.getContentResolver();
+        final Cursor c =
+            resolver.query(DataQuery.CONTENT_URI, DataQuery.PROJECTION, DataQuery.SELECTION,
+                new String[] {String.valueOf(rawContactId)}, null);
+        try {
+            while (c.moveToNext()) {
+                final long id = c.getLong(DataQuery.COLUMN_ID);
+                final String mimeType = c.getString(DataQuery.COLUMN_MIMETYPE);
+                final long tempServerId = c.getLong(DataQuery.COLUMN_SERVER_ID);
+                if (tempServerId > 0) {
+                    serverId = tempServerId;
+                }
+                final Uri uri = ContentUris.withAppendedId(Data.CONTENT_URI, id);
+                if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) {
+                    lastName = c.getString(DataQuery.COLUMN_FAMILY_NAME);
+                    firstName = c.getString(DataQuery.COLUMN_GIVEN_NAME);
+                    fullName = c.getString(DataQuery.COLUMN_FULL_NAME);
+                } 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);
+                    } else if (type == Phone.TYPE_HOME) {
+                        homePhone = c.getString(DataQuery.COLUMN_PHONE_NUMBER);
+                    } else if (type == Phone.TYPE_WORK) {
+                        workPhone = c.getString(DataQuery.COLUMN_PHONE_NUMBER);
+                    }
+                } else if (mimeType.equals(Email.CONTENT_ITEM_TYPE)) {
+                    email = c.getString(DataQuery.COLUMN_EMAIL_ADDRESS);
+                }
+            } // while
+        } finally {
+            c.close();
+        }
+
+        // Now that we've extracted all the information we care about,
+        // create the actual User object.
+        RawContact rawContact = RawContact.create(fullName, firstName, lastName, cellPhone,
+                workPhone, homePhone, email, null, false, rawContactId, serverId);
+
+        return rawContact;
+    }
+
+    /**
+     * Update the status message associated with the specified user.  The status
+     * message would be something that is likely to be used by IM or social
+     * networking sync providers, and less by a straightforward contact provider.
+     * But it's a useful demo to see how it's done.
+     *
+     * @param context the Authenticator Activity context
+     * @param rawContact the contact who's status we should update
+     * @param batchOperation allow us to batch together multiple operations
+     */
+    private static void updateContactStatus(Context context, RawContact rawContact,
+            BatchOperation batchOperation) {
+        final ContentValues values = new ContentValues();
+        final ContentResolver resolver = context.getContentResolver();
+
+        final long userId = rawContact.getServerContactId();
+        final String username = rawContact.getUserName();
+        final String status = rawContact.getStatus();
+
+        // Look up the user's sample SyncAdapter data row
+        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);
+            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, userId);
+            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,
+                    false, true).withValues(values).build());
+        }
+    }
+
+    /**
+     * Clear the local system 'dirty' flag for a contact.
+     *
+     * @param context the Authenticator Activity context
+     * @param rawContactId the id of the contact update
+     * @param batchOperation allow us to batch together multiple operations
+     */
+    private static void clearDirtyFlag(Context context, long rawContactId,
+        BatchOperation batchOperation) {
+        final ContactOperations contactOp =
+                ContactOperations.updateExistingContact(context, rawContactId, true,
+                batchOperation);
+
+        final Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+        contactOp.updateDirtyFlag(false, uri);
+    }
+
+     /**
+     * Deletes a contact from the platform contacts provider. This method is used
+     * both for contacts that were deleted locally and then that deletion was synced
+     * to the server, and for contacts that were deleted on the server and the
+     * deletion was synced to the client.
+     *
      * @param context the Authenticator Activity context
      * @param rawContactId the unique Id for this rawContact in contacts
      *        provider
@@ -222,39 +541,43 @@
         BatchOperation batchOperation) {
 
         batchOperation.add(ContactOperations.newDeleteCpo(
-            ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId), true).build());
+                ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
+                true, 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
+     *
+     * @param resolver the content resolver to use
+     * @param serverContactId the sample SyncAdapter user ID to lookup
      * @return the RawContact id, or 0 if not found
      */
-    private static long lookupRawContact(ContentResolver resolver, long userId) {
+    private static long lookupRawContact(ContentResolver resolver, long serverContactId) {
 
-        long authorId = 0;
-        final Cursor c =
-            resolver.query(RawContacts.CONTENT_URI, UserIdQuery.PROJECTION, UserIdQuery.SELECTION,
-                new String[] {String.valueOf(userId)}, null);
+        long rawContactId = 0;
+        final Cursor c = resolver.query(
+                UserIdQuery.CONTENT_URI,
+                UserIdQuery.PROJECTION,
+                UserIdQuery.SELECTION,
+                new String[] {String.valueOf(serverContactId)},
+                null);
         try {
-            if (c.moveToFirst()) {
-                authorId = c.getLong(UserIdQuery.COLUMN_ID);
+            if ((c != null) && c.moveToFirst()) {
+                rawContactId = c.getLong(UserIdQuery.COLUMN_RAW_CONTACT_ID);
             }
         } finally {
             if (c != null) {
                 c.close();
             }
         }
-        return authorId;
+        return rawContactId;
     }
 
     /**
      * 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
@@ -266,7 +589,7 @@
             resolver.query(Data.CONTENT_URI, ProfileQuery.PROJECTION, ProfileQuery.SELECTION,
                 new String[] {String.valueOf(userId)}, null);
         try {
-            if (c != null && c.moveToFirst()) {
+            if ((c != null) && c.moveToFirst()) {
                 profileId = c.getLong(ProfileQuery.COLUMN_ID);
             }
         } finally {
@@ -277,6 +600,46 @@
         return profileId;
     }
 
+    final public static class EditorQuery {
+
+        private EditorQuery() {
+        }
+
+        public static final String[] PROJECTION = new String[] {
+            RawContacts.ACCOUNT_NAME,
+            Data._ID,
+            RawContacts.Entity.DATA_ID,
+            Data.MIMETYPE,
+            Data.DATA1,
+            Data.DATA2,
+            Data.DATA3,
+            Data.DATA15,
+            Data.SYNC1
+            };
+
+        public static final int COLUMN_ACCOUNT_NAME = 0;
+        public static final int COLUMN_RAW_CONTACT_ID = 1;
+        public static final int COLUMN_DATA_ID = 2;
+        public static final int COLUMN_MIMETYPE = 3;
+        public static final int COLUMN_DATA1 = 4;
+        public static final int COLUMN_DATA2 = 5;
+        public static final int COLUMN_DATA3 = 6;
+        public static final int COLUMN_DATA15 = 7;
+        public static final int COLUMN_SYNC1 = 8;
+
+        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_FULL_NAME = COLUMN_DATA1;
+        public static final int COLUMN_GIVEN_NAME = COLUMN_DATA2;
+        public static final int COLUMN_FAMILY_NAME = COLUMN_DATA3;
+        public static final int COLUMN_AVATAR_IMAGE = COLUMN_DATA15;
+        public static final int COLUMN_SYNC_DIRTY = COLUMN_SYNC1;
+
+        public static final String SELECTION = Data.RAW_CONTACT_ID + "=?";
+    }
+
     /**
      * Constants for a query to find a contact given a sample SyncAdapter user
      * ID.
@@ -304,9 +667,15 @@
         private UserIdQuery() {
         }
 
-        public final static String[] PROJECTION = new String[] {RawContacts._ID};
+        public final static String[] PROJECTION = new String[] {
+            RawContacts._ID,
+            RawContacts.CONTACT_ID
+            };
 
-        public final static int COLUMN_ID = 0;
+        public final static int COLUMN_RAW_CONTACT_ID = 0;
+        public final static int COLUMN_LINKED_CONTACT_ID = 1;
+
+        public final static Uri CONTENT_URI = RawContacts.CONTENT_URI;
 
         public static final String SELECTION =
             RawContacts.ACCOUNT_TYPE + "='" + Constants.ACCOUNT_TYPE + "' AND "
@@ -314,6 +683,40 @@
     }
 
     /**
+     * Constants for a query to find SampleSyncAdapter contacts that are
+     * in need of syncing to the server. This should cover new, edited,
+     * and deleted contacts.
+     */
+    final private static class DirtyQuery {
+
+        private DirtyQuery() {
+        }
+
+        public final static String[] PROJECTION = new String[] {
+            RawContacts._ID,
+            RawContacts.SOURCE_ID,
+            RawContacts.DIRTY,
+            RawContacts.DELETED,
+            RawContacts.VERSION
+            };
+
+        public final static int COLUMN_RAW_CONTACT_ID = 0;
+        public final static int COLUMN_SERVER_ID = 1;
+        public final static int COLUMN_DIRTY = 2;
+        public final static int COLUMN_DELETED = 3;
+        public final static int COLUMN_VERSION = 4;
+
+        public static final Uri CONTENT_URI = RawContacts.CONTENT_URI.buildUpon()
+            .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
+            .build();
+
+        public static final String SELECTION =
+            RawContacts.DIRTY + "=1 AND "
+                + RawContacts.ACCOUNT_TYPE + "='" + Constants.ACCOUNT_TYPE + "' AND "
+                + RawContacts.ACCOUNT_NAME + "=?";
+    }
+
+    /**
      * Constants for a query to get contact data for a given rawContactId
      */
     final private static class DataQuery {
@@ -322,29 +725,29 @@
         }
 
         public static final String[] PROJECTION =
-            new String[] {Data._ID, Data.MIMETYPE, Data.DATA1, Data.DATA2, Data.DATA3,};
+            new String[] {Data._ID, RawContacts.SOURCE_ID, Data.MIMETYPE, Data.DATA1,
+            Data.DATA2, Data.DATA3, Data.DATA15, Data.SYNC1};
 
         public static final int COLUMN_ID = 0;
+        public static final int COLUMN_SERVER_ID = 1;
+        public static final int COLUMN_MIMETYPE = 2;
+        public static final int COLUMN_DATA1 = 3;
+        public static final int COLUMN_DATA2 = 4;
+        public static final int COLUMN_DATA3 = 5;
+        public static final int COLUMN_DATA15 = 6;
+        public static final int COLUMN_SYNC1 = 7;
 
-        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 Uri CONTENT_URI = Data.CONTENT_URI;
 
         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_FULL_NAME = COLUMN_DATA1;
         public static final int COLUMN_GIVEN_NAME = COLUMN_DATA2;
-
         public static final int COLUMN_FAMILY_NAME = COLUMN_DATA3;
+        public static final int COLUMN_AVATAR_IMAGE = COLUMN_DATA15;
+        public static final int COLUMN_SYNC_DIRTY = COLUMN_SYNC1;
 
         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
index db01f48..cb8e97b 100644
--- a/samples/SampleSyncAdapter/src/com/example/android/samplesync/platform/ContactOperations.java
+++ b/samples/SampleSyncAdapter/src/com/example/android/samplesync/platform/ContactOperations.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -15,115 +15,134 @@
  */
 package com.example.android.samplesync.platform;
 
+import com.example.android.samplesync.Constants;
+import com.example.android.samplesync.R;
+import com.example.android.samplesync.client.NetworkUtilities;
+
 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.Photo;
 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
 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 boolean mIsSyncOperation;
     private long mRawContactId;
-
     private int mBackReference;
-
     private boolean mIsNewContact;
 
     /**
+     * Since we're sending a lot of contact provider operations in a single
+     * batched operation, we want to make sure that we "yield" periodically
+     * so that the Contact Provider can write changes to the DB, and can
+     * open a new transaction.  This prevents ANR (application not responding)
+     * errors.  The recommended time to specify that a yield is permitted is
+     * with the first operation on a particular contact.  So if we're updating
+     * multiple fields for a single contact, we make sure that we call
+     * withYieldAllowed(true) on the first field that we update. We use
+     * mIsYieldAllowed to keep track of what value we should pass to
+     * withYieldAllowed().
+     */
+    private boolean mIsYieldAllowed;
+
+    /**
      * 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
+     * @param accountName the username for the SyncAdapter account
+     * @param isSyncOperation are we executing this as part of a sync operation?
      * @return instance of ContactOperations
      */
-    public static ContactOperations createNewContact(Context context, int userId,
-        String accountName, BatchOperation batchOperation) {
-
-        return new ContactOperations(context, userId, accountName, batchOperation);
+    public static ContactOperations createNewContact(Context context, long userId,
+            String accountName, boolean isSyncOperation, BatchOperation batchOperation) {
+        return new ContactOperations(context, userId, accountName, isSyncOperation, 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
+     * @param isSyncOperation are we executing this as part of a sync operation?
      * @return instance of ContactOperations
      */
     public static ContactOperations updateExistingContact(Context context, long rawContactId,
-        BatchOperation batchOperation) {
-
-        return new ContactOperations(context, rawContactId, batchOperation);
+            boolean isSyncOperation, BatchOperation batchOperation) {
+        return new ContactOperations(context, rawContactId, isSyncOperation, batchOperation);
     }
 
-    public ContactOperations(Context context, BatchOperation batchOperation) {
+    public ContactOperations(Context context, boolean isSyncOperation,
+            BatchOperation batchOperation) {
         mValues = new ContentValues();
-        mYield = true;
+        mIsYieldAllowed = true;
+        mIsSyncOperation = isSyncOperation;
         mContext = context;
         mBatchOperation = batchOperation;
     }
 
-    public ContactOperations(Context context, int userId, String accountName,
-        BatchOperation batchOperation) {
-
-        this(context, batchOperation);
+    public ContactOperations(Context context, long userId, String accountName,
+            boolean isSyncOperation, BatchOperation batchOperation) {
+        this(context, isSyncOperation, 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());
+        ContentProviderOperation.Builder builder =
+                newInsertCpo(RawContacts.CONTENT_URI, mIsSyncOperation, true).withValues(mValues);
+        mBatchOperation.add(builder.build());
     }
 
-    public ContactOperations(Context context, long rawContactId, BatchOperation batchOperation) {
-        this(context, batchOperation);
+    public ContactOperations(Context context, long rawContactId, boolean isSyncOperation,
+            BatchOperation batchOperation) {
+        this(context, isSyncOperation, batchOperation);
         mIsNewContact = false;
         mRawContactId = rawContactId;
     }
 
     /**
-     * Adds a contact name
-     * 
-     * @param name Name of contact
-     * @param nameType type of name: family name, given name, etc.
+     * Adds a contact name. We can take either a full name ("Bob Smith") or separated
+     * first-name and last-name ("Bob" and "Smith").
+     *
+     * @param fullName The full name of the contact - typically from an edit form
+     *      Can be null if firstName/lastName are specified.
+     * @param firstName The first name of the contact - can be null if fullName
+     *      is specified.
+     * @param lastName The last name of the contact - can be null if fullName
+     *      is specified.
      * @return instance of ContactOperations
      */
-    public ContactOperations addName(String firstName, String lastName) {
-
+    public ContactOperations addName(String fullName, String firstName, String lastName) {
         mValues.clear();
-        if (!TextUtils.isEmpty(firstName)) {
-            mValues.put(StructuredName.GIVEN_NAME, firstName);
+
+        if (!TextUtils.isEmpty(fullName)) {
+            mValues.put(StructuredName.DISPLAY_NAME, fullName);
             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);
+        } else {
+            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();
@@ -133,8 +152,8 @@
 
     /**
      * Adds an email
-     * 
-     * @param new email for user
+     *
+     * @param the email address we're adding
      * @return instance of ContactOperations
      */
     public ContactOperations addEmail(String email) {
@@ -150,7 +169,7 @@
 
     /**
      * Adds a phone number
-     * 
+     *
      * @param phone new phone number for the contact
      * @param phoneType the type: cell, home, etc.
      * @return instance of ContactOperations
@@ -166,9 +185,22 @@
         return this;
     }
 
+    public ContactOperations addAvatar(String avatarUrl) {
+        if (avatarUrl != null) {
+            byte[] avatarBuffer = NetworkUtilities.downloadAvatar(avatarUrl);
+            if (avatarBuffer != null) {
+                mValues.clear();
+                mValues.put(Photo.PHOTO, avatarBuffer);
+                mValues.put(Photo.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
+                addInsertOp();
+            }
+        }
+        return this;
+    }
+
     /**
      * Adds a profile action
-     * 
+     *
      * @param userId the userId of the sample SyncAdapter user object
      * @return instance of ContactOperations
      */
@@ -187,8 +219,22 @@
     }
 
     /**
+     * Updates contact's serverId
+     *
+     * @param serverId the serverId for this contact
+     * @param uri Uri for the existing raw contact to be updated
+     * @return instance of ContactOperations
+     */
+    public ContactOperations updateServerId(long serverId, Uri uri) {
+        mValues.clear();
+        mValues.put(RawContacts.SOURCE_ID, serverId);
+        addUpdateOp(uri);
+        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
@@ -203,25 +249,38 @@
     }
 
     /**
-     * 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.
+     * Updates contact's name. The caller can either provide first-name
+     * and last-name fields or a full-name field.
+     *
      * @param uri Uri for the existing raw contact to be updated
+     * @param existingFirstName the first name stored in provider
+     * @param existingLastName the last name stored in provider
+     * @param existingFullName the full name stored in provider
+     * @param firstName the new first name to store
+     * @param lastName the new last name to store
+     * @param fullName the new full name to store
      * @return instance of ContactOperations
      */
-    public ContactOperations updateName(Uri uri, String existingFirstName, String existingLastName,
-        String firstName, String lastName) {
+    public ContactOperations updateName(Uri uri,
+        String existingFirstName,
+        String existingLastName,
+        String existingFullName,
+        String firstName,
+        String lastName,
+        String fullName) {
 
-        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 (TextUtils.isEmpty(fullName)) {
+            if (!TextUtils.equals(existingFirstName, firstName)) {
+                mValues.put(StructuredName.GIVEN_NAME, firstName);
+            }
+            if (!TextUtils.equals(existingLastName, lastName)) {
+                mValues.put(StructuredName.FAMILY_NAME, lastName);
+            }
+        } else {
+            if (!TextUtils.equals(existingFullName, fullName)) {
+                mValues.put(StructuredName.DISPLAY_NAME, fullName);
+            }
         }
         if (mValues.size() > 0) {
             addUpdateOp(uri);
@@ -229,9 +288,17 @@
         return this;
     }
 
+    public ContactOperations updateDirtyFlag(boolean isDirty, Uri uri) {
+        int isDirtyValue = isDirty ? 1 : 0;
+        mValues.clear();
+        mValues.put(RawContacts.DIRTY, isDirtyValue);
+        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
@@ -246,9 +313,22 @@
         return this;
     }
 
+    public ContactOperations updateAvatar(String avatarUrl, Uri uri) {
+        if (avatarUrl != null) {
+            byte[] avatarBuffer = NetworkUtilities.downloadAvatar(avatarUrl);
+            if (avatarBuffer != null) {
+                mValues.clear();
+                mValues.put(Photo.PHOTO, avatarBuffer);
+                mValues.put(Photo.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
+                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
@@ -268,41 +348,62 @@
         if (!mIsNewContact) {
             mValues.put(Phone.RAW_CONTACT_ID, mRawContactId);
         }
-        mBuilder = newInsertCpo(addCallerIsSyncAdapterParameter(Data.CONTENT_URI), mYield);
-        mBuilder.withValues(mValues);
+        ContentProviderOperation.Builder builder =
+                newInsertCpo(Data.CONTENT_URI, mIsSyncOperation, mIsYieldAllowed);
+        builder.withValues(mValues);
         if (mIsNewContact) {
-            mBuilder.withValueBackReference(Data.RAW_CONTACT_ID, mBackReference);
+            builder.withValueBackReference(Data.RAW_CONTACT_ID, mBackReference);
         }
-        mYield = false;
-        mBatchOperation.add(mBuilder.build());
+        mIsYieldAllowed = false;
+        mBatchOperation.add(builder.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());
+        ContentProviderOperation.Builder builder =
+                newUpdateCpo(uri, mIsSyncOperation, mIsYieldAllowed).withValues(mValues);
+        mIsYieldAllowed = false;
+        mBatchOperation.add(builder.build());
     }
 
-    public static ContentProviderOperation.Builder newInsertCpo(Uri uri, boolean yield) {
-        return ContentProviderOperation.newInsert(addCallerIsSyncAdapterParameter(uri))
-            .withYieldAllowed(yield);
+    public static ContentProviderOperation.Builder newInsertCpo(Uri uri,
+            boolean isSyncOperation, boolean isYieldAllowed) {
+        return ContentProviderOperation
+                .newInsert(addCallerIsSyncAdapterParameter(uri, isSyncOperation))
+                .withYieldAllowed(isYieldAllowed);
     }
 
-    public static ContentProviderOperation.Builder newUpdateCpo(Uri uri, boolean yield) {
-        return ContentProviderOperation.newUpdate(addCallerIsSyncAdapterParameter(uri))
-            .withYieldAllowed(yield);
+    public static ContentProviderOperation.Builder newUpdateCpo(Uri uri,
+            boolean isSyncOperation, boolean isYieldAllowed) {
+        return ContentProviderOperation
+                .newUpdate(addCallerIsSyncAdapterParameter(uri, isSyncOperation))
+                .withYieldAllowed(isYieldAllowed);
     }
 
-    public static ContentProviderOperation.Builder newDeleteCpo(Uri uri, boolean yield) {
-        return ContentProviderOperation.newDelete(addCallerIsSyncAdapterParameter(uri))
-            .withYieldAllowed(yield);
+    public static ContentProviderOperation.Builder newDeleteCpo(Uri uri,
+            boolean isSyncOperation, boolean isYieldAllowed) {
+        return ContentProviderOperation
+                .newDelete(addCallerIsSyncAdapterParameter(uri, isSyncOperation))
+                .withYieldAllowed(isYieldAllowed);
     }
 
-    private static Uri addCallerIsSyncAdapterParameter(Uri uri) {
-        return uri.buildUpon().appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
-            .build();
+    private static Uri addCallerIsSyncAdapterParameter(Uri uri, boolean isSyncOperation) {
+        if (isSyncOperation) {
+            // If we're in the middle of a real sync-adapter operation, then go ahead
+            // and tell the Contacts provider that we're the sync adapter.  That
+            // gives us some special permissions - like the ability to really
+            // delete a contact, and the ability to clear the dirty flag.
+            //
+            // If we're not in the middle of a sync operation (for example, we just
+            // locally created/edited a new contact), then we don't want to use
+            // the special permissions, and the system will automagically mark
+            // the contact as 'dirty' for us!
+            return uri.buildUpon()
+                    .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
+                    .build();
+        }
+        return uri;
     }
 }
diff --git a/samples/SampleSyncAdapter/src/com/example/android/samplesync/platform/SampleSyncAdapterColumns.java b/samples/SampleSyncAdapter/src/com/example/android/samplesync/platform/SampleSyncAdapterColumns.java
index 7b60d5b..e8a99a4 100644
--- a/samples/SampleSyncAdapter/src/com/example/android/samplesync/platform/SampleSyncAdapterColumns.java
+++ b/samples/SampleSyncAdapter/src/com/example/android/samplesync/platform/SampleSyncAdapterColumns.java
@@ -1,12 +1,12 @@
 /*
  * 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
diff --git a/samples/SampleSyncAdapter/src/com/example/android/samplesync/syncadapter/SyncAdapter.java b/samples/SampleSyncAdapter/src/com/example/android/samplesync/syncadapter/SyncAdapter.java
index 206189a..0ca8dee 100644
--- a/samples/SampleSyncAdapter/src/com/example/android/samplesync/syncadapter/SyncAdapter.java
+++ b/samples/SampleSyncAdapter/src/com/example/android/samplesync/syncadapter/SyncAdapter.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -24,12 +24,12 @@
 import android.content.Context;
 import android.content.SyncResult;
 import android.os.Bundle;
+import android.text.TextUtils;
 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.client.RawContact;
 import com.example.android.samplesync.platform.ContactManager;
 
 import org.apache.http.ParseException;
@@ -37,23 +37,27 @@
 import org.json.JSONException;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
 
 /**
  * SyncAdapter implementation for syncing sample SyncAdapter contacts to the
- * platform ContactOperations provider.
+ * platform ContactOperations provider.  This sample shows a basic 2-way
+ * sync between the client and a sample server.  It also contains an
+ * example of how to update the contacts' status messages, which
+ * would be useful for a messaging or social networking client.
  */
 public class SyncAdapter extends AbstractThreadedSyncAdapter {
 
     private static final String TAG = "SyncAdapter";
+    private static final String SYNC_MARKER_KEY = "com.example.android.samplesync.marker";
+    private static final boolean NOTIFY_AUTH_FAILURE = true;
 
     private final AccountManager mAccountManager;
 
     private final Context mContext;
 
-    private Date mLastUpdated;
-
     public SyncAdapter(Context context, boolean autoInitialize) {
         super(context, autoInitialize);
         mContext = context;
@@ -64,42 +68,101 @@
     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.
+            // see if we already have a sync-state attached to this account. By handing
+            // This value to the server, we can just get the contacts that have
+            // been updated on the server-side since our last sync-up
+            long lastSyncMarker = getServerSyncMarker(account);
+
+            // By default, contacts from a 3rd party provider are hidden in the contacts
+            // list. So let's set the flag that causes them to be visible, so that users
+            // can actually see these contacts.
+            if (lastSyncMarker == 0) {
+                ContactManager.setAccountContactsVisibility(getContext(), account, true);
+            }
+
+            List<RawContact> dirtyContacts;
+            List<RawContact> updatedContacts;
+
+            // Use the account manager to request the AuthToken we'll need
+            // to talk to our sample server.  If we don't have an AuthToken
+            // yet, this could involve a round-trip to the server to request
+            // and AuthToken.
+            final String authtoken = mAccountManager.blockingGetAuthToken(account,
+                    Constants.AUTHTOKEN_TYPE, NOTIFY_AUTH_FAILURE);
+
+            // Find the local 'dirty' contacts that we need to tell the server about...
+            // Find the local users that need to be sync'd to the server...
+            dirtyContacts = ContactManager.getDirtyContacts(mContext, account);
+
+            // Send the dirty contacts to the server, and retrieve the server-side changes
+            updatedContacts = NetworkUtilities.syncContacts(account, authtoken,
+                    lastSyncMarker, dirtyContacts);
+
+            // Update the local contacts database with the changes. updateContacts()
+            // returns a syncState value that indicates the high-water-mark for
+            // the changes we received.
             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);
+            long newSyncState = ContactManager.updateContacts(mContext,
+                    account.name,
+                    updatedContacts,
+                    lastSyncMarker);
+
+            // This is a demo of how you can update IM-style status messages
+            // for contacts on the client. This probably won't apply to
+            // 2-way contact sync providers - it's more likely that one-way
+            // sync providers (IM clients, social networking apps, etc) would
+            // use this feature.
+            ContactManager.updateStatusMessages(mContext, updatedContacts);
+
+            // Save off the new sync marker. On our next sync, we only want to receive
+            // contacts that have changed since this sync...
+            setServerSyncMarker(account, newSyncState);
+
+            if (dirtyContacts.size() > 0) {
+                ContactManager.clearSyncFlags(mContext, dirtyContacts);
+            }
+
         } catch (final AuthenticatorException e) {
-            syncResult.stats.numParseExceptions++;
             Log.e(TAG, "AuthenticatorException", e);
+            syncResult.stats.numParseExceptions++;
         } 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);
+            syncResult.stats.numAuthExceptions++;
         } catch (final ParseException e) {
-            syncResult.stats.numParseExceptions++;
             Log.e(TAG, "ParseException", e);
-        } catch (final JSONException e) {
             syncResult.stats.numParseExceptions++;
+        } catch (final JSONException e) {
             Log.e(TAG, "JSONException", e);
+            syncResult.stats.numParseExceptions++;
         }
     }
+
+    /**
+     * This helper function fetches the last known high-water-mark
+     * we received from the server - or 0 if we've never synced.
+     * @param account the account we're syncing
+     * @return the change high-water-mark
+     */
+    private long getServerSyncMarker(Account account) {
+        String markerString = mAccountManager.getUserData(account, SYNC_MARKER_KEY);
+        if (!TextUtils.isEmpty(markerString)) {
+            return Long.parseLong(markerString);
+        }
+        return 0;
+    }
+
+    /**
+     * Save off the high-water-mark we receive back from the server.
+     * @param account The account we're syncing
+     * @param marker The high-water-mark we want to save.
+     */
+    private void setServerSyncMarker(Account account, long marker) {
+        mAccountManager.setUserData(account, SYNC_MARKER_KEY, Long.toString(marker));
+    }
 }