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