SampleSyncAdapter sample code.

This commit is contained in:
Megha Joshi
2009-11-18 14:54:03 -08:00
parent c220077d52
commit 00bf0f0296
29 changed files with 2969 additions and 0 deletions

View File

@@ -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))

View File

@@ -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>

View File

@@ -0,0 +1,26 @@
<p>A sample that demonstrates how an application can communicate with cloud-based services and synchronize their data with data stored locally in a content provider.
The sample uses two related parts of the Android framework &mdash; the account manager and the synchronization manager (through a sync adapter).</p>
<p> The <a href="../../../android/accounts/AccountManager">account manager</a> allows sharing of credentials across multiple applications and services.
Users enter the credentials for each account only once &mdash; applications with the <code>USE_CREDENTIALS</code> permission can then query the account manager
to obtain an auth token for the account.The authenticator (a pluggable component of account manager) requests credentials from the user, validates them
with an authentication server running in the cloud, and then stores them to the AccountManager.
This sample demonstrates how to write an authenticator for your
service by extending the new <code><a href="../../../android/accounts/AbstractAccountAuthenticator.html">AbstractAccountAuthenticator</a></code> abstract class.
</p>
<p>The sync adapter (essential to the synchronization service) declares the account type and ContentProvider authority to the sync manager.
This sample demosntrates how to write your own sync adapters by extending the <code><a href="../../../android/content/AbstractThreadedSyncAdapter.html">AbstractThreadedSyncAdapter</a></code>
abstract class and implementing the onPerformSync() method that gets called whenever the sync manager issues a sync operation for that sync adapter.
</p>
<p> The service for this sample application is running at: <br>
http://samplesyncadapter.appspot.com/users
</p>
<p class="note">When you install this sample application, a new syncable "SampleSyncAdapter" account will be added to your phone's account manager.
You can go to "Settings | Accounts & sync" to view the accounts that are stored in the account manager and to change their sync settings. </p>
<img alt="Screenshot 1" src="../images/SampleSyncAdapter1.png" />
<img alt="Screenshot 2" src="../images/SampleSyncAdapter2.png" />
<img alt="Screenshot 3" src="../images/SampleSyncAdapter3.png" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"
/>

View File

@@ -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>

View File

@@ -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"
/>

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -0,0 +1,19 @@
<html>
<body>
<h1> Sample Sync Adapter </h1>
<p>
<h3> List of Users </h3>
<table>
{% for user in users %}
<tr><td>
<a
href="/edit_user?user={{ user.key.id}}">{{ user.firstname }}&nbsp; {{ user.lastname }} </a>
</td><td>&nbsp;&nbsp;<a href="/user_friends?user={{ user.handle }}">Friends</a> </td>
</tr>
{% endfor %}
</table>
</p>
<a href = "/add_user"> Insert More </a>

View File

@@ -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>

View File

@@ -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";
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}

View File

@@ -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 + "=?";
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}