New version of SampleSyncAdapter sample code that allows local editing

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

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

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

Change-Id: I017f1d3f9320a02fe05a20f1613846963107145e
This commit is contained in:
John Evans
2011-04-04 13:38:01 -07:00
parent 3f2b06f3c5
commit 15ef1a8091
45 changed files with 3159 additions and 1237 deletions

View File

@@ -6,10 +6,12 @@ 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_PACKAGE_NAME := SampleSyncAdapter
LOCAL_SDK_VERSION := current
LOCAL_DX_FLAGS=--target-api=11
include $(BUILD_PACKAGE)
# Use the folloing include to make our test apk.

View File

@@ -46,7 +46,7 @@
<uses-permission
android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-sdk android:minSdkVersion="5" />
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="11"/>
<application
android:icon="@drawable/icon"
@@ -82,11 +82,36 @@
android:label="@string/ui_activity_title"
android:theme="@android:style/Theme.Dialog"
android:excludeFromRecents="true"
android:configChanges="orientation"
>
<!--
No intent-filter here! This activity is only ever launched by
someone who explicitly knows the class name
-->
</activity>
<activity
android:name=".editor.ContactEditorActivity"
android:theme="@style/ContactEditTheme"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action
android:name="android.intent.action.INSERT" />
<data
android:mimeType="vnd.android.cursor.item/contact" />
</intent-filter>
<!--
Note that the editor gets a raw contact URI, but is expected to call
setResult with the corresponding aggregate contact URI, not raw contact
URI.
-->
<intent-filter>
<action
android:name="android.intent.action.EDIT" />
<data
android:mimeType="vnd.android.cursor.item/raw_contact" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -2,7 +2,8 @@
cloud-based service and synchronize its data with data stored locally in a
content provider. The sample uses two related parts of the Android framework
&mdash; the account manager and the synchronization manager (through a sync
adapter).</p>
adapter). It also demonstrates how to provide users the ability to create
and edit synchronized contacts using a custom editor.</p>
<p> The <a
href="../../../reference/android/accounts/AccountManager.html">account
@@ -26,7 +27,7 @@ AbstractThreadedSyncAdapter</a></code> abstract class and implementing the
issues a sync operation for that sync adapter. </p>
<p> The cloud-based service for this sample application is running at: </p>
<p style="margin-left:2em;">http://samplesyncadapter.appspot.com/users</p>
<p style="margin-left:2em;">http://samplesyncadapter2.appspot.com/</p>
<p>When you install this sample application, a new syncable "SampleSyncAdapter"
account will be added to your phone's account manager. You can go to "Settings |

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/EditPanelBackgroundColor" />
<stroke android:width="2dip" android:color="@color/EditPanelBorderColor" />
<padding android:left="5dip" android:top="5dip" android:right="5dip" android:bottom="5dip" />
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 B

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
/**
* Copyright (c) 2010, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal">
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="600dip"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@drawable/border">
<include layout="@layout/editor_header" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dip"
android:paddingTop="20dip"
android:paddingRight="20dip"
android:paddingBottom="20dip"
android:paddingLeft="20dip"
android:layout_weight="1">
<include layout="@layout/editor_fields" />
</ScrollView>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2010 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- Account info header -->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="64dip"
android:layout_width="match_parent"
android:background="?android:attr/selectableItemBackground">
<ImageView
android:id="@+id/header_account_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="7dip"
android:layout_marginRight="7dip"
android:layout_centerVertical="true"
android:layout_alignParentRight="true"
android:src="@drawable/icon" />
<TextView
android:id="@+id/header_account_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/header_account_icon"
android:layout_alignTop="@id/header_account_icon"
android:layout_marginTop="-4dip"
android:textSize="24sp"
android:textColor="?android:attr/textColorPrimary"
android:singleLine="true"
android:text="@string/header_account_type" />
<TextView
android:id="@+id/header_account_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toLeftOf="@+id/header_account_icon"
android:layout_alignBottom="@+id/header_account_icon"
android:layout_marginBottom="2dip"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary"
android:singleLine="true" />
</RelativeLayout>

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
/**
* Copyright (c) 2010, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dip"
android:paddingTop="20dip"
android:paddingRight="20dip"
android:paddingBottom="20dip"
android:paddingLeft="20dip"
android:layout_weight="1">
<include layout="@layout/editor_fields" />
</ScrollView>
</LinearLayout>

View File

@@ -0,0 +1,127 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
/**
* Copyright (c) 2010, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-->
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:stretchColumns="2">
<TableRow>
<TextView
android:layout_column="1"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_name"
android:padding="3dip" />
<EditText
android:layout_column="2"
android:id="@+id/editor_name"
android:singleLine="true"
android:inputType="textPersonName"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:minWidth="250dip"
android:scrollHorizontally="true"
android:capitalize="none"
android:textSize="@dimen/contact_name_text_size"
android:gravity="fill_horizontal"
android:autoText="false" />
</TableRow>
<TableRow>
<TextView
android:layout_column="1"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_phone_home"
android:padding="3dip" />
<EditText
android:id="@+id/editor_phone_home"
android:singleLine="true"
android:inputType="phone"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:minWidth="250dip"
android:scrollHorizontally="true"
android:capitalize="none"
android:gravity="fill_horizontal"
android:autoText="false" />
</TableRow>
<TableRow>
<TextView
android:layout_column="1"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_phone_mobile"
android:padding="3dip" />
<EditText
android:id="@+id/editor_phone_mobile"
android:singleLine="true"
android:inputType="phone"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:minWidth="250dip"
android:scrollHorizontally="true"
android:capitalize="none"
android:gravity="fill_horizontal"
android:autoText="false" />
</TableRow>
<TableRow>
<TextView
android:layout_column="1"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_phone_work"
android:padding="3dip" />
<EditText
android:id="@+id/editor_phone_work"
android:singleLine="true"
android:phoneNumber="true"
android:autoText="true"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:minWidth="250dip"
android:scrollHorizontally="true"
android:capitalize="none"
android:gravity="fill_horizontal" />
</TableRow>
<TableRow>
<TextView
android:layout_column="1"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_email"
android:padding="3dip" />
<EditText
android:id="@+id/editor_email"
android:singleLine="true"
android:inputType="textEmailAddress"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:minWidth="250dip"
android:scrollHorizontally="true"
android:capitalize="none"
android:gravity="fill_horizontal"
android:autoText="false" />
</TableRow>
</TableLayout>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2011 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_done"
android:alphabeticShortcut="\n"
android:icon="@drawable/done_menu_icon"
android:title="@string/menu_done"
android:showAsAction="always|withText" />
<item
android:id="@+id/menu_cancel"
android:alphabeticShortcut="q"
android:title="@string/menu_cancel"
android:showAsAction="always|withText" />
</menu>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/**
* Copyright (c) 2011, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-->
<resources>
<!-- Font size used for the contact name in the editor -->
<dimen name="contact_name_text_size">26sp</dimen>
</resources>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/**
* Copyright (c) 2011, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-->
<resources>
<!-- Font size used for the contact name in the editor -->
<dimen name="contact_name_text_size">18sp</dimen>
</resources>

View File

@@ -20,7 +20,7 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- Label for this package -->
<string
name="label">SamplesyncAdapter</string>
name="label">Sample SyncAdapter</string>
<!-- Permission label -->
<string
@@ -90,4 +90,16 @@
name="profile_action">Sample profile</string>
<string
name="view_profile">View Profile</string>
<string name="header_account_type">SampleSync contact</string>
<string name="label_name">Name</string>
<string name="label_phone_home">Home Phone</string>
<string name="label_phone_mobile">Mobile Phone</string>
<string name="label_phone_work">Work Phone</string>
<string name="label_email">Email</string>
<string
name="menu_done">Done</string>
<string
name="menu_cancel">Cancel</string>
</resources>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2011 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<!--
These styles will only be used in Honeycomb and later because
Android doesn't support third-party contact editing in pre-
Honeycomb versions.
-->
<color name="EditPanelBackgroundColor">#ffffff</color>
<color name="EditPanelBorderColor">#cccccc</color>
<style name="ContactEditTheme" parent="android:Theme.Holo.Light">
</style>
</resources>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/**
* Copyright (c) 2011, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-->
<ContactsAccountType
xmlns:android="http://schemas.android.com/apk/res/android"
editContactActivity="com.example.android.samplesync.editor.ContactEditorActivity"
createContactActivity="com.example.android.samplesync.editor.ContactEditorActivity"
>
<ContactsDataKind
android:mimeType="vnd.android.cursor.item/vnd.samplesyncadapter.profile"
android:icon="@drawable/icon"
android:summaryColumn="data2"
android:detailColumn="data3"
android:detailSocialSummary="true" />
</ContactsAccountType>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/**
* Copyright (c) 2011, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-->
<!--
The attributes in this XML file provide configuration information
for the SampleSyncAdapter.
See xml/syncadapter.xml for greater details, but this version of
the file specifies that uploading (and thus editing) is supported.
-->
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:contentAuthority="com.android.contacts"
android:accountType="com.example.android.samplesync"
android:supportsUploading="true"
android:userVisible="true"
/>

View File

@@ -17,7 +17,11 @@
*/
-->
<ContactsSource xmlns:android="http://schemas.android.com/apk/res/android">
<ContactsSource
xmlns:android="http://schemas.android.com/apk/res/android"
editContactActivity="com.example.android.samplesync.editor.ContactEditorActivity"
createContactActivity="com.example.android.samplesync.editor.ContactEditorActivity"
>
<ContactsDataKind
android:mimeType="vnd.android.cursor.item/vnd.samplesyncadapter.profile"

View File

@@ -17,11 +17,21 @@
*/
-->
<!-- The attributes in this XML file provide configuration information -->
<!-- for the SyncAdapter. -->
<!--
The attributes in this XML file provide configuration information
for the SampleSyncAdapter.
We have two versions of this file - one here, and one in the
xml-v11 directory (Honeycomb and beyond). This one specifies that
the syncadapter does not support uploading (and thus the contacts
associated with this syncadapter are not editable). The SDK 11
version of the file specifies that the adapter DOES support
uploading, so the contacts on SDK 11 and greater are editable.
-->
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:contentAuthority="com.android.contacts"
android:accountType="com.example.android.samplesync"
android:supportsUploading="false"
android:userVisible="true"
/>

View File

@@ -1,44 +1,59 @@
application: samplesyncadapter
# Copyright (C) 2010 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
application: samplesyncadapter2
version: 1
runtime: python
api_version: 1
handlers:
#
# Define a handler for our static files (css, images, etc)
#
- url: /static
static_dir: static
#
# Route all "web services" requests to the main.py file
#
- url: /auth
script: main.py
script: web_services.py
- url: /login
script: main.py
- url: /sync
script: web_services.py
- url: /fetch_friend_updates
script: main.py
- url: /reset_database
script: web_services.py
- url: /fetch_status
script: main.py
- url: /add_user
#
# Route all page requests to the dashboard.py file
#
- url: /
script: dashboard.py
- url: /edit_user
- url: /add_contact
script: dashboard.py
- url: /users
- url: /edit_contact
script: dashboard.py
- url: /delete_friend
- url: /delete_contact
script: dashboard.py
- url: /edit_user
- url: /avatar
script: dashboard.py
- url: /add_credentials
script: dashboard.py
- url: /user_credentials
script: dashboard.py
- url: /add_friend
script: dashboard.py
- url: /user_friends
- url: /edit_avatar
script: dashboard.py

View File

@@ -0,0 +1,24 @@
# Copyright (C) 2011 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
cron:
#
# Create a weekly cron job that cleans up the SampleSyncAdapter server database.
# We remove all existing contacts from the db, and create three initial
# contacts: Romeo, Juliet, and Tybalt.
#
- description: weekly cleanup job
url: /reset_database
schedule: every sunday 00:00
timezone: America/Los_Angeles

View File

@@ -14,8 +14,10 @@
# License for the specific language governing permissions and limitations under
# the License.
"""Defines Django forms for inserting/updating/viewing data
to/from SampleSyncAdapter datastore."""
"""
Defines Django forms for inserting/updating/viewing contact data
to/from SampleSyncAdapter datastore.
"""
import cgi
import datetime
@@ -26,248 +28,181 @@ from google.appengine.ext import webapp
from google.appengine.ext.webapp import template
from google.appengine.ext.db import djangoforms
from model import datastore
from google.appengine.api import images
import wsgiref.handlers
class BaseRequestHandler(webapp.RequestHandler):
"""
Base class for our page-based request handlers that contains
some helper functions we use in most pages.
"""
class UserForm(djangoforms.ModelForm):
"""Represents django form for entering user info."""
"""
Return a form (potentially partially filled-in) to
the user.
"""
def send_form(self, title, action, contactId, handle, content_obj):
if (contactId >= 0):
idInfo = '<input type="hidden" name="_id" value="%s">'
else:
idInfo = ''
class Meta:
model = datastore.User
template_values = {
'title': title,
'header': title,
'action': action,
'contactId': contactId,
'handle': handle,
'has_contactId': (contactId >= 0),
'has_handle': (handle != None),
'form_data_rows': str(content_obj)
}
path = os.path.join(os.path.dirname(__file__), 'templates', 'simple_form.html')
self.response.out.write(template.render(path, template_values))
class ContactForm(djangoforms.ModelForm):
"""Represents django form for entering contact info."""
class Meta:
model = datastore.Contact
class UserInsertPage(webapp.RequestHandler):
"""Inserts new users. GET presents a blank form. POST processes it."""
class ContactInsertPage(BaseRequestHandler):
"""
Processes requests to add a new contact. GET presents an empty
contact form for the user to fill in. POST saves the new contact
with the POSTed information.
"""
def get(self):
self.response.out.write('<html><body>'
'<form method="POST" '
'action="/add_user">'
'<table>')
# This generates our shopping list form and writes it in the response
self.response.out.write(UserForm())
self.response.out.write('</table>'
'<input type="submit">'
'</form></body></html>')
def get(self):
self.send_form('Add Contact', '/add_contact', -1, None, ContactForm())
def post(self):
data = UserForm(data=self.request.POST)
if data.is_valid():
# Save the data, and redirect to the view page
entity = data.save(commit=False)
entity.put()
self.redirect('/users')
else:
# Reprint the form
self.response.out.write('<html><body>'
'<form method="POST" '
'action="/">'
'<table>')
self.response.out.write(data)
self.response.out.write('</table>'
'<input type="submit">'
'</form></body></html>')
def post(self):
data = ContactForm(data=self.request.POST)
if data.is_valid():
# Save the data, and redirect to the view page
entity = data.save(commit=False)
entity.put()
self.redirect('/')
else:
# Reprint the form
self.send_form('Add Contact', '/add_contact', -1, None, data)
class UserEditPage(webapp.RequestHandler):
"""Edits users. GET presents a form prefilled with user info
from datastore. POST processes it."""
class ContactEditPage(BaseRequestHandler):
"""
Process requests to edit a contact's information. GET presents a form
with the current contact information filled in. POST saves new information
into the contact record.
"""
def get(self):
id = int(self.request.get('user'))
user = datastore.User.get(db.Key.from_path('User', id))
self.response.out.write('<html><body>'
'<form method="POST" '
'action="/edit_user">'
'<table>')
# This generates our shopping list form and writes it in the response
self.response.out.write(UserForm(instance=user))
self.response.out.write('</table>'
'<input type="hidden" name="_id" value="%s">'
'<input type="submit">'
'</form></body></html>' % id)
def get(self):
id = int(self.request.get('id'))
contact = datastore.Contact.get(db.Key.from_path('Contact', id))
self.send_form('Edit Contact', '/edit_contact', id, contact.handle,
ContactForm(instance=contact))
def post(self):
id = int(self.request.get('_id'))
user = datastore.User.get(db.Key.from_path('User', id))
data = UserForm(data=self.request.POST, instance=user)
if data.is_valid():
# Save the data, and redirect to the view page
entity = data.save(commit=False)
entity.updated = datetime.datetime.utcnow()
entity.put()
self.redirect('/users')
else:
# Reprint the form
self.response.out.write('<html><body>'
'<form method="POST" '
'action="/edit_user">'
'<table>')
self.response.out.write(data)
self.response.out.write('</table>'
'<input type="hidden" name="_id" value="%s">'
'<input type="submit">'
'</form></body></html>' % id)
def post(self):
id = int(self.request.get('id'))
contact = datastore.Contact.get(db.Key.from_path('Contact', id))
data = ContactForm(data=self.request.POST, instance=contact)
if data.is_valid():
# Save the data, and redirect to the view page
entity = data.save(commit=False)
entity.updated = datetime.datetime.utcnow()
entity.put()
self.redirect('/')
else:
# Reprint the form
self.send_form('Edit Contact', '/edit_contact', id, contact.handle, data)
class ContactDeletePage(BaseRequestHandler):
"""Processes delete contact request."""
class UsersListPage(webapp.RequestHandler):
"""Lists all Users. In addition displays links for editing user info,
viewing user's friends and adding new users."""
def get(self):
id = int(self.request.get('id'))
contact = datastore.Contact.get(db.Key.from_path('Contact', id))
contact.deleted = True
contact.updated = datetime.datetime.utcnow()
contact.put()
def get(self):
users = datastore.User.all()
template_values = {
'users': users
}
self.redirect('/')
path = os.path.join(os.path.dirname(__file__), 'templates', 'users.html')
self.response.out.write(template.render(path, template_values))
class AvatarEditPage(webapp.RequestHandler):
"""
Processes requests to edit contact's avatar. GET is used to fetch
a page that displays the contact's current avatar and allows the user
to specify a file containing a new avatar image. POST is used to
submit the form which will change the contact's avatar.
"""
def get(self):
id = int(self.request.get('id'))
contact = datastore.Contact.get(db.Key.from_path('Contact', id))
template_values = {
'avatar': contact.avatar,
'contactId': id
}
class UserCredentialsForm(djangoforms.ModelForm):
"""Represents django form for entering user's credentials."""
path = os.path.join(os.path.dirname(__file__), 'templates', 'edit_avatar.html')
self.response.out.write(template.render(path, template_values))
class Meta:
model = datastore.UserCredentials
def post(self):
id = int(self.request.get('id'))
contact = datastore.Contact.get(db.Key.from_path('Contact', id))
#avatar = images.resize(self.request.get("avatar"), 128, 128)
avatar = self.request.get("avatar")
contact.avatar = db.Blob(avatar)
contact.updated = datetime.datetime.utcnow()
contact.put()
self.redirect('/')
class AvatarViewPage(BaseRequestHandler):
"""
Processes request to view contact's avatar. This is different from
the GET AvatarEditPage request in that this doesn't return a page -
it just returns the raw image itself.
"""
class UserCredentialsInsertPage(webapp.RequestHandler):
"""Inserts user credentials. GET shows a blank form, POST processes it."""
def get(self):
id = int(self.request.get('id'))
contact = datastore.Contact.get(db.Key.from_path('Contact', id))
if (contact.avatar):
self.response.headers['Content-Type'] = "image/png"
self.response.out.write(contact.avatar)
else:
self.redirect(self.request.host_url + '/static/img/default_avatar.gif')
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>')
class ContactsListPage(webapp.RequestHandler):
"""
Display a page that lists all the contacts associated with
the specifies user account.
"""
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>')
def get(self):
contacts = datastore.Contact.all()
template_values = {
'contacts': contacts,
'username': 'user'
}
class UserFriendsForm(djangoforms.ModelForm):
"""Represents django form for entering user's friends."""
class Meta:
model = datastore.UserFriends
exclude = ['deleted', 'username']
class UserFriendsInsertPage(webapp.RequestHandler):
"""Inserts user's new friends. GET shows a blank form, POST processes it."""
def get(self):
user = self.request.get('user')
self.response.out.write('<html><body>'
'<form method="POST" '
'action="/add_friend">'
'<table>')
# This generates our shopping list form and writes it in the response
self.response.out.write(UserFriendsForm())
self.response.out.write('</table>'
'<input type = hidden name = "user" value = "%s">'
'<input type="submit">'
'</form></body></html>' % user)
def post(self):
data = UserFriendsForm(data=self.request.POST)
if data.is_valid():
user = self.request.get('user')
# Save the data, and redirect to the view page
entity = data.save(commit=False)
entity.username = user
query = datastore.UserFriends.all()
query.filter('username = ', user)
query.filter('friend_handle = ', entity.friend_handle)
result = query.get()
if result:
result.deleted = False
result.updated = datetime.datetime.utcnow()
result.put()
else:
entity.deleted = False
entity.put()
self.redirect('/user_friends?user=' + user)
else:
# Reprint the form
self.response.out.write('<html><body>'
'<form method="POST" '
'action="/add_friend">'
'<table>')
self.response.out.write(data)
self.response.out.write('</table>'
'<input type="submit">'
'</form></body></html>')
class UserFriendsListPage(webapp.RequestHandler):
"""Lists all friends for a user. In addition displays links for removing
friends and adding new friends."""
def get(self):
user = self.request.get('user')
query = datastore.UserFriends.all()
query.filter('deleted = ', False)
query.filter('username = ', user)
friends = query.fetch(50)
template_values = {
'friends': friends,
'user': user
}
path = os.path.join(os.path.dirname(__file__),
'templates', 'view_friends.html')
self.response.out.write(template.render(path, template_values))
class DeleteFriendPage(webapp.RequestHandler):
"""Processes delete friend request."""
def get(self):
user = self.request.get('user')
friend = self.request.get('friend')
query = datastore.UserFriends.all()
query.filter('username =', user)
query.filter('friend_handle =', friend)
result = query.get()
result.deleted = True
result.updated = datetime.datetime.utcnow()
result.put()
self.redirect('/user_friends?user=' + user)
path = os.path.join(os.path.dirname(__file__), 'templates', 'contacts.html')
self.response.out.write(template.render(path, template_values))
def main():
application = webapp.WSGIApplication(
[('/add_user', UserInsertPage),
('/users', UsersListPage),
('/add_credentials', UserCredentialsInsertPage),
('/add_friend', UserFriendsInsertPage),
('/user_friends', UserFriendsListPage),
('/delete_friend', DeleteFriendPage),
('/edit_user', UserEditPage)
],
debug=True)
wsgiref.handlers.CGIHandler().run(application)
application = webapp.WSGIApplication(
[('/', ContactsListPage),
('/add_contact', ContactInsertPage),
('/edit_contact', ContactEditPage),
('/delete_contact', ContactDeletePage),
('/avatar', AvatarViewPage),
('/edit_avatar', AvatarEditPage)
],
debug=True)
wsgiref.handlers.CGIHandler().run(application)
if __name__ == '__main__':
main()

View File

@@ -1,14 +1,15 @@
indexes:
# Copyright (C) 2010 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
# This index.yaml is automatically updated whenever the dev_appserver
# detects that a new type of query is run. If you want to manage the
# index.yaml file manually, remove the above marker line (the line
# saying "# AUTOGENERATED"). If you want to manage some indexes
# manually, move them above the marker line. The index.yaml file is
# automatically uploaded to the admin console when you next deploy
# your application using appcfg.py.
- kind: UserFriends
properties:
- name: username
- name: updated
# AUTOGENERATED

View File

@@ -1,173 +0,0 @@
#!/usr/bin/python2.5
# Copyright (C) 2010 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Handlers for Sample SyncAdapter services.
Contains several RequestHandler subclasses used to handle post operations.
This script is designed to be run directly as a WSGI application.
Authenticate: Handles user requests for authentication.
FetchFriends: Handles user requests for friend list.
FriendData: Stores information about user's friends.
"""
import cgi
from datetime import datetime
from django.utils import simplejson
from google.appengine.api import users
from google.appengine.ext import db
from google.appengine.ext import webapp
from model import datastore
import wsgiref.handlers
class Authenticate(webapp.RequestHandler):
"""Handles requests for login and authentication.
UpdateHandler only accepts post events. It expects each
request to include username and password fields. It returns authtoken
after successful authentication and "invalid credentials" error otherwise.
"""
def post(self):
self.username = self.request.get('username')
self.password = self.request.get('password')
password = datastore.UserCredentials.get(self.username)
if password == self.password:
self.response.set_status(200, 'OK')
# return the password as AuthToken
self.response.out.write(password)
else:
self.response.set_status(401, 'Invalid Credentials')
class FetchFriends(webapp.RequestHandler):
"""Handles requests for fetching user's friendlist.
UpdateHandler only accepts post events. It expects each
request to include username and authtoken. If the authtoken is valid
it returns user's friend info in JSON format.It uses helper
class FriendData to fetch user's friendlist.
"""
def post(self):
self.username = self.request.get('username')
self.password = self.request.get('password')
self.timestamp = None
timestamp = self.request.get('timestamp')
if timestamp:
self.timestamp = datetime.strptime(timestamp, '%Y/%m/%d %H:%M')
password = datastore.UserCredentials.get(self.username)
if password == self.password:
self.friend_list = []
friends = datastore.UserFriends.get_friends(self.username)
if friends:
for friend in friends:
friend_handle = getattr(friend, 'friend_handle')
if self.timestamp is None or getattr(friend, 'updated') > self.timestamp:
if (getattr(friend, 'deleted')) == True:
friend = {}
friend['u'] = friend_handle
friend['d'] = 'true'
friend['i'] = str(datastore.User.get_user_id(friend_handle))
self.friend_list.append(friend)
else:
FriendsData(self.friend_list, friend_handle)
else:
if datastore.User.get_user_last_updated(friend_handle) > self.timestamp:
FriendsData(self.friend_list, friend_handle)
self.response.set_status(200)
self.response.out.write(toJSON(self.friend_list))
else:
self.response.set_status(401, 'Invalid Credentials')
class FetchStatus(webapp.RequestHandler):
"""Handles requests fetching friend statuses.
UpdateHandler only accepts post events. It expects each
request to include username and authtoken. If the authtoken is valid
it returns status info in JSON format.
"""
def post(self):
self.username = self.request.get('username')
self.password = self.request.get('password')
password = datastore.UserCredentials.get(self.username)
if password == self.password:
self.status_list = []
friends = datastore.UserFriends.get_friends(self.username)
if friends:
for friend in friends:
friend_handle = getattr(friend, 'friend_handle')
status_text = datastore.User.get_user_status(friend_handle)
user_id = datastore.User.get_user_id(friend_handle)
status = {}
status['i'] = str(user_id)
status['s'] = status_text
self.status_list.append(status)
self.response.set_status(200)
self.response.out.write(toJSON(self.status_list))
else:
self.response.set_status(401, 'Invalid Credentials')
def toJSON(self):
"""Dumps the data represented by the object to JSON for wire transfer."""
return simplejson.dumps(self.friend_list)
def toJSON(object):
"""Dumps the data represented by the object to JSON for wire transfer."""
return simplejson.dumps(object)
class FriendsData(object):
"""Holds data for user's friends.
This class knows how to serialize itself to JSON.
"""
__FIELD_MAP = {
'handle': 'u',
'firstname': 'f',
'lastname': 'l',
'status': 's',
'phone_home': 'h',
'phone_office': 'o',
'phone_mobile': 'm',
'email': 'e',
}
def __init__(self, friend_list, username):
obj = datastore.User.get_user_info(username)
friend = {}
for obj_name, json_name in self.__FIELD_MAP.items():
if hasattr(obj, obj_name):
friend[json_name] = str(getattr(obj, obj_name))
friend['i'] = str(obj.key().id())
friend_list.append(friend)
def main():
application = webapp.WSGIApplication(
[('/auth', Authenticate),
('/login', Authenticate),
('/fetch_friend_updates', FetchFriends),
('/fetch_status', FetchStatus),
],
debug=True)
wsgiref.handlers.CGIHandler().run(application)
if __name__ == "__main__":
main()

View File

@@ -14,80 +14,51 @@
# License for the specific language governing permissions and limitations under
# the License.
"""Represents user's contact information, friends and credentials."""
"""Represents user's contact information"""
from google.appengine.ext import db
class User(db.Model):
class Contact(db.Model):
"""Data model class to hold user objects."""
handle = db.StringProperty(required=True)
firstname = db.TextProperty()
lastname = db.TextProperty()
status = db.TextProperty()
firstname = db.StringProperty()
lastname = db.StringProperty()
phone_home = db.PhoneNumberProperty()
phone_office = db.PhoneNumberProperty()
phone_mobile = db.PhoneNumberProperty()
email = db.EmailProperty()
status = db.TextProperty()
avatar = db.BlobProperty()
deleted = db.BooleanProperty()
updated = db.DateTimeProperty(auto_now_add=True)
@classmethod
def get_user_info(cls, username):
def get_contact_info(cls, username):
if username not in (None, ''):
query = cls.gql('WHERE handle = :1', username)
return query.get()
return None
@classmethod
def get_user_last_updated(cls, username):
def get_contact_last_updated(cls, username):
if username not in (None, ''):
query = cls.gql('WHERE handle = :1', username)
return query.get().updated
return None
@classmethod
def get_user_id(cls, username):
def get_contact_id(cls, username):
if username not in (None, ''):
query = cls.gql('WHERE handle = :1', username)
return query.get().key().id()
return None
@classmethod
def get_user_status(cls, username):
def get_contact_status(cls, username):
if username not in (None, ''):
query = cls.gql('WHERE handle = :1', username)
return query.get().status
return None
class UserCredentials(db.Model):
"""Data model class to hold credentials for a Voiper user."""
username = db.StringProperty(required=True)
password = db.StringProperty()
@classmethod
def get(cls, username):
if username not in (None, ''):
query = cls.gql('WHERE username = :1', username)
return query.get().password
return None
class UserFriends(db.Model):
"""Data model class to hold user's friendlist info."""
username = db.StringProperty()
friend_handle = db.StringProperty(required=True)
updated = db.DateTimeProperty(auto_now_add=True)
deleted = db.BooleanProperty()
@classmethod
def get_friends(cls, username):
if username not in (None, ''):
query = cls.gql('WHERE username = :1', username)
friends = query.fetch(50)
return friends
return None

View File

@@ -0,0 +1,77 @@
/**
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
html,body {
height: 100%;
}
body {
padding: 20px;
margin: 0;
background-color: #fff;
font-family: Verdana, Arial, Helvetica, sans-serif;
text-align: left;
}
a {
color: #0033cc;
}
h1 {
font-family: Arial;
font-weight: normal;
border-bottom: solid 1px #ccc;
margin: 0 0 20px 0;
padding: 0 0 5px 0;
}
h3 {
font-family: Arial;
font-weight: bold;
line-height: 2em;
}
table {
border-collapse: collapse;
margin-bottom: 20px;
}
th,td {
padding: 5px 8px;
text-align: left;
}
td.center {
text-align: center;
}
.deleted td {
text-decoration: line-through;
}
.data th {
font-weight: normal;
border-bottom: solid 1px #000;
}
.data td {
border-bottom: solid 1px #eee;
}
.form th {
font-weight: normal;
text-align: right;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
<!--
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
-->
<head>
<title>SampleSync: Contacts for '{{ username }}'</title>
<link type="text/css" rel="stylesheet" href="/static/css/main.css" media="screen" />
</head>
<body>
<h1>SampleSync: Contacts for '{{ username }}'</h1>
<table class="data" cellpadding="0" cellspacing="0">
<tr>
<th>&nbsp;</th>
<th>Id</th>
<th>Name</th>
<th>Email</th>
<th>Home</th>
<th>Office</th>
<th>Mobile</th>
<th>Status</th>
</tr>
{% for contact in contacts %}
<tr {% if contact.deleted %} class="deleted" {% endif %}>
<td class="center">
<a href="/edit_avatar?id={{ contact.key.id }}"><img src="/avatar?id={{ contact.key.id }}" height="25" width="25" /></a>
</td>
<td><a href="/edit_contact?id={{ contact.key.id }}">{{ contact.key.id }}</a></td>
<td><a href="/edit_contact?id={{ contact.key.id }}">{{ contact.firstname }} {{ contact.lastname }}</a></td>
<td>{{ contact.email }}</td>
<td>{{ contact.phone_home }}</td>
<td>{{ contact.phone_office }}</td>
<td>{{ contact.phone_mobile }}</td>
<td><span style="whitespace: no-wrap;">{{ contact.status }}</span></td>
</tr>
{% endfor %}
</table>
<a href = "/add_contact">Add Contact</a>
</body>
</html>

View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
<!--
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
-->
<head>
<title>SampleSync: Edit Picture</title>
<link type="text/css" rel="stylesheet" href="/static/css/main.css" media="screen" />
</head>
<body>
<h1>SampleSync: Edit Picture</h1>
<form method="POST" action="/edit_avatar" enctype="multipart/form-data">
<h3>Current Avatar:</h3>
<blockquote>
{% if avatar %}
<img src="/avatar?id={{ contactId }}" />
{% else %}
<i>You haven't added a picture for this friend...</i>
{% endif %}
</blockquote>
<h3>New Avatar:</h3>
<p>Please select a file containing the image you'd like to use for this friend</p>
<input type="file" name="avatar" />
<p>&nbsp;</p>
<input type="submit" name="Save" value="Save Changes" />
<input type="button" name="Cancel" value="Cancel" onclick="document.location='/';return false;" />
<input type="hidden" name="id" value="{{ contactId }}" />
</form>
</body>
</html>

View File

@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
<!--
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
-->
<head>
<title>SampleSync: {{ title }}</title>
<link type="text/css" rel="stylesheet" href="/static/css/main.css" media="screen" />
</head>
<body>
<h1>SampleSync: {{ header }}</h1>
<form method="POST" action="{{ action }}">
<table class="form" cellpadding="0" cellspacing="0">
{{ form_data_rows }}
</table>
<input type="submit" name="Save" value="Save Changes" />
<input type="button" name="Cancel" value="Cancel" onclick="document.location='/';return false;" />
{% if has_contactId %}
<input type="hidden" name="id" value="{{ contactId }}" />
{% endif %}
{% if has_handle %}
<input type="hidden" name="username" value="{{ handle }}" />
{% endif %}
</form>
</body>
</html>

View File

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

View File

@@ -1,17 +0,0 @@
<html>
<body>
<h1> Sample Sync Adapter </h1>
<p>
{{user}}'s friends
<table>
{% for friend in friends %}
<tr><td>
{{ friend.friend_handle }} </td><td> <a href="/delete_friend?user={{ user }}&friend={{friend.friend_handle}}">Remove</a>
</td></tr>
{% endfor %}
</table>
</p>
<a href = "/add_friend?user={{user}}"> Add More </a>

View File

@@ -0,0 +1,400 @@
#!/usr/bin/python2.5
# Copyright (C) 2010 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""
Handlers for Sample SyncAdapter services.
Contains several RequestHandler subclasses used to handle post operations.
This script is designed to be run directly as a WSGI application.
"""
import cgi
import logging
import time as _time
from datetime import datetime
from django.utils import simplejson
from google.appengine.api import users
from google.appengine.ext import db
from google.appengine.ext import webapp
from model import datastore
import wsgiref.handlers
class BaseWebServiceHandler(webapp.RequestHandler):
"""
Base class for our web services. We put some common helper
functions here.
"""
"""
Since we're only simulating a single user account, declare our
hard-coded credentials here, so that they're easy to see/find.
We actually accept any and all usernames that start with this
hard-coded values. So if ACCT_USER_NAME is 'user', then we'll
accept 'user', 'user75', 'userbuddy', etc, all as legal account
usernames.
"""
ACCT_USER_NAME = 'user'
ACCT_PASSWORD = 'test'
ACCT_AUTH_TOKEN = 'xyzzy'
DATE_TIME_FORMAT = '%Y/%m/%d %H:%M'
"""
Process a request to authenticate a client. We assume that the username
and password will be included in the request. If successful, we'll return
an authtoken as the only content. If auth fails, we'll send an "invalid
credentials" error.
We return a boolean indicating whether we were successful (true) or not (false).
In the event that this call fails, we will setup the response, so callers just
need to RETURN in the error case.
"""
def authenticate(self):
self.username = self.request.get('username')
self.password = self.request.get('password')
logging.info('Authenticatng username: ' + self.username)
if ((self.username != None) and
(self.username.startswith(BaseWebServiceHandler.ACCT_USER_NAME)) and
(self.password == BaseWebServiceHandler.ACCT_PASSWORD)):
# Authentication was successful - return our hard-coded
# auth-token as the only response.
self.response.set_status(200, 'OK')
self.response.out.write(BaseWebServiceHandler.ACCT_AUTH_TOKEN)
return True
else:
# Authentication failed. Return the standard HTTP auth failure
# response to let the client know.
self.response.set_status(401, 'Invalid Credentials')
return False
"""
Validate the credentials of the client for a web service request.
The request should include username/password parameters that correspond
to our hard-coded single account values.
We return a boolean indicating whether we were successful (true) or not (false).
In the event that this call fails, we will setup the response, so callers just
need to RETURN in the error case.
"""
def validate(self):
self.username = self.request.get('username')
self.authtoken = self.request.get('authtoken')
logging.info('Validating username: ' + self.username)
if ((self.username != None) and
(self.username.startswith(BaseWebServiceHandler.ACCT_USER_NAME)) and
(self.authtoken == BaseWebServiceHandler.ACCT_AUTH_TOKEN)):
return True
else:
self.response.set_status(401, 'Invalid Credentials')
return False
class Authenticate(BaseWebServiceHandler):
"""
Handles requests for login and authentication.
UpdateHandler only accepts post events. It expects each
request to include username and password fields. It returns authtoken
after successful authentication and "invalid credentials" error otherwise.
"""
def post(self):
self.authenticate()
def get(self):
"""Used for debugging in a browser..."""
self.post()
class SyncContacts(BaseWebServiceHandler):
"""Handles requests for fetching user's contacts.
UpdateHandler only accepts post events. It expects each
request to include username and authtoken. If the authtoken is valid
it returns user's contact info in JSON format.
"""
def get(self):
"""Used for debugging in a browser..."""
self.post()
def post(self):
logging.info('*** Starting contact sync ***')
if (not self.validate()):
return
updated_contacts = []
# Process any client-side changes sent up in the request.
# Any new contacts that were added are included in the
# updated_contacts list, so that we return them to the
# client. That way, the client can see the serverId of
# the newly added contact.
client_buffer = self.request.get('contacts')
if ((client_buffer != None) and (client_buffer != '')):
self.process_client_changes(client_buffer, updated_contacts)
# Add any contacts that have been updated on the server-side
# since the last sync by this client.
client_state = self.request.get('syncstate')
self.get_updated_contacts(client_state, updated_contacts)
logging.info('Returning ' + str(len(updated_contacts)) + ' contact records')
# Return the list of updated contacts to the client
self.response.set_status(200)
self.response.out.write(toJSON(updated_contacts))
def get_updated_contacts(self, client_state, updated_contacts):
logging.info('* Processing server changes')
timestamp = None
base_url = self.request.host_url
# The client sends the last high-water-mark that they successfully
# sync'd to in the syncstate parameter. It's opaque to them, but
# its actually a seconds-in-unix-epoch timestamp that we use
# as a baseline.
if client_state:
logging.info('Client sync state: ' + client_state)
timestamp = datetime.utcfromtimestamp(float(client_state))
# Keep track of the update/delete counts, so we can log it
# below. Makes debugging easier...
update_count = 0
delete_count = 0
contacts = datastore.Contact.all()
if contacts:
# Find the high-water mark for the most recently updated friend.
# We'll return this as the syncstate (x) value for all the friends
# we return from this function.
high_water_date = datetime.min
for contact in contacts:
if (contact.updated > high_water_date):
high_water_date = contact.updated
high_water_mark = str(long(_time.mktime(high_water_date.utctimetuple())) + 1)
logging.info('New sync state: ' + high_water_mark)
# Now build the updated_contacts containing all the friends that have been
# changed since the last sync
for contact in contacts:
# If our list of contacts we're returning already contains this
# contact (for example, it's a contact just uploaded from the client)
# then don't bother processing it any further...
if (self.list_contains_contact(updated_contacts, contact)):
continue
handle = contact.handle
if timestamp is None or contact.updated > timestamp:
if contact.deleted == True:
delete_count = delete_count + 1
DeletedContactData(updated_contacts, handle, high_water_mark)
else:
update_count = update_count + 1
UpdatedContactData(updated_contacts, handle, None, base_url, high_water_mark)
logging.info('Server-side updates: ' + str(update_count))
logging.info('Server-side deletes: ' + str(delete_count))
def process_client_changes(self, contacts_buffer, updated_contacts):
logging.info('* Processing client changes: ' + self.username)
base_url = self.request.host_url
# Build an array of generic objects containing contact data,
# using the Django built-in JSON parser
logging.info('Uploaded contacts buffer: ' + contacts_buffer)
json_list = simplejson.loads(contacts_buffer)
logging.info('Client-side updates: ' + str(len(json_list)))
# Keep track of the number of new contacts the client sent to us,
# so that we can log it below.
new_contact_count = 0
for jcontact in json_list:
new_contact = False
id = self.safe_attr(jcontact, 'i')
if (id != None):
logging.info('Updating contact: ' + str(id))
contact = datastore.Contact.get(db.Key.from_path('Contact', id))
else:
logging.info('Creating new contact record')
new_contact = True
contact = datastore.Contact(handle='temp')
# If the 'change' for this contact is that they were deleted
# on the client-side, all we want to do is set the deleted
# flag here, and we're done.
if (self.safe_attr(jcontact, 'd') == True):
contact.deleted = True
contact.put()
logging.info('Deleted contact: ' + contact.handle)
continue
contact.firstname = self.safe_attr(jcontact, 'f')
contact.lastname = self.safe_attr(jcontact, 'l')
contact.phone_home = self.safe_attr(jcontact, 'h')
contact.phone_office = self.safe_attr(jcontact, 'o')
contact.phone_mobile = self.safe_attr(jcontact, 'm')
contact.email = self.safe_attr(jcontact, 'e')
contact.deleted = (self.safe_attr(jcontact, 'd') == 'true')
if (new_contact):
# New record - add them to db...
new_contact_count = new_contact_count + 1
contact.handle = contact.firstname + '_' + contact.lastname
logging.info('Created new contact handle: ' + contact.handle)
contact.put()
logging.info('Saved contact: ' + contact.handle)
# We don't save off the client_id value (thus we add it after
# the "put"), but we want it to be in the JSON object we
# serialize out, so that the client can match this contact
# up with the client version.
client_id = self.safe_attr(jcontact, 'c')
# Create a high-water-mark for sync-state from the 'updated' time
# for this contact, so we return the correct value to the client.
high_water = str(long(_time.mktime(contact.updated.utctimetuple())) + 1)
# Add new contacts to our updated_contacts, so that we return them
# to the client (so the client gets the serverId for the
# added contact)
if (new_contact):
UpdatedContactData(updated_contacts, contact.handle, client_id, base_url,
high_water)
logging.info('Client-side adds: ' + str(new_contact_count))
def list_contains_contact(self, contact_list, contact):
if (contact is None):
return False
contact_id = str(contact.key().id())
for next in contact_list:
if ((next != None) and (next['i'] == contact_id)):
return True
return False
def safe_attr(self, obj, attr_name):
if attr_name in obj:
return obj[attr_name]
return None
class ResetDatabase(BaseWebServiceHandler):
"""
Handles cron request to reset the contact database.
We have a weekly cron task that resets the database back to a
few contacts, so that it doesn't grow to an absurd size.
"""
def get(self):
# Delete all the existing contacts from the database
contacts = datastore.Contact.all()
for contact in contacts:
contact.delete()
# Now create three sample contacts
contact1 = datastore.Contact(handle = 'juliet',
firstname = 'Juliet',
lastname = 'Capulet',
phone_mobile = '(650) 555-1000',
phone_home = '(650) 555-1001',
status = 'Wherefore art thou Romeo?')
contact1.put()
contact2 = datastore.Contact(handle = 'romeo',
firstname = 'Romeo',
lastname = 'Montague',
phone_mobile = '(650) 555-2000',
phone_home = '(650) 555-2001',
status = 'I dream\'d a dream to-night')
contact2.put()
contact3 = datastore.Contact(handle = 'tybalt',
firstname = 'Tybalt',
lastname = 'Capulet',
phone_mobile = '(650) 555-3000',
phone_home = '(650) 555-3001',
status = 'Have at thee, coward')
contact3.put()
def toJSON(object):
"""Dumps the data represented by the object to JSON for wire transfer."""
return simplejson.dumps(object)
class UpdatedContactData(object):
"""Holds data for user's contacts.
This class knows how to serialize itself to JSON.
"""
__FIELD_MAP = {
'handle': 'u',
'firstname': 'f',
'lastname': 'l',
'status': 's',
'phone_home': 'h',
'phone_office': 'o',
'phone_mobile': 'm',
'email': 'e',
'client_id': 'c'
}
def __init__(self, contact_list, username, client_id, host_url, high_water_mark):
obj = datastore.Contact.get_contact_info(username)
contact = {}
for obj_name, json_name in self.__FIELD_MAP.items():
if hasattr(obj, obj_name):
v = getattr(obj, obj_name)
if (v != None):
contact[json_name] = str(v)
else:
contact[json_name] = None
contact['i'] = str(obj.key().id())
contact['a'] = host_url + "/avatar?id=" + str(obj.key().id())
contact['x'] = high_water_mark
if (client_id != None):
contact['c'] = str(client_id)
contact_list.append(contact)
class DeletedContactData(object):
def __init__(self, contact_list, username, high_water_mark):
obj = datastore.Contact.get_contact_info(username)
contact = {}
contact['d'] = 'true'
contact['i'] = str(obj.key().id())
contact['x'] = high_water_mark
contact_list.append(contact)
def main():
application = webapp.WSGIApplication(
[('/auth', Authenticate),
('/sync', SyncContacts),
('/reset_database', ResetDatabase),
],
debug=True)
wsgiref.handlers.CGIHandler().run(application)
if __name__ == "__main__":
main()

View File

@@ -13,6 +13,7 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.samplesync.authenticator;
import android.app.Service;
@@ -49,7 +50,7 @@ public class AuthenticationService extends Service {
public IBinder onBind(Intent intent) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "getBinder()... returning the AccountAuthenticator binder for intent "
+ intent);
+ intent);
}
return mAuthenticator.getIBinder();
}

View File

@@ -13,26 +13,45 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.samplesync.authenticator;
import com.example.android.samplesync.Constants;
import com.example.android.samplesync.client.NetworkUtilities;
import android.accounts.AbstractAccountAuthenticator;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.accounts.NetworkErrorException;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import com.example.android.samplesync.Constants;
import com.example.android.samplesync.R;
import com.example.android.samplesync.client.NetworkUtilities;
import android.text.TextUtils;
import android.util.Log;
/**
* This class is an implementation of AbstractAccountAuthenticator for
* authenticating accounts in the com.example.android.samplesync domain.
* authenticating accounts in the com.example.android.samplesync domain. The
* interesting thing that this class demonstrates is the use of authTokens as
* part of the authentication process. In the account setup UI, the user enters
* their username and password. But for our subsequent calls off to the service
* for syncing, we want to use an authtoken instead - so we're not continually
* sending the password over the wire. getAuthToken() will be called when
* SyncAdapter calls AccountManager.blockingGetAuthToken(). When we get called,
* we need to return the appropriate authToken for the specified account. If we
* already have an authToken stored in the account, we return that authToken. If
* we don't, but we do have a username and password, then we'll attempt to talk
* to the sample service to fetch an authToken. If that fails (or we didn't have
* a username/password), then we need to prompt the user - so we create an
* AuthenticatorActivity intent and return that. That will display the dialog
* that prompts the user for their login information.
*/
class Authenticator extends AbstractAccountAuthenticator {
/** The tag used to log to adb console. **/
private static final String TAG = "Authenticator";
// Authentication Service context
private final Context mContext;
@@ -43,10 +62,9 @@ class Authenticator extends AbstractAccountAuthenticator {
@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType,
String authTokenType, String[] requiredFeatures, Bundle options) {
String authTokenType, String[] requiredFeatures, Bundle options) {
Log.v(TAG, "addAccount()");
final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
intent.putExtra(AuthenticatorActivity.PARAM_AUTHTOKEN_TYPE, authTokenType);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
final Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
@@ -54,54 +72,49 @@ class Authenticator extends AbstractAccountAuthenticator {
}
@Override
public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account,
Bundle options) {
if (options != null && options.containsKey(AccountManager.KEY_PASSWORD)) {
final String password = options.getString(AccountManager.KEY_PASSWORD);
final boolean verified = onlineConfirmPassword(account.name, password);
final Bundle result = new Bundle();
result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, verified);
return result;
}
// Launch AuthenticatorActivity to confirm credentials
final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
intent.putExtra(AuthenticatorActivity.PARAM_USERNAME, account.name);
intent.putExtra(AuthenticatorActivity.PARAM_CONFIRM_CREDENTIALS, true);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
final Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
public Bundle confirmCredentials(
AccountAuthenticatorResponse response, Account account, Bundle options) {
Log.v(TAG, "confirmCredentials()");
return null;
}
@Override
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
Log.v(TAG, "editProperties()");
throw new UnsupportedOperationException();
}
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
String authTokenType, Bundle loginOptions) {
String authTokenType, Bundle loginOptions) throws NetworkErrorException {
Log.v(TAG, "getAuthToken()");
// If the caller requested an authToken type we don't support, then
// return an error
if (!authTokenType.equals(Constants.AUTHTOKEN_TYPE)) {
final Bundle result = new Bundle();
result.putString(AccountManager.KEY_ERROR_MESSAGE, "invalid authTokenType");
return result;
}
// Extract the username and password from the Account Manager, and ask
// the server for an appropriate AuthToken.
final AccountManager am = AccountManager.get(mContext);
final String password = am.getPassword(account);
if (password != null) {
final boolean verified = onlineConfirmPassword(account.name, password);
if (verified) {
final String authToken = NetworkUtilities.authenticate(account.name, password);
if (!TextUtils.isEmpty(authToken)) {
final Bundle result = new Bundle();
result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
result.putString(AccountManager.KEY_ACCOUNT_TYPE, Constants.ACCOUNT_TYPE);
result.putString(AccountManager.KEY_AUTHTOKEN, password);
result.putString(AccountManager.KEY_AUTHTOKEN, authToken);
return result;
}
}
// the password was missing or incorrect, return an Intent to an
// Activity that will prompt the user for the password.
// If we get here, then we couldn't access the user's password - so we
// need to re-prompt them for their credentials. We do that by creating
// an intent to display our AuthenticatorActivity panel.
final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
intent.putExtra(AuthenticatorActivity.PARAM_USERNAME, account.name);
intent.putExtra(AuthenticatorActivity.PARAM_AUTHTOKEN_TYPE, authTokenType);
@@ -113,39 +126,27 @@ class Authenticator extends AbstractAccountAuthenticator {
@Override
public String getAuthTokenLabel(String authTokenType) {
if (Constants.AUTHTOKEN_TYPE.equals(authTokenType)) {
return mContext.getString(R.string.label);
}
// null means we don't support multiple authToken types
Log.v(TAG, "getAuthTokenLabel()");
return null;
}
@Override
public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account,
String[] features) {
public Bundle hasFeatures(
AccountAuthenticatorResponse response, Account account, String[] features) {
// This call is used to query whether the Authenticator supports
// specific features. We don't expect to get called, so we always
// return false (no) for any queries.
Log.v(TAG, "hasFeatures()");
final Bundle result = new Bundle();
result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
return result;
}
/**
* Validates user's password on the server
*/
private boolean onlineConfirmPassword(String username, String password) {
return NetworkUtilities
.authenticate(username, password, null/* Handler */, null/* Context */);
}
@Override
public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account,
String authTokenType, Bundle loginOptions) {
final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
intent.putExtra(AuthenticatorActivity.PARAM_USERNAME, account.name);
intent.putExtra(AuthenticatorActivity.PARAM_AUTHTOKEN_TYPE, authTokenType);
intent.putExtra(AuthenticatorActivity.PARAM_CONFIRM_CREDENTIALS, false);
final Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
String authTokenType, Bundle loginOptions) {
Log.v(TAG, "updateCredentials()");
return null;
}
}

View File

@@ -13,8 +13,13 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.samplesync.authenticator;
import com.example.android.samplesync.Constants;
import com.example.android.samplesync.R;
import com.example.android.samplesync.client.NetworkUtilities;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorActivity;
import android.accounts.AccountManager;
@@ -23,6 +28,7 @@ import android.app.ProgressDialog;
import android.content.ContentResolver;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.provider.ContactsContract;
@@ -33,41 +39,36 @@ import android.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 {
/** The Intent flag to confirm credentials. **/
/** The Intent flag to confirm credentials. */
public static final String PARAM_CONFIRM_CREDENTIALS = "confirmCredentials";
/** The Intent extra to store password. **/
/** The Intent extra to store password. */
public static final String PARAM_PASSWORD = "password";
/** The Intent extra to store username. **/
/** The Intent extra to store username. */
public static final String PARAM_USERNAME = "username";
/** The Intent extra to store authtoken type. **/
/** The Intent extra to store username. */
public static final String PARAM_AUTHTOKEN_TYPE = "authtokenType";
/** The tag used to log to adb console. **/
/** The tag used to log to adb console. */
private static final String TAG = "AuthenticatorActivity";
private AccountManager mAccountManager;
private Thread mAuthThread;
/** Keep track of the login task so can cancel it if requested */
private UserLoginTask mAuthTask = null;
private String mAuthtoken;
private String mAuthtokenType;
/** Keep track of the progress dialog so we can dismiss it */
private ProgressDialog mProgressDialog = null;
/**
* If set we are just checking that the user knows their credentials; this
* doesn't cause the user's password to be changed on the device.
* doesn't cause the user's password or authToken to be changed on the
* device.
*/
private Boolean mConfirmCredentials = false;
@@ -99,14 +100,13 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity {
Log.i(TAG, "loading data from Intent");
final Intent intent = getIntent();
mUsername = intent.getStringExtra(PARAM_USERNAME);
mAuthtokenType = intent.getStringExtra(PARAM_AUTHTOKEN_TYPE);
mRequestNewAccount = mUsername == null;
mConfirmCredentials = intent.getBooleanExtra(PARAM_CONFIRM_CREDENTIALS, false);
Log.i(TAG, " request new: " + mRequestNewAccount);
requestWindowFeature(Window.FEATURE_LEFT_ICON);
setContentView(R.layout.login_activity);
getWindow().setFeatureDrawableResource(Window.FEATURE_LEFT_ICON,
android.R.drawable.ic_dialog_alert);
getWindow().setFeatureDrawableResource(
Window.FEATURE_LEFT_ICON, android.R.drawable.ic_dialog_alert);
mMessage = (TextView) findViewById(R.id.message);
mUsernameEdit = (EditText) findViewById(R.id.username_edit);
mPasswordEdit = (EditText) findViewById(R.id.password_edit);
@@ -118,26 +118,30 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity {
* {@inheritDoc}
*/
@Override
protected Dialog onCreateDialog(int id) {
protected Dialog onCreateDialog(int id, Bundle args) {
final ProgressDialog dialog = new ProgressDialog(this);
dialog.setMessage(getText(R.string.ui_activity_authenticating));
dialog.setIndeterminate(true);
dialog.setCancelable(true);
dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
public void onCancel(DialogInterface dialog) {
Log.i(TAG, "dialog cancel has been invoked");
if (mAuthThread != null) {
mAuthThread.interrupt();
finish();
Log.i(TAG, "user cancelling authentication");
if (mAuthTask != null) {
mAuthTask.cancel(true);
}
}
});
// We save off the progress dialog in a field so that we can dismiss
// it later. We can't just call dismissDialog(0) because the system
// can lose track of our dialog if there's an orientation change.
mProgressDialog = dialog;
return dialog;
}
/**
* Handles onClick event on the Submit button. Sends username/password to
* the server for authentication.
* the server for authentication. The button is configured to call
* handleLogin() in the layout XML.
*
* @param view The Submit button for which this method is invoked
*/
@@ -149,11 +153,11 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity {
if (TextUtils.isEmpty(mUsername) || TextUtils.isEmpty(mPassword)) {
mMessage.setText(getMessage());
} else {
// Show a progress dialog, and kick off a background task to perform
// the user login attempt.
showProgress();
// Start authenticating...
mAuthThread =
NetworkUtilities.attemptAuth(mUsername, mPassword, mHandler,
AuthenticatorActivity.this);
mAuthTask = new UserLoginTask();
mAuthTask.execute();
}
}
@@ -162,7 +166,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity {
* request. See onAuthenticationResult(). Sets the
* AccountAuthenticatorResult which is sent back to the caller.
*
* @param the confirmCredentials result.
* @param result the confirmCredentials result.
*/
private void finishConfirmCredentials(boolean result) {
Log.i(TAG, "finishConfirmCredentials()");
@@ -178,12 +182,13 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity {
/**
* 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.
* AccountAuthenticatorResult which is sent back to the caller. We store the
* authToken that's returned from the server as the 'password' for this
* account - so we're never storing the user's actual password locally.
*
* @param the confirmCredentials result.
* @param result the confirmCredentials result.
*/
private void finishLogin() {
private void finishLogin(String authToken) {
Log.i(TAG, "finishLogin()");
final Account account = new Account(mUsername, Constants.ACCOUNT_TYPE);
@@ -195,37 +200,35 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity {
mAccountManager.setPassword(account, mPassword);
}
final Intent intent = new Intent();
mAuthtoken = mPassword;
intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, mUsername);
intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, Constants.ACCOUNT_TYPE);
if (mAuthtokenType != null && mAuthtokenType.equals(Constants.AUTHTOKEN_TYPE)) {
intent.putExtra(AccountManager.KEY_AUTHTOKEN, mAuthtoken);
}
setAccountAuthenticatorResult(intent.getExtras());
setResult(RESULT_OK, intent);
finish();
}
/**
* Hides the progress UI for a lengthy operation.
*/
private void hideProgress() {
dismissDialog(0);
}
/**
* Called when the authentication process completes (see attemptLogin()).
*
* @param authToken the authentication token returned by the server, or NULL if
* authentication failed.
*/
public void onAuthenticationResult(boolean result) {
public void onAuthenticationResult(String authToken) {
boolean success = ((authToken != null) && (authToken.length() > 0));
Log.i(TAG, "onAuthenticationResult(" + success + ")");
// Our task is complete, so clear it out
mAuthTask = null;
Log.i(TAG, "onAuthenticationResult(" + result + ")");
// Hide the progress dialog
hideProgress();
if (result) {
if (success) {
if (!mConfirmCredentials) {
finishLogin();
finishLogin(authToken);
} else {
finishConfirmCredentials(true);
finishConfirmCredentials(success);
}
} else {
Log.e(TAG, "onAuthenticationResult: failed to authenticate");
@@ -241,6 +244,16 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity {
}
}
public void onAuthenticationCancel() {
Log.i(TAG, "onAuthenticationCancel()");
// Our task is complete, so clear it out
mAuthTask = null;
// Hide the progress dialog
hideProgress();
}
/**
* Returns the message to be displayed at the top of the login dialog box.
*/
@@ -265,4 +278,49 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity {
private void showProgress() {
showDialog(0);
}
/**
* Hides the progress UI for a lengthy operation.
*/
private void hideProgress() {
if (mProgressDialog != null) {
mProgressDialog.dismiss();
mProgressDialog = null;
}
}
/**
* Represents an asynchronous task used to authenticate a user against the
* SampleSync Service
*/
public class UserLoginTask extends AsyncTask<Void, Void, String> {
@Override
protected String doInBackground(Void... params) {
// We do the actual work of authenticating the user
// in the NetworkUtilities class.
try {
return NetworkUtilities.authenticate(mUsername, mPassword);
} catch (Exception ex) {
Log.e(TAG, "UserLoginTask.doInBackground: failed to authenticate");
Log.i(TAG, ex.toString());
return null;
}
}
@Override
protected void onPostExecute(final String authToken) {
// On a successful authentication, call back into the Activity to
// communicate the authToken (or null for an error).
onAuthenticationResult(authToken);
}
@Override
protected void onCancelled() {
// If the action was canceled (by the user clicking the cancel
// button in the progress dialog), then call back into the
// activity to let it know.
onAuthenticationCancel();
}
}
}

View File

@@ -13,11 +13,15 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.samplesync.client;
import android.accounts.Account;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;
import com.example.android.samplesync.authenticator.AuthenticatorActivity;
@@ -37,11 +41,19 @@ import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.util.EntityUtils;
import org.json.JSONObject;
import org.json.JSONArray;
import org.json.JSONException;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
@@ -52,31 +64,26 @@ import java.util.TimeZone;
* Provides utility methods for communicating with the server.
*/
final public class NetworkUtilities {
/** The tag used to log to adb console. **/
/** The tag used to log to adb console. */
private static final String TAG = "NetworkUtilities";
/** The Intent extra to store password. **/
public static final String PARAM_PASSWORD = "password";
/** The Intent extra to store username. **/
/** POST parameter name for the user's account name */
public static final String PARAM_USERNAME = "username";
public static final String PARAM_UPDATED = "timestamp";
public static final String USER_AGENT = "AuthenticationService/1.0";
public static final int REGISTRATION_TIMEOUT_MS = 30 * 1000; // ms
public static final String BASE_URL = "https://samplesyncadapter.appspot.com";
/** POST parameter name for the user's password */
public static final String PARAM_PASSWORD = "password";
/** POST parameter name for the user's authentication token */
public static final String PARAM_AUTH_TOKEN = "authtoken";
/** POST parameter name for the client's last-known sync state */
public static final String PARAM_SYNC_STATE = "syncstate";
/** POST parameter name for the sending client-edited contact info */
public static final String PARAM_CONTACTS_DATA = "contacts";
/** Timeout (in ms) we specify for each http request */
public static final int HTTP_REQUEST_TIMEOUT_MS = 30 * 1000;
/** Base URL for the v2 Sample Sync Service */
public static final String BASE_URL = "https://samplesyncadapter2.appspot.com";
/** URI for authentication service */
public static final String AUTH_URI = BASE_URL + "/auth";
public static final String FETCH_FRIEND_UPDATES_URI = BASE_URL + "/fetch_friend_updates";
public static final String FETCH_STATUS_URI = BASE_URL + "/fetch_status";
private static HttpClient mHttpClient;
/** URI for sync service */
public static final String SYNC_CONTACTS_URI = BASE_URL + "/sync";
private NetworkUtilities() {
}
@@ -84,224 +91,188 @@ final public class NetworkUtilities {
/**
* Configures the httpClient to connect to the URL provided.
*/
public static void maybeCreateHttpClient() {
if (mHttpClient == null) {
mHttpClient = new DefaultHttpClient();
final HttpParams params = mHttpClient.getParams();
HttpConnectionParams.setConnectionTimeout(params, REGISTRATION_TIMEOUT_MS);
HttpConnectionParams.setSoTimeout(params, REGISTRATION_TIMEOUT_MS);
ConnManagerParams.setTimeout(params, REGISTRATION_TIMEOUT_MS);
}
public static HttpClient getHttpClient() {
HttpClient httpClient = new DefaultHttpClient();
final HttpParams params = httpClient.getParams();
HttpConnectionParams.setConnectionTimeout(params, HTTP_REQUEST_TIMEOUT_MS);
HttpConnectionParams.setSoTimeout(params, HTTP_REQUEST_TIMEOUT_MS);
ConnManagerParams.setTimeout(params, HTTP_REQUEST_TIMEOUT_MS);
return httpClient;
}
/**
* Executes the network requests on a separate thread.
* Connects to the SampleSync test server, authenticates the provided
* username and password.
*
* @param runnable The runnable instance containing network mOperations to
* be executed.
* @param username The server account username
* @param password The server account password
* @return String The authentication token returned by the server (or null)
*/
public static Thread performOnBackgroundThread(final Runnable runnable) {
final Thread t = new Thread() {
@Override
public void run() {
try {
runnable.run();
} finally {
}
}
};
t.start();
return t;
}
/**
* Connects to the Voiper server, authenticates the provided username and
* password.
*
* @param username The user's username
* @param password The user's password
* @param handler The hander instance from the calling UI thread.
* @param context The context of the calling Activity.
* @return boolean The boolean result indicating whether the user was
* successfully authenticated.
*/
public static boolean authenticate(String username, String password, Handler handler,
final Context context) {
public static String authenticate(String username, String password) {
final HttpResponse resp;
final ArrayList<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair(PARAM_USERNAME, username));
params.add(new BasicNameValuePair(PARAM_PASSWORD, password));
HttpEntity entity = null;
final HttpEntity entity;
try {
entity = new UrlEncodedFormEntity(params);
} catch (final UnsupportedEncodingException e) {
// this should never happen.
throw new AssertionError(e);
throw new IllegalStateException(e);
}
Log.i(TAG, "Authenticating to: " + AUTH_URI);
final HttpPost post = new HttpPost(AUTH_URI);
post.addHeader(entity.getContentType());
post.setEntity(entity);
maybeCreateHttpClient();
try {
resp = mHttpClient.execute(post);
resp = getHttpClient().execute(post);
String authToken = null;
if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Successful authentication");
InputStream istream = (resp.getEntity() != null) ? resp.getEntity().getContent()
: null;
if (istream != null) {
BufferedReader ireader = new BufferedReader(new InputStreamReader(istream));
authToken = ireader.readLine().trim();
}
sendResult(true, handler, context);
return true;
}
if ((authToken != null) && (authToken.length() > 0)) {
Log.v(TAG, "Successful authentication");
return authToken;
} else {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Error authenticating" + resp.getStatusLine());
}
sendResult(false, handler, context);
return false;
Log.e(TAG, "Error authenticating" + resp.getStatusLine());
return null;
}
} catch (final IOException e) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "IOException when getting authtoken", e);
}
sendResult(false, handler, context);
return false;
Log.e(TAG, "IOException when getting authtoken", e);
return null;
} finally {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "getAuthtoken completing");
}
Log.v(TAG, "getAuthtoken completing");
}
}
/**
* Sends the authentication response from server back to the caller main UI
* thread through its handler.
* Perform 2-way sync with the server-side contacts. We send a request that
* includes all the locally-dirty contacts so that the server can process
* those changes, and we receive (and return) a list of contacts that were
* updated on the server-side that need to be updated locally.
*
* @param result The boolean holding authentication result
* @param handler The main UI thread's handler instance.
* @param context The caller Activity's context.
* @param account The account being synced
* @param authtoken The authtoken stored in the AccountManager for this
* account
* @param serverSyncState A token returned from the server on the last sync
* @param dirtyContacts A list of the contacts to send to the server
* @return A list of contacts that we need to update locally
*/
private static void sendResult(final Boolean result, final Handler handler,
final Context context) {
if (handler == null || context == null) {
return;
public static List<RawContact> syncContacts(
Account account, String authtoken, long serverSyncState, List<RawContact> dirtyContacts)
throws JSONException, ParseException, IOException, AuthenticationException {
// Convert our list of User objects into a list of JSONObject
List<JSONObject> jsonContacts = new ArrayList<JSONObject>();
for (RawContact rawContact : dirtyContacts) {
jsonContacts.add(rawContact.toJSONObject());
}
handler.post(new Runnable() {
public void run() {
((AuthenticatorActivity) context).onAuthenticationResult(result);
}
});
}
/**
* Attempts to authenticate the user credentials on the server.
*
* @param username The user's username
* @param password The user's password to be authenticated
* @param handler The main UI thread's handler instance.
* @param context The caller Activity's context
* @return Thread The thread on which the network mOperations are executed.
*/
public static Thread attemptAuth(final String username, final String password,
final Handler handler, final Context context) {
// Create a special JSONArray of our JSON contacts
JSONArray buffer = new JSONArray(jsonContacts);
final Runnable runnable = new Runnable() {
public void run() {
authenticate(username, password, handler, context);
}
};
// run on background thread.
return NetworkUtilities.performOnBackgroundThread(runnable);
}
// Create an array that will hold the server-side contacts
// that have been changed (returned by the server).
final ArrayList<RawContact> serverDirtyList = new ArrayList<RawContact>();
/**
* Fetches the list of friend data updates from the server
*
* @param account The account being synced.
* @param authtoken The authtoken stored in AccountManager for this account
* @param lastUpdated The last time that sync was performed
* @return list The list of updates received from the server.
*/
public static List<User> fetchFriendUpdates(Account account, String authtoken, Date lastUpdated)
throws JSONException, ParseException, IOException, AuthenticationException {
final ArrayList<User> friendList = new ArrayList<User>();
// Prepare our POST data
final ArrayList<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair(PARAM_USERNAME, account.name));
params.add(new BasicNameValuePair(PARAM_PASSWORD, authtoken));
if (lastUpdated != null) {
final SimpleDateFormat formatter = new SimpleDateFormat("yyyy/MM/dd HH:mm");
formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
params.add(new BasicNameValuePair(PARAM_UPDATED, formatter.format(lastUpdated)));
params.add(new BasicNameValuePair(PARAM_AUTH_TOKEN, authtoken));
params.add(new BasicNameValuePair(PARAM_CONTACTS_DATA, buffer.toString()));
if (serverSyncState > 0) {
params.add(new BasicNameValuePair(PARAM_SYNC_STATE, Long.toString(serverSyncState)));
}
Log.i(TAG, params.toString());
HttpEntity entity = null;
entity = new UrlEncodedFormEntity(params);
final HttpPost post = new HttpPost(FETCH_FRIEND_UPDATES_URI);
HttpEntity entity = new UrlEncodedFormEntity(params);
// Send the updated friends data to the server
Log.i(TAG, "Syncing to: " + SYNC_CONTACTS_URI);
final HttpPost post = new HttpPost(SYNC_CONTACTS_URI);
post.addHeader(entity.getContentType());
post.setEntity(entity);
maybeCreateHttpClient();
final HttpResponse resp = mHttpClient.execute(post);
final HttpResponse resp = getHttpClient().execute(post);
final String response = EntityUtils.toString(resp.getEntity());
if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
// Succesfully connected to the samplesyncadapter server and
// authenticated.
// Extract friends data in json format.
final JSONArray friends = new JSONArray(response);
// Our request to the server was successful - so we assume
// that they accepted all the changes we sent up, and
// that the response includes the contacts that we need
// to update on our side...
final JSONArray serverContacts = new JSONArray(response);
Log.d(TAG, response);
for (int i = 0; i < friends.length(); i++) {
friendList.add(User.valueOf(friends.getJSONObject(i)));
for (int i = 0; i < serverContacts.length(); i++) {
RawContact rawContact = RawContact.valueOf(serverContacts.getJSONObject(i));
if (rawContact != null) {
serverDirtyList.add(rawContact);
}
}
} else {
if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
Log.e(TAG, "Authentication exception in fetching remote contacts");
Log.e(TAG, "Authentication exception in sending dirty contacts");
throw new AuthenticationException();
} else {
Log.e(TAG, "Server error in fetching remote contacts: " + resp.getStatusLine());
Log.e(TAG, "Server error in sending dirty contacts: " + resp.getStatusLine());
throw new IOException();
}
}
return friendList;
return serverDirtyList;
}
/**
* Fetches status messages for the user's friends from the server
* Download the avatar image 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.
* @param avatarUrl the URL pointing to the avatar image
* @return a byte array with the raw JPEG avatar image
*/
public static List<User.Status> fetchFriendStatuses(Account account, String authtoken)
throws JSONException, ParseException, IOException, AuthenticationException {
final ArrayList<User.Status> statusList = new ArrayList<User.Status>();
final ArrayList<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair(PARAM_USERNAME, account.name));
params.add(new BasicNameValuePair(PARAM_PASSWORD, authtoken));
HttpEntity entity = null;
entity = new UrlEncodedFormEntity(params);
final HttpPost post = new HttpPost(FETCH_STATUS_URI);
post.addHeader(entity.getContentType());
post.setEntity(entity);
maybeCreateHttpClient();
final HttpResponse resp = mHttpClient.execute(post);
final String response = EntityUtils.toString(resp.getEntity());
if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
// Succesfully connected to the samplesyncadapter server and
// authenticated.
// Extract friends data in json format.
final JSONArray statuses = new JSONArray(response);
for (int i = 0; i < statuses.length(); i++) {
statusList.add(User.Status.valueOf(statuses.getJSONObject(i)));
}
} else {
if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
Log.e(TAG, "Authentication exception in fetching friend status list");
throw new AuthenticationException();
} else {
Log.e(TAG, "Server error in fetching friend status list");
throw new IOException();
}
public static byte[] downloadAvatar(final String avatarUrl) {
// If there is no avatar, we're done
if (TextUtils.isEmpty(avatarUrl)) {
return null;
}
return statusList;
try {
Log.i(TAG, "Downloading avatar: " + avatarUrl);
// Request the avatar image from the server, and create a bitmap
// object from the stream we get back.
URL url = new URL(avatarUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.connect();
try {
final BitmapFactory.Options options = new BitmapFactory.Options();
final Bitmap avatar = BitmapFactory.decodeStream(connection.getInputStream(),
null, options);
// Take the image we received from the server, whatever format it
// happens to be in, and convert it to a JPEG image. Note: we're
// not resizing the avatar - we assume that the image we get from
// the server is a reasonable size...
Log.i(TAG, "Converting avatar to JPEG");
ByteArrayOutputStream convertStream = new ByteArrayOutputStream(
avatar.getWidth() * avatar.getHeight() * 4);
avatar.compress(Bitmap.CompressFormat.JPEG, 95, convertStream);
convertStream.flush();
convertStream.close();
// On pre-Honeycomb systems, it's important to call recycle on bitmaps
avatar.recycle();
return convertStream.toByteArray();
} finally {
connection.disconnect();
}
} catch (MalformedURLException muex) {
// A bad URL - nothing we can really do about it here...
Log.e(TAG, "Malformed avatar URL: " + avatarUrl);
} catch (IOException ioex) {
// If we're unable to download the avatar, it's a bummer but not the
// end of the world. We'll try to get it next time we sync.
Log.e(TAG, "Failed to download user avatar: " + avatarUrl);
}
return null;
}
}

View File

@@ -0,0 +1,260 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.samplesync.client;
import android.text.TextUtils;
import android.util.Log;
import org.json.JSONObject;
import org.json.JSONException;
import java.lang.StringBuilder;
/**
* Represents a low-level contacts RawContact - or at least
* the fields of the RawContact that we care about.
*/
final public class RawContact {
/** The tag used to log to adb console. **/
private static final String TAG = "RawContact";
private final String mUserName;
private final String mFullName;
private final String mFirstName;
private final String mLastName;
private final String mCellPhone;
private final String mOfficePhone;
private final String mHomePhone;
private final String mEmail;
private final String mStatus;
private final String mAvatarUrl;
private final boolean mDeleted;
private final boolean mDirty;
private final long mServerContactId;
private final long mRawContactId;
private final long mSyncState;
public long getServerContactId() {
return mServerContactId;
}
public long getRawContactId() {
return mRawContactId;
}
public String getUserName() {
return mUserName;
}
public String getFirstName() {
return mFirstName;
}
public String getLastName() {
return mLastName;
}
public String getFullName() {
return mFullName;
}
public String getCellPhone() {
return mCellPhone;
}
public String getOfficePhone() {
return mOfficePhone;
}
public String getHomePhone() {
return mHomePhone;
}
public String getEmail() {
return mEmail;
}
public String getStatus() {
return mStatus;
}
public String getAvatarUrl() {
return mAvatarUrl;
}
public boolean isDeleted() {
return mDeleted;
}
public boolean isDirty() {
return mDirty;
}
public long getSyncState() {
return mSyncState;
}
public String getBestName() {
if (!TextUtils.isEmpty(mFullName)) {
return mFullName;
} else if (TextUtils.isEmpty(mFirstName)) {
return mLastName;
} else {
return mFirstName;
}
}
/**
* Convert the RawContact object into a JSON string. From the
* JSONString interface.
* @return a JSON string representation of the object
*/
public JSONObject toJSONObject() {
JSONObject json = new JSONObject();
try {
if (!TextUtils.isEmpty(mFirstName)) {
json.put("f", mFirstName);
}
if (!TextUtils.isEmpty(mLastName)) {
json.put("l", mLastName);
}
if (!TextUtils.isEmpty(mCellPhone)) {
json.put("m", mCellPhone);
}
if (!TextUtils.isEmpty(mOfficePhone)) {
json.put("o", mOfficePhone);
}
if (!TextUtils.isEmpty(mHomePhone)) {
json.put("h", mHomePhone);
}
if (!TextUtils.isEmpty(mEmail)) {
json.put("e", mEmail);
}
if (mServerContactId > 0) {
json.put("i", mServerContactId);
}
if (mRawContactId > 0) {
json.put("c", mRawContactId);
}
if (mDeleted) {
json.put("d", mDeleted);
}
} catch (final Exception ex) {
Log.i(TAG, "Error converting RawContact to JSONObject" + ex.toString());
}
return json;
}
public RawContact(String name, String fullName, String firstName, String lastName,
String cellPhone, String officePhone, String homePhone, String email,
String status, String avatarUrl, boolean deleted, long serverContactId,
long rawContactId, long syncState, boolean dirty) {
mUserName = name;
mFullName = fullName;
mFirstName = firstName;
mLastName = lastName;
mCellPhone = cellPhone;
mOfficePhone = officePhone;
mHomePhone = homePhone;
mEmail = email;
mStatus = status;
mAvatarUrl = avatarUrl;
mDeleted = deleted;
mServerContactId = serverContactId;
mRawContactId = rawContactId;
mSyncState = syncState;
mDirty = dirty;
}
/**
* Creates and returns an instance of the RawContact from the provided JSON data.
*
* @param user The JSONObject containing user data
* @return user The new instance of Sample RawContact created from the JSON data.
*/
public static RawContact valueOf(JSONObject contact) {
try {
final String userName = !contact.isNull("u") ? contact.getString("u") : null;
final int serverContactId = !contact.isNull("i") ? contact.getInt("i") : -1;
// If we didn't get either a username or serverId for the contact, then
// we can't do anything with it locally...
if ((userName == null) && (serverContactId <= 0)) {
throw new JSONException("JSON contact missing required 'u' or 'i' fields");
}
final int rawContactId = !contact.isNull("c") ? contact.getInt("c") : -1;
final String firstName = !contact.isNull("f") ? contact.getString("f") : null;
final String lastName = !contact.isNull("l") ? contact.getString("l") : null;
final String cellPhone = !contact.isNull("m") ? contact.getString("m") : null;
final String officePhone = !contact.isNull("o") ? contact.getString("o") : null;
final String homePhone = !contact.isNull("h") ? contact.getString("h") : null;
final String email = !contact.isNull("e") ? contact.getString("e") : null;
final String status = !contact.isNull("s") ? contact.getString("s") : null;
final String avatarUrl = !contact.isNull("a") ? contact.getString("a") : null;
final boolean deleted = !contact.isNull("d") ? contact.getBoolean("d") : false;
final long syncState = !contact.isNull("x") ? contact.getLong("x") : 0;
return new RawContact(userName, null, firstName, lastName, cellPhone,
officePhone, homePhone, email, status, avatarUrl, deleted,
serverContactId, rawContactId, syncState, false);
} catch (final Exception ex) {
Log.i(TAG, "Error parsing JSON contact object" + ex.toString());
}
return null;
}
/**
* Creates and returns RawContact instance from all the supplied parameters.
*/
public static RawContact create(String fullName, String firstName, String lastName,
String cellPhone, String officePhone, String homePhone,
String email, String status, boolean deleted, long rawContactId,
long serverContactId) {
return new RawContact(null, fullName, firstName, lastName, cellPhone, officePhone,
homePhone, email, status, null, deleted, serverContactId, rawContactId,
-1, true);
}
/**
* Creates and returns a User instance that represents a deleted user.
* Since the user is deleted, all we need are the client/server IDs.
* @param clientUserId The client-side ID for the contact
* @param serverUserId The server-side ID for the contact
* @return a minimal User object representing the deleted contact.
*/
public static RawContact createDeletedContact(long rawContactId, long serverContactId)
{
return new RawContact(null, null, null, null, null, null, null,
null, null, null, true, serverContactId, rawContactId, -1, true);
}
}

View File

@@ -1,155 +0,0 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.samplesync.client;
import android.util.Log;
import org.json.JSONObject;
/**
* Represents a sample SyncAdapter user
*/
final public class User {
private final String mUserName;
private final String mFirstName;
private final String mLastName;
private final String mCellPhone;
private final String mOfficePhone;
private final String mHomePhone;
private final String mEmail;
private final boolean mDeleted;
private final int mUserId;
public int getUserId() {
return mUserId;
}
public String getUserName() {
return mUserName;
}
public String getFirstName() {
return mFirstName;
}
public String getLastName() {
return mLastName;
}
public String getCellPhone() {
return mCellPhone;
}
public String getOfficePhone() {
return mOfficePhone;
}
public String getHomePhone() {
return mHomePhone;
}
public String getEmail() {
return mEmail;
}
public boolean isDeleted() {
return mDeleted;
}
private User(String name, String firstName, String lastName, String cellPhone,
String officePhone, String homePhone, String email, Boolean deleted, Integer userId) {
mUserName = name;
mFirstName = firstName;
mLastName = lastName;
mCellPhone = cellPhone;
mOfficePhone = officePhone;
mHomePhone = homePhone;
mEmail = email;
mDeleted = deleted;
mUserId = userId;
}
/**
* Creates and returns an instance of the user from the provided JSON data.
*
* @param user The JSONObject containing user data
* @return user The new instance of Voiper user created from the JSON data.
*/
public static User valueOf(JSONObject user) {
try {
final String userName = user.getString("u");
final String firstName = user.has("f") ? user.getString("f") : null;
final String lastName = user.has("l") ? user.getString("l") : null;
final String cellPhone = user.has("m") ? user.getString("m") : null;
final String officePhone = user.has("o") ? user.getString("o") : null;
final String homePhone = user.has("h") ? user.getString("h") : null;
final String email = user.has("e") ? user.getString("e") : null;
final boolean deleted = user.has("d") ? user.getBoolean("d") : false;
final int userId = user.getInt("i");
return new User(userName, firstName, lastName, cellPhone, officePhone, homePhone,
email, deleted, userId);
} catch (final Exception ex) {
Log.i("User", "Error parsing JSON user object" + ex.toString());
}
return null;
}
/**
* Represents the User's status messages
*
*/
final public static class Status {
private final Integer mUserId;
private final String mStatus;
public int getUserId() {
return mUserId;
}
public String getStatus() {
return mStatus;
}
public Status(Integer userId, String status) {
mUserId = userId;
mStatus = status;
}
public static User.Status valueOf(JSONObject userStatus) {
try {
final int userId = userStatus.getInt("i");
final String status = userStatus.getString("s");
return new User.Status(userId, status);
} catch (final Exception ex) {
Log.i("User.Status", "Error parsing JSON user object");
}
return null;
}
}
}

View File

@@ -0,0 +1,369 @@
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.example.android.samplesync.editor;
import com.example.android.samplesync.R;
import com.example.android.samplesync.client.RawContact;
import com.example.android.samplesync.platform.BatchOperation;
import com.example.android.samplesync.platform.ContactManager;
import com.example.android.samplesync.platform.ContactManager.EditorQuery;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.RawContacts;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.EditText;
import android.widget.TextView;
/**
* Implements a sample editor for a contact that belongs to a remote contact service.
* The editor can be invoked for an existing SampleSyncAdapter contact, or it can
* be used to create a brand new SampleSyncAdapter contact. We look at the Intent
* object to figure out whether this is a "new" or "edit" operation.
*/
public class ContactEditorActivity extends Activity {
private static final String TAG = "SampleSyncAdapter";
// Keep track of whether we're inserting a new contact or editing an
// existing contact.
private boolean mIsInsert;
// The name of the external account we're syncing this contact to.
private String mAccountName;
// For existing contacts, this is the URI to the contact data.
private Uri mRawContactUri;
// The raw clientId for this contact
private long mRawContactId;
// Make sure we only attempt to save the contact once if the
// user presses the "done" button multiple times...
private boolean mSaveInProgress = false;
// Keep track of the controls used to edit contact values, so we can get/set
// those values easily.
private EditText mNameEditText;
private EditText mHomePhoneEditText;
private EditText mMobilePhoneEditText;
private EditText mWorkPhoneEditText;
private EditText mEmailEditText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.editor);
mNameEditText = (EditText)findViewById(R.id.editor_name);
mHomePhoneEditText = (EditText)findViewById(R.id.editor_phone_home);
mMobilePhoneEditText = (EditText)findViewById(R.id.editor_phone_mobile);
mWorkPhoneEditText = (EditText)findViewById(R.id.editor_phone_work);
mEmailEditText = (EditText)findViewById(R.id.editor_email);
// Figure out whether we're creating a new contact (ACTION_INSERT) or editing
// an existing contact.
Intent intent = getIntent();
String action = intent.getAction();
if (Intent.ACTION_INSERT.equals(action)) {
// We're inserting a new contact, so save off the external account name
// which should have been added to the intent we were passed.
mIsInsert = true;
String accountName = intent.getStringExtra(RawContacts.ACCOUNT_NAME);
if (accountName == null) {
Log.e(TAG, "Account name is required");
finish();
}
setAccountName(accountName);
} else {
// We're editing an existing contact. Load in the data from the contact
// so that the user can edit it.
mIsInsert = false;
mRawContactUri = intent.getData();
if (mRawContactUri == null) {
Log.e(TAG, "Raw contact URI is required");
finish();
}
startLoadRawContactEntity();
}
}
@Override
public void onBackPressed() {
// This method will have been called if the user presses the "Back" button
// in the ActionBar. We treat that the same way as the "Done" button in
// the ActionBar.
save();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// This method gets called so that we can place items in the main Options menu -
// for example, the ActionBar items. We add our menus from the res/menu/edit.xml
// file.
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.edit, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
case R.id.menu_done:
// The user pressed the "Home" button or our "Done" button - both
// in the ActionBar. In both cases, we want to save the contact
// and exit.
save();
return true;
case R.id.menu_cancel:
// The user pressed the Cancel menu item in the ActionBar.
// Close the editor without saving any changes.
finish();
return true;
}
return false;
}
/**
* Create an AsyncTask to load the contact from the Contacts data provider
*/
private void startLoadRawContactEntity() {
Uri uri = Uri.withAppendedPath(mRawContactUri, RawContacts.Entity.CONTENT_DIRECTORY);
new LoadRawContactTask().execute(uri);
}
/**
* Called by the LoadRawContactTask when the contact information has been
* successfully loaded from the Contacts data provider.
*/
public void onRawContactEntityLoaded(Cursor cursor) {
while (cursor.moveToNext()) {
String mimetype = cursor.getString(EditorQuery.COLUMN_MIMETYPE);
if (StructuredName.CONTENT_ITEM_TYPE.equals(mimetype)) {
setAccountName(cursor.getString(EditorQuery.COLUMN_ACCOUNT_NAME));
mRawContactId = cursor.getLong(EditorQuery.COLUMN_RAW_CONTACT_ID);
mNameEditText.setText(cursor.getString(EditorQuery.COLUMN_FULL_NAME));
} else if (Phone.CONTENT_ITEM_TYPE.equals(mimetype)) {
final int type = cursor.getInt(EditorQuery.COLUMN_PHONE_TYPE);
if (type == Phone.TYPE_HOME) {
mHomePhoneEditText.setText(cursor.getString(EditorQuery.COLUMN_PHONE_NUMBER));
} else if (type == Phone.TYPE_MOBILE) {
mMobilePhoneEditText.setText(cursor.getString(EditorQuery.COLUMN_PHONE_NUMBER));
} else if (type == Phone.TYPE_WORK) {
mWorkPhoneEditText.setText(cursor.getString(EditorQuery.COLUMN_PHONE_NUMBER));
}
} else if (Email.CONTENT_ITEM_TYPE.equals(mimetype)) {
mEmailEditText.setText(cursor.getString(EditorQuery.COLUMN_DATA1));
}
}
}
/**
* Save the updated contact data. We actually take two different actions
* depending on whether we are creating a new contact or editing an
* existing contact.
*/
public void save() {
// If we're already saving this contact, don't kick-off yet
// another save - the user probably just pressed the "Done"
// button multiple times...
if (mSaveInProgress) {
return;
}
mSaveInProgress = true;
if (mIsInsert) {
saveNewContact();
} else {
saveChanges();
}
}
/**
* Save off the external contacts provider account name. We show the account name
* in the header section of the edit panel, and we also need it later when we
* save off a brand new contact.
*/
private void setAccountName(String accountName) {
mAccountName = accountName;
if (accountName != null) {
TextView accountNameLabel = (TextView)findViewById(R.id.header_account_name);
if (accountNameLabel != null) {
accountNameLabel.setText(accountName);
}
}
}
/**
* Save a new contact using the Contacts content provider. The actual insertion
* is performed in an AsyncTask.
*/
@SuppressWarnings("unchecked")
private void saveNewContact() {
new InsertContactTask().execute(buildRawContact());
}
/**
* Save changes to an existing contact. The actual update is performed in
* an AsyncTask.
*/
@SuppressWarnings("unchecked")
private void saveChanges() {
new UpdateContactTask().execute(buildRawContact());
}
/**
* Build a RawContact object from the data in the user-editable form
* @return a new RawContact object representing the edited user
*/
private RawContact buildRawContact() {
return RawContact.create(mNameEditText.getText().toString(),
null,
null,
mMobilePhoneEditText.getText().toString(),
mWorkPhoneEditText.getText().toString(),
mHomePhoneEditText.getText().toString(),
mEmailEditText.getText().toString(),
null,
false,
mRawContactId,
-1);
}
/**
* Called after a contact is saved - both for edited contacts and new contacts.
* We set the final result of the activity to be "ok", and then close the activity
* by calling finish().
*/
public void onContactSaved(Uri result) {
if (result != null) {
Intent intent = new Intent();
intent.setData(result);
setResult(RESULT_OK, intent);
finish();
}
mSaveInProgress = false;
}
/**
* Represents an asynchronous task used to load a contact from
* the Contacts content provider.
*
*/
public class LoadRawContactTask extends AsyncTask<Uri, Void, Cursor> {
@Override
protected Cursor doInBackground(Uri... params) {
// Our background task is to load the contact from the Contacts provider
return getContentResolver().query(params[0], EditorQuery.PROJECTION, null, null, null);
}
@Override
protected void onPostExecute(Cursor cursor) {
// After we've successfully loaded the contact, call back into
// the ContactEditorActivity so we can update the UI
try {
if (cursor != null) {
onRawContactEntityLoaded(cursor);
}
} finally {
cursor.close();
}
}
}
/**
* Represents an asynchronous task used to save a new contact
* into the contacts database.
*/
public class InsertContactTask extends AsyncTask<RawContact, Void, Uri> {
@Override
protected Uri doInBackground(RawContact... params) {
try {
final RawContact rawContact = params[0];
final Context context = getApplicationContext();
final ContentResolver resolver = getContentResolver();
final BatchOperation batchOperation = new BatchOperation(context, resolver);
ContactManager.addContact(context, mAccountName, rawContact, false, batchOperation);
Uri rawContactUri = batchOperation.execute();
// Convert the raw contact URI to a contact URI
if (rawContactUri != null) {
return RawContacts.getContactLookupUri(resolver, rawContactUri);
} else {
Log.e(TAG, "Could not save new contact");
return null;
}
} catch (Exception e) {
Log.e(TAG, "An error occurred while saving new contact", e);
}
return null;
}
@Override
protected void onPostExecute(Uri result) {
// Tell the UI that the contact has been successfully saved
onContactSaved(result);
}
}
/**
* Represents an asynchronous task used to save an updated contact
* into the contacts database.
*/
public class UpdateContactTask extends AsyncTask<RawContact, Void, Uri> {
@Override
protected Uri doInBackground(RawContact... params) {
try {
final RawContact rawContact = params[0];
final Context context = getApplicationContext();
final ContentResolver resolver = getContentResolver();
final BatchOperation batchOperation = new BatchOperation(context, resolver);
ContactManager.updateContact(context, resolver, rawContact, false, false, false,
false, rawContact.getRawContactId(), batchOperation);
batchOperation.execute();
// Convert the raw contact URI to a contact URI
return RawContacts.getContactLookupUri(resolver, mRawContactUri);
} catch (Exception e) {
Log.e(TAG, "Could not save changes", e);
}
return null;
}
@Override
protected void onPostExecute(Uri result) {
// Tell the UI that the contact has been successfully saved
onContactSaved(result);
}
}
}

View File

@@ -16,9 +16,11 @@
package com.example.android.samplesync.platform;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentResolver;
import android.content.Context;
import android.content.OperationApplicationException;
import android.net.Uri;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.util.Log;
@@ -50,19 +52,24 @@ final public class BatchOperation {
mOperations.add(cpo);
}
public void execute() {
public Uri execute() {
Uri result = null;
if (mOperations.size() == 0) {
return;
return result;
}
// Apply the mOperations to the content provider
try {
mResolver.applyBatch(ContactsContract.AUTHORITY, mOperations);
ContentProviderResult[] results = mResolver.applyBatch(ContactsContract.AUTHORITY,
mOperations);
if ((results != null) && (results.length > 0))
result = results[0].uri;
} catch (final OperationApplicationException e1) {
Log.e(TAG, "storing contact data failed", e1);
} catch (final RemoteException e2) {
Log.e(TAG, "storing contact data failed", e2);
}
mOperations.clear();
return result;
}
}

View File

@@ -15,25 +15,30 @@
*/
package com.example.android.samplesync.platform;
import com.example.android.samplesync.Constants;
import com.example.android.samplesync.R;
import com.example.android.samplesync.client.RawContact;
import android.accounts.Account;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.StatusUpdates;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Im;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.Settings;
import android.provider.ContactsContract.StatusUpdates;
import android.util.Log;
import com.example.android.samplesync.Constants;
import com.example.android.samplesync.R;
import com.example.android.samplesync.client.User;
import java.util.ArrayList;
import java.util.List;
/**
@@ -49,81 +54,172 @@ public class ContactManager {
private static final String TAG = "ContactManager";
/**
* Synchronize raw contacts
* Take a list of updated contacts and apply those changes to the
* contacts database. Typically this list of contacts would have been
* returned from the server, and we want to apply those changes locally.
*
* @param context The context of Authenticator Activity
* @param account The username for the account
* @param users The list of users
* @param rawContacts The list of contacts to update
* @param lastSyncMarker The previous server sync-state
* @return the server syncState that should be used in our next
* sync request.
*/
public static synchronized void syncContacts(Context context, String account, List<User> users) {
public static synchronized long updateContacts(Context context, String account,
List<RawContact> rawContacts, long lastSyncMarker) {
long userId;
long rawContactId = 0;
long currentSyncMarker = lastSyncMarker;
final ContentResolver resolver = context.getContentResolver();
final BatchOperation batchOperation = new BatchOperation(context, resolver);
final List<RawContact> newUsers = new ArrayList<RawContact>();
Log.d(TAG, "In SyncContacts");
for (final User user : users) {
userId = user.getUserId();
// Check to see if the contact needs to be inserted or updated
rawContactId = lookupRawContact(resolver, userId);
for (final RawContact rawContact : rawContacts) {
// The server returns a syncState (x) value with each contact record.
// The syncState is sequential, so higher values represent more recent
// changes than lower values. We keep track of the highest value we
// see, and consider that a "high water mark" for the changes we've
// received from the server. That way, on our next sync, we can just
// ask for changes that have occurred since that most-recent change.
if (rawContact.getSyncState() > currentSyncMarker) {
currentSyncMarker = rawContact.getSyncState();
}
// If the server returned a clientId for this user, then it's likely
// that the user was added here, and was just pushed to the server
// for the first time. In that case, we need to update the main
// row for this contact so that the RawContacts.SOURCE_ID value
// contains the correct serverId.
final long rawContactId;
final boolean updateServerId;
if (rawContact.getRawContactId() > 0) {
rawContactId = rawContact.getRawContactId();
updateServerId = true;
} else {
long serverContactId = rawContact.getServerContactId();
rawContactId = lookupRawContact(resolver, serverContactId);
updateServerId = false;
}
if (rawContactId != 0) {
if (!user.isDeleted()) {
// update contact
updateContact(context, resolver, account, user, rawContactId, batchOperation);
if (!rawContact.isDeleted()) {
updateContact(context, resolver, rawContact, updateServerId,
true, true, true, rawContactId, batchOperation);
} else {
// delete contact
deleteContact(context, rawContactId, batchOperation);
}
} else {
// add new contact
Log.d(TAG, "In addContact");
if (!user.isDeleted()) {
addContact(context, account, user, batchOperation);
if (!rawContact.isDeleted()) {
newUsers.add(rawContact);
addContact(context, account, rawContact, true, batchOperation);
}
}
// A sync adapter should batch operations on multiple contacts,
// because it will make a dramatic performance difference.
// (UI updates, etc)
if (batchOperation.size() >= 50) {
batchOperation.execute();
}
}
batchOperation.execute();
return currentSyncMarker;
}
/**
* Add a list of status messages to the contacts provider.
* Return a list of the local contacts that have been marked as
* "dirty", and need syncing to the SampleSync server.
*
* @param context the context to use
* @param accountName the username of the logged in user
* @param statuses the list of statuses to store
* @param context The context of Authenticator Activity
* @param account The account that we're interested in syncing
* @return a list of Users that are considered "dirty"
*/
public static void insertStatuses(Context context, String username, List<User.Status> list) {
public static List<RawContact> getDirtyContacts(Context context, Account account) {
Log.i(TAG, "*** Looking for local dirty contacts");
List<RawContact> dirtyContacts = new ArrayList<RawContact>();
final ContentValues values = new ContentValues();
final ContentResolver resolver = context.getContentResolver();
final Cursor c = resolver.query(DirtyQuery.CONTENT_URI,
DirtyQuery.PROJECTION,
DirtyQuery.SELECTION,
new String[] {account.name},
null);
try {
while (c.moveToNext()) {
final long rawContactId = c.getLong(DirtyQuery.COLUMN_RAW_CONTACT_ID);
final long serverContactId = c.getLong(DirtyQuery.COLUMN_SERVER_ID);
final boolean isDirty = "1".equals(c.getString(DirtyQuery.COLUMN_DIRTY));
final boolean isDeleted = "1".equals(c.getString(DirtyQuery.COLUMN_DELETED));
// The system actually keeps track of a change version number for
// each contact. It may be something you're interested in for your
// client-server sync protocol. We're not using it in this example,
// other than to log it.
final long version = c.getLong(DirtyQuery.COLUMN_VERSION);
Log.i(TAG, "Dirty Contact: " + Long.toString(rawContactId));
Log.i(TAG, "Contact Version: " + Long.toString(version));
if (isDeleted) {
Log.i(TAG, "Contact is marked for deletion");
RawContact rawContact = RawContact.createDeletedContact(rawContactId,
serverContactId);
dirtyContacts.add(rawContact);
} else if (isDirty) {
RawContact rawContact = getRawContact(context, rawContactId);
Log.i(TAG, "Contact Name: " + rawContact.getBestName());
dirtyContacts.add(rawContact);
}
}
} finally {
if (c != null) {
c.close();
}
}
return dirtyContacts;
}
/**
* Update the status messages for a list of users. This is typically called
* for contacts we've just added to the system, since we can't monkey with
* the contact's status until they have a profileId.
*
* @param context The context of Authenticator Activity
* @param rawContacts The list of users we want to update
*/
public static void updateStatusMessages(Context context, List<RawContact> rawContacts) {
final ContentResolver resolver = context.getContentResolver();
final BatchOperation batchOperation = new BatchOperation(context, resolver);
for (final User.Status status : list) {
// Look up the user's sample SyncAdapter data row
final long userId = status.getUserId();
final long profileId = lookupProfile(resolver, userId);
// Insert the activity into the stream
if (profileId > 0) {
values.put(StatusUpdates.DATA_ID, profileId);
values.put(StatusUpdates.STATUS, status.getStatus());
values.put(StatusUpdates.PROTOCOL, Im.PROTOCOL_CUSTOM);
values.put(StatusUpdates.CUSTOM_PROTOCOL, CUSTOM_IM_PROTOCOL);
values.put(StatusUpdates.IM_ACCOUNT, username);
values.put(StatusUpdates.IM_HANDLE, status.getUserId());
values.put(StatusUpdates.STATUS_RES_PACKAGE, context.getPackageName());
values.put(StatusUpdates.STATUS_ICON, R.drawable.icon);
values.put(StatusUpdates.STATUS_LABEL, R.string.label);
batchOperation.add(ContactOperations.newInsertCpo(StatusUpdates.CONTENT_URI, true)
.withValues(values).build());
// A sync adapter should batch operations on multiple contacts,
// because it will make a dramatic performance difference.
if (batchOperation.size() >= 50) {
batchOperation.execute();
}
for (RawContact rawContact : rawContacts) {
updateContactStatus(context, rawContact, batchOperation);
}
batchOperation.execute();
}
/**
* After we've finished up a sync operation, we want to clean up the sync-state
* so that we're ready for the next time. This involves clearing out the 'dirty'
* flag on the synced contacts - but we also have to finish the DELETE operation
* on deleted contacts. When the user initially deletes them on the client, they're
* marked for deletion - but they're not actually deleted until we delete them
* again, and include the ContactsContract.CALLER_IS_SYNCADAPTER parameter to
* tell the contacts provider that we're really ready to let go of this contact.
*
* @param context The context of Authenticator Activity
* @param dirtyContacts The list of contacts that we're cleaning up
*/
public static void clearSyncFlags(Context context, List<RawContact> dirtyContacts) {
Log.i(TAG, "*** Clearing Sync-related Flags");
final ContentResolver resolver = context.getContentResolver();
final BatchOperation batchOperation = new BatchOperation(context, resolver);
for (RawContact rawContact : dirtyContacts) {
if (rawContact.isDeleted()) {
Log.i(TAG, "Deleting contact: " + Long.toString(rawContact.getRawContactId()));
deleteContact(context, rawContact.getRawContactId(), batchOperation);
} else if (rawContact.isDirty()) {
Log.i(TAG, "Clearing dirty flag for: " + rawContact.getBestName());
clearDirtyFlag(context, rawContact.getRawContactId(), batchOperation);
}
}
batchOperation.execute();
@@ -131,88 +227,311 @@ public class ContactManager {
/**
* Adds a single contact to the platform contacts provider.
* This can be used to respond to a new contact found as part
* of sync information returned from the server, or because a
* user added a new contact.
*
* @param context the Authenticator Activity context
* @param accountName the account the contact belongs to
* @param user the sample SyncAdapter User object
* @param rawContact the sample SyncAdapter User object
* @param inSync is the add part of a client-server sync?
* @param batchOperation allow us to batch together multiple operations
* into a single provider call
*/
private static void addContact(Context context, String accountName, User user,
BatchOperation batchOperation) {
public static void addContact(Context context, String accountName, RawContact rawContact,
boolean inSync, BatchOperation batchOperation) {
// Put the data in the contacts provider
final ContactOperations contactOp =
ContactOperations.createNewContact(context, user.getUserId(), accountName,
batchOperation);
contactOp.addName(user.getFirstName(), user.getLastName()).addEmail(user.getEmail())
.addPhone(user.getCellPhone(), Phone.TYPE_MOBILE).addPhone(user.getHomePhone(),
Phone.TYPE_OTHER).addProfileAction(user.getUserId());
final ContactOperations contactOp = ContactOperations.createNewContact(
context, rawContact.getServerContactId(), accountName, inSync, batchOperation);
contactOp.addName(rawContact.getFullName(), rawContact.getFirstName(),
rawContact.getLastName())
.addEmail(rawContact.getEmail())
.addPhone(rawContact.getCellPhone(), Phone.TYPE_MOBILE)
.addPhone(rawContact.getHomePhone(), Phone.TYPE_HOME)
.addPhone(rawContact.getOfficePhone(), Phone.TYPE_WORK)
.addAvatar(rawContact.getAvatarUrl());
// If we have a serverId, then go ahead and create our status profile.
// Otherwise skip it - and we'll create it after we sync-up to the
// server later on.
if (rawContact.getServerContactId() > 0) {
contactOp.addProfileAction(rawContact.getServerContactId());
}
}
/**
* Updates a single contact to the platform contacts provider.
* This method can be used to update a contact from a sync
* operation or as a result of a user editing a contact
* record.
*
* This operation is actually relatively complex. We query
* the database to find all the rows of info that already
* exist for this Contact. For rows that exist (and thus we're
* modifying existing fields), we create an update operation
* to change that field. But for fields we're adding, we create
* "add" operations to create new rows for those fields.
*
* @param context the Authenticator Activity context
* @param resolver the ContentResolver to use
* @param accountName the account the contact belongs to
* @param user the sample SyncAdapter contact object.
* @param rawContact the sample SyncAdapter contact object
* @param updateStatus should we update this user's status
* @param updateAvatar should we update this user's avatar image
* @param inSync is the update part of a client-server sync?
* @param rawContactId the unique Id for this rawContact in contacts
* provider
* @param batchOperation allow us to batch together multiple operations
* into a single provider call
*/
private static void updateContact(Context context, ContentResolver resolver,
String accountName, User user, long rawContactId, BatchOperation batchOperation) {
public static void updateContact(Context context, ContentResolver resolver,
RawContact rawContact, boolean updateServerId, boolean updateStatus, boolean updateAvatar,
boolean inSync, long rawContactId, BatchOperation batchOperation) {
boolean existingCellPhone = false;
boolean existingHomePhone = false;
boolean existingWorkPhone = false;
boolean existingEmail = false;
boolean existingAvatar = false;
Uri uri;
String cellPhone = null;
String otherPhone = null;
String email = null;
final Cursor c =
resolver.query(Data.CONTENT_URI, DataQuery.PROJECTION, DataQuery.SELECTION,
resolver.query(DataQuery.CONTENT_URI, DataQuery.PROJECTION, DataQuery.SELECTION,
new String[] {String.valueOf(rawContactId)}, null);
final ContactOperations contactOp =
ContactOperations.updateExistingContact(context, rawContactId, batchOperation);
ContactOperations.updateExistingContact(context, rawContactId,
inSync, batchOperation);
try {
// Iterate over the existing rows of data, and update each one
// with the information we received from the server.
while (c.moveToNext()) {
final long id = c.getLong(DataQuery.COLUMN_ID);
final String mimeType = c.getString(DataQuery.COLUMN_MIMETYPE);
uri = ContentUris.withAppendedId(Data.CONTENT_URI, id);
final Uri uri = ContentUris.withAppendedId(Data.CONTENT_URI, id);
if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) {
final String lastName = c.getString(DataQuery.COLUMN_FAMILY_NAME);
final String firstName = c.getString(DataQuery.COLUMN_GIVEN_NAME);
contactOp.updateName(uri, firstName, lastName, user.getFirstName(), user
.getLastName());
contactOp.updateName(uri,
c.getString(DataQuery.COLUMN_GIVEN_NAME),
c.getString(DataQuery.COLUMN_FAMILY_NAME),
c.getString(DataQuery.COLUMN_FULL_NAME),
rawContact.getFirstName(),
rawContact.getLastName(),
rawContact.getFullName());
} else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) {
final int type = c.getInt(DataQuery.COLUMN_PHONE_TYPE);
if (type == Phone.TYPE_MOBILE) {
cellPhone = c.getString(DataQuery.COLUMN_PHONE_NUMBER);
contactOp.updatePhone(cellPhone, user.getCellPhone(), uri);
} else if (type == Phone.TYPE_OTHER) {
otherPhone = c.getString(DataQuery.COLUMN_PHONE_NUMBER);
contactOp.updatePhone(otherPhone, user.getHomePhone(), uri);
existingCellPhone = true;
contactOp.updatePhone(c.getString(DataQuery.COLUMN_PHONE_NUMBER),
rawContact.getCellPhone(), uri);
} else if (type == Phone.TYPE_HOME) {
existingHomePhone = true;
contactOp.updatePhone(c.getString(DataQuery.COLUMN_PHONE_NUMBER),
rawContact.getHomePhone(), uri);
} else if (type == Phone.TYPE_WORK) {
existingWorkPhone = true;
contactOp.updatePhone(c.getString(DataQuery.COLUMN_PHONE_NUMBER),
rawContact.getOfficePhone(), uri);
}
} else if (Data.MIMETYPE.equals(Email.CONTENT_ITEM_TYPE)) {
email = c.getString(DataQuery.COLUMN_EMAIL_ADDRESS);
contactOp.updateEmail(user.getEmail(), email, uri);
} else if (mimeType.equals(Email.CONTENT_ITEM_TYPE)) {
existingEmail = true;
contactOp.updateEmail(rawContact.getEmail(),
c.getString(DataQuery.COLUMN_EMAIL_ADDRESS), uri);
} else if (mimeType.equals(Photo.CONTENT_ITEM_TYPE)) {
existingAvatar = true;
contactOp.updateAvatar(rawContact.getAvatarUrl(), uri);
}
} // while
} finally {
c.close();
}
// Add the cell phone, if present and not updated above
if (cellPhone == null) {
contactOp.addPhone(user.getCellPhone(), Phone.TYPE_MOBILE);
if (!existingCellPhone) {
contactOp.addPhone(rawContact.getCellPhone(), Phone.TYPE_MOBILE);
}
// Add the other phone, if present and not updated above
if (otherPhone == null) {
contactOp.addPhone(user.getHomePhone(), Phone.TYPE_OTHER);
// Add the home phone, if present and not updated above
if (!existingHomePhone) {
contactOp.addPhone(rawContact.getHomePhone(), Phone.TYPE_HOME);
}
// Add the work phone, if present and not updated above
if (!existingWorkPhone) {
contactOp.addPhone(rawContact.getOfficePhone(), Phone.TYPE_WORK);
}
// Add the email address, if present and not updated above
if (email == null) {
contactOp.addEmail(user.getEmail());
if (!existingEmail) {
contactOp.addEmail(rawContact.getEmail());
}
// Add the avatar if we didn't update the existing avatar
if (!existingAvatar) {
contactOp.addAvatar(rawContact.getAvatarUrl());
}
// If we need to update the serverId of the contact record, take
// care of that. This will happen if the contact is created on the
// client, and then synced to the server. When we get the updated
// record back from the server, we can set the SOURCE_ID property
// on the contact, so we can (in the future) lookup contacts by
// the serverId.
if (updateServerId) {
Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
contactOp.updateServerId(rawContact.getServerContactId(), uri);
}
// If we don't have a status profile, then create one. This could
// happen for contacts that were created on the client - we don't
// create the status profile until after the first sync...
final long serverId = rawContact.getServerContactId();
final long profileId = lookupProfile(resolver, serverId);
if (profileId <= 0) {
contactOp.addProfileAction(serverId);
}
}
/**
* Deletes a contact from the platform contacts provider.
* When we first add a sync adapter to the system, the contacts from that
* sync adapter will be hidden unless they're merged/grouped with an existing
* contact. But typically we want to actually show those contacts, so we
* need to mess with the Settings table to get them to show up.
*
* @param context the Authenticator Activity context
* @param account the Account who's visibility we're changing
* @param visible true if we want the contacts visible, false for hidden
*/
public static void setAccountContactsVisibility(Context context, Account account,
boolean visible) {
ContentValues values = new ContentValues();
values.put(RawContacts.ACCOUNT_NAME, account.name);
values.put(RawContacts.ACCOUNT_TYPE, Constants.ACCOUNT_TYPE);
values.put(Settings.UNGROUPED_VISIBLE, visible ? 1 : 0);
context.getContentResolver().insert(Settings.CONTENT_URI, values);
}
/**
* Return a User object with data extracted from a contact stored
* in the local contacts database.
*
* Because a contact is actually stored over several rows in the
* database, our query will return those multiple rows of information.
* We then iterate over the rows and build the User structure from
* what we find.
*
* @param context the Authenticator Activity context
* @param rawContactId the unique ID for the local contact
* @return a User object containing info on that contact
*/
private static RawContact getRawContact(Context context, long rawContactId) {
String firstName = null;
String lastName = null;
String fullName = null;
String cellPhone = null;
String homePhone = null;
String workPhone = null;
String email = null;
long serverId = -1;
final ContentResolver resolver = context.getContentResolver();
final Cursor c =
resolver.query(DataQuery.CONTENT_URI, DataQuery.PROJECTION, DataQuery.SELECTION,
new String[] {String.valueOf(rawContactId)}, null);
try {
while (c.moveToNext()) {
final long id = c.getLong(DataQuery.COLUMN_ID);
final String mimeType = c.getString(DataQuery.COLUMN_MIMETYPE);
final long tempServerId = c.getLong(DataQuery.COLUMN_SERVER_ID);
if (tempServerId > 0) {
serverId = tempServerId;
}
final Uri uri = ContentUris.withAppendedId(Data.CONTENT_URI, id);
if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) {
lastName = c.getString(DataQuery.COLUMN_FAMILY_NAME);
firstName = c.getString(DataQuery.COLUMN_GIVEN_NAME);
fullName = c.getString(DataQuery.COLUMN_FULL_NAME);
} else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) {
final int type = c.getInt(DataQuery.COLUMN_PHONE_TYPE);
if (type == Phone.TYPE_MOBILE) {
cellPhone = c.getString(DataQuery.COLUMN_PHONE_NUMBER);
} else if (type == Phone.TYPE_HOME) {
homePhone = c.getString(DataQuery.COLUMN_PHONE_NUMBER);
} else if (type == Phone.TYPE_WORK) {
workPhone = c.getString(DataQuery.COLUMN_PHONE_NUMBER);
}
} else if (mimeType.equals(Email.CONTENT_ITEM_TYPE)) {
email = c.getString(DataQuery.COLUMN_EMAIL_ADDRESS);
}
} // while
} finally {
c.close();
}
// Now that we've extracted all the information we care about,
// create the actual User object.
RawContact rawContact = RawContact.create(fullName, firstName, lastName, cellPhone,
workPhone, homePhone, email, null, false, rawContactId, serverId);
return rawContact;
}
/**
* Update the status message associated with the specified user. The status
* message would be something that is likely to be used by IM or social
* networking sync providers, and less by a straightforward contact provider.
* But it's a useful demo to see how it's done.
*
* @param context the Authenticator Activity context
* @param rawContact the contact who's status we should update
* @param batchOperation allow us to batch together multiple operations
*/
private static void updateContactStatus(Context context, RawContact rawContact,
BatchOperation batchOperation) {
final ContentValues values = new ContentValues();
final ContentResolver resolver = context.getContentResolver();
final long userId = rawContact.getServerContactId();
final String username = rawContact.getUserName();
final String status = rawContact.getStatus();
// Look up the user's sample SyncAdapter data row
final long profileId = lookupProfile(resolver, userId);
// Insert the activity into the stream
if (profileId > 0) {
values.put(StatusUpdates.DATA_ID, profileId);
values.put(StatusUpdates.STATUS, status);
values.put(StatusUpdates.PROTOCOL, Im.PROTOCOL_CUSTOM);
values.put(StatusUpdates.CUSTOM_PROTOCOL, CUSTOM_IM_PROTOCOL);
values.put(StatusUpdates.IM_ACCOUNT, username);
values.put(StatusUpdates.IM_HANDLE, userId);
values.put(StatusUpdates.STATUS_RES_PACKAGE, context.getPackageName());
values.put(StatusUpdates.STATUS_ICON, R.drawable.icon);
values.put(StatusUpdates.STATUS_LABEL, R.string.label);
batchOperation.add(ContactOperations.newInsertCpo(StatusUpdates.CONTENT_URI,
false, true).withValues(values).build());
}
}
/**
* Clear the local system 'dirty' flag for a contact.
*
* @param context the Authenticator Activity context
* @param rawContactId the id of the contact update
* @param batchOperation allow us to batch together multiple operations
*/
private static void clearDirtyFlag(Context context, long rawContactId,
BatchOperation batchOperation) {
final ContactOperations contactOp =
ContactOperations.updateExistingContact(context, rawContactId, true,
batchOperation);
final Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
contactOp.updateDirtyFlag(false, uri);
}
/**
* Deletes a contact from the platform contacts provider. This method is used
* both for contacts that were deleted locally and then that deletion was synced
* to the server, and for contacts that were deleted on the server and the
* deletion was synced to the client.
*
* @param context the Authenticator Activity context
* @param rawContactId the unique Id for this rawContact in contacts
@@ -222,33 +541,37 @@ public class ContactManager {
BatchOperation batchOperation) {
batchOperation.add(ContactOperations.newDeleteCpo(
ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId), true).build());
ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId),
true, true).build());
}
/**
* Returns the RawContact id for a sample SyncAdapter contact, or 0 if the
* sample SyncAdapter user isn't found.
*
* @param context the Authenticator Activity context
* @param userId the sample SyncAdapter user ID to lookup
* @param resolver the content resolver to use
* @param serverContactId the sample SyncAdapter user ID to lookup
* @return the RawContact id, or 0 if not found
*/
private static long lookupRawContact(ContentResolver resolver, long userId) {
private static long lookupRawContact(ContentResolver resolver, long serverContactId) {
long authorId = 0;
final Cursor c =
resolver.query(RawContacts.CONTENT_URI, UserIdQuery.PROJECTION, UserIdQuery.SELECTION,
new String[] {String.valueOf(userId)}, null);
long rawContactId = 0;
final Cursor c = resolver.query(
UserIdQuery.CONTENT_URI,
UserIdQuery.PROJECTION,
UserIdQuery.SELECTION,
new String[] {String.valueOf(serverContactId)},
null);
try {
if (c.moveToFirst()) {
authorId = c.getLong(UserIdQuery.COLUMN_ID);
if ((c != null) && c.moveToFirst()) {
rawContactId = c.getLong(UserIdQuery.COLUMN_RAW_CONTACT_ID);
}
} finally {
if (c != null) {
c.close();
}
}
return authorId;
return rawContactId;
}
/**
@@ -266,7 +589,7 @@ public class ContactManager {
resolver.query(Data.CONTENT_URI, ProfileQuery.PROJECTION, ProfileQuery.SELECTION,
new String[] {String.valueOf(userId)}, null);
try {
if (c != null && c.moveToFirst()) {
if ((c != null) && c.moveToFirst()) {
profileId = c.getLong(ProfileQuery.COLUMN_ID);
}
} finally {
@@ -277,6 +600,46 @@ public class ContactManager {
return profileId;
}
final public static class EditorQuery {
private EditorQuery() {
}
public static final String[] PROJECTION = new String[] {
RawContacts.ACCOUNT_NAME,
Data._ID,
RawContacts.Entity.DATA_ID,
Data.MIMETYPE,
Data.DATA1,
Data.DATA2,
Data.DATA3,
Data.DATA15,
Data.SYNC1
};
public static final int COLUMN_ACCOUNT_NAME = 0;
public static final int COLUMN_RAW_CONTACT_ID = 1;
public static final int COLUMN_DATA_ID = 2;
public static final int COLUMN_MIMETYPE = 3;
public static final int COLUMN_DATA1 = 4;
public static final int COLUMN_DATA2 = 5;
public static final int COLUMN_DATA3 = 6;
public static final int COLUMN_DATA15 = 7;
public static final int COLUMN_SYNC1 = 8;
public static final int COLUMN_PHONE_NUMBER = COLUMN_DATA1;
public static final int COLUMN_PHONE_TYPE = COLUMN_DATA2;
public static final int COLUMN_EMAIL_ADDRESS = COLUMN_DATA1;
public static final int COLUMN_EMAIL_TYPE = COLUMN_DATA2;
public static final int COLUMN_FULL_NAME = COLUMN_DATA1;
public static final int COLUMN_GIVEN_NAME = COLUMN_DATA2;
public static final int COLUMN_FAMILY_NAME = COLUMN_DATA3;
public static final int COLUMN_AVATAR_IMAGE = COLUMN_DATA15;
public static final int COLUMN_SYNC_DIRTY = COLUMN_SYNC1;
public static final String SELECTION = Data.RAW_CONTACT_ID + "=?";
}
/**
* Constants for a query to find a contact given a sample SyncAdapter user
* ID.
@@ -304,15 +667,55 @@ public class ContactManager {
private UserIdQuery() {
}
public final static String[] PROJECTION = new String[] {RawContacts._ID};
public final static String[] PROJECTION = new String[] {
RawContacts._ID,
RawContacts.CONTACT_ID
};
public final static int COLUMN_ID = 0;
public final static int COLUMN_RAW_CONTACT_ID = 0;
public final static int COLUMN_LINKED_CONTACT_ID = 1;
public final static Uri CONTENT_URI = RawContacts.CONTENT_URI;
public static final String SELECTION =
RawContacts.ACCOUNT_TYPE + "='" + Constants.ACCOUNT_TYPE + "' AND "
+ RawContacts.SOURCE_ID + "=?";
}
/**
* Constants for a query to find SampleSyncAdapter contacts that are
* in need of syncing to the server. This should cover new, edited,
* and deleted contacts.
*/
final private static class DirtyQuery {
private DirtyQuery() {
}
public final static String[] PROJECTION = new String[] {
RawContacts._ID,
RawContacts.SOURCE_ID,
RawContacts.DIRTY,
RawContacts.DELETED,
RawContacts.VERSION
};
public final static int COLUMN_RAW_CONTACT_ID = 0;
public final static int COLUMN_SERVER_ID = 1;
public final static int COLUMN_DIRTY = 2;
public final static int COLUMN_DELETED = 3;
public final static int COLUMN_VERSION = 4;
public static final Uri CONTENT_URI = RawContacts.CONTENT_URI.buildUpon()
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.build();
public static final String SELECTION =
RawContacts.DIRTY + "=1 AND "
+ RawContacts.ACCOUNT_TYPE + "='" + Constants.ACCOUNT_TYPE + "' AND "
+ RawContacts.ACCOUNT_NAME + "=?";
}
/**
* Constants for a query to get contact data for a given rawContactId
*/
@@ -322,29 +725,29 @@ public class ContactManager {
}
public static final String[] PROJECTION =
new String[] {Data._ID, Data.MIMETYPE, Data.DATA1, Data.DATA2, Data.DATA3,};
new String[] {Data._ID, RawContacts.SOURCE_ID, Data.MIMETYPE, Data.DATA1,
Data.DATA2, Data.DATA3, Data.DATA15, Data.SYNC1};
public static final int COLUMN_ID = 0;
public static final int COLUMN_SERVER_ID = 1;
public static final int COLUMN_MIMETYPE = 2;
public static final int COLUMN_DATA1 = 3;
public static final int COLUMN_DATA2 = 4;
public static final int COLUMN_DATA3 = 5;
public static final int COLUMN_DATA15 = 6;
public static final int COLUMN_SYNC1 = 7;
public static final int COLUMN_MIMETYPE = 1;
public static final int COLUMN_DATA1 = 2;
public static final int COLUMN_DATA2 = 3;
public static final int COLUMN_DATA3 = 4;
public static final Uri CONTENT_URI = Data.CONTENT_URI;
public static final int COLUMN_PHONE_NUMBER = COLUMN_DATA1;
public static final int COLUMN_PHONE_TYPE = COLUMN_DATA2;
public static final int COLUMN_EMAIL_ADDRESS = COLUMN_DATA1;
public static final int COLUMN_EMAIL_TYPE = COLUMN_DATA2;
public static final int COLUMN_FULL_NAME = COLUMN_DATA1;
public static final int COLUMN_GIVEN_NAME = COLUMN_DATA2;
public static final int COLUMN_FAMILY_NAME = COLUMN_DATA3;
public static final int COLUMN_AVATAR_IMAGE = COLUMN_DATA15;
public static final int COLUMN_SYNC_DIRTY = COLUMN_SYNC1;
public static final String SELECTION = Data.RAW_CONTACT_ID + "=?";
}

View File

@@ -15,56 +15,62 @@
*/
package com.example.android.samplesync.platform;
import com.example.android.samplesync.Constants;
import com.example.android.samplesync.R;
import com.example.android.samplesync.client.NetworkUtilities;
import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.content.Context;
import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;
import android.text.TextUtils;
import android.util.Log;
import com.example.android.samplesync.Constants;
import com.example.android.samplesync.R;
/**
* Helper class for storing data in the platform content providers.
*/
public class ContactOperations {
private final ContentValues mValues;
private ContentProviderOperation.Builder mBuilder;
private final BatchOperation mBatchOperation;
private final Context mContext;
private boolean mYield;
private boolean mIsSyncOperation;
private long mRawContactId;
private int mBackReference;
private boolean mIsNewContact;
/**
* Since we're sending a lot of contact provider operations in a single
* batched operation, we want to make sure that we "yield" periodically
* so that the Contact Provider can write changes to the DB, and can
* open a new transaction. This prevents ANR (application not responding)
* errors. The recommended time to specify that a yield is permitted is
* with the first operation on a particular contact. So if we're updating
* multiple fields for a single contact, we make sure that we call
* withYieldAllowed(true) on the first field that we update. We use
* mIsYieldAllowed to keep track of what value we should pass to
* withYieldAllowed().
*/
private boolean mIsYieldAllowed;
/**
* Returns an instance of ContactOperations instance for adding new contact
* to the platform contacts provider.
*
* @param context the Authenticator Activity context
* @param userId the userId of the sample SyncAdapter user object
* @param accountName the username of the current login
* @param accountName the username for the SyncAdapter account
* @param isSyncOperation are we executing this as part of a sync operation?
* @return instance of ContactOperations
*/
public static ContactOperations createNewContact(Context context, int userId,
String accountName, BatchOperation batchOperation) {
return new ContactOperations(context, userId, accountName, batchOperation);
public static ContactOperations createNewContact(Context context, long userId,
String accountName, boolean isSyncOperation, BatchOperation batchOperation) {
return new ContactOperations(context, userId, accountName, isSyncOperation, batchOperation);
}
/**
@@ -73,57 +79,70 @@ public class ContactOperations {
*
* @param context the Authenticator Activity context
* @param rawContactId the unique Id of the existing rawContact
* @param isSyncOperation are we executing this as part of a sync operation?
* @return instance of ContactOperations
*/
public static ContactOperations updateExistingContact(Context context, long rawContactId,
BatchOperation batchOperation) {
return new ContactOperations(context, rawContactId, batchOperation);
boolean isSyncOperation, BatchOperation batchOperation) {
return new ContactOperations(context, rawContactId, isSyncOperation, batchOperation);
}
public ContactOperations(Context context, BatchOperation batchOperation) {
public ContactOperations(Context context, boolean isSyncOperation,
BatchOperation batchOperation) {
mValues = new ContentValues();
mYield = true;
mIsYieldAllowed = true;
mIsSyncOperation = isSyncOperation;
mContext = context;
mBatchOperation = batchOperation;
}
public ContactOperations(Context context, int userId, String accountName,
BatchOperation batchOperation) {
this(context, batchOperation);
public ContactOperations(Context context, long userId, String accountName,
boolean isSyncOperation, BatchOperation batchOperation) {
this(context, isSyncOperation, batchOperation);
mBackReference = mBatchOperation.size();
mIsNewContact = true;
mValues.put(RawContacts.SOURCE_ID, userId);
mValues.put(RawContacts.ACCOUNT_TYPE, Constants.ACCOUNT_TYPE);
mValues.put(RawContacts.ACCOUNT_NAME, accountName);
mBuilder = newInsertCpo(RawContacts.CONTENT_URI, true).withValues(mValues);
mBatchOperation.add(mBuilder.build());
ContentProviderOperation.Builder builder =
newInsertCpo(RawContacts.CONTENT_URI, mIsSyncOperation, true).withValues(mValues);
mBatchOperation.add(builder.build());
}
public ContactOperations(Context context, long rawContactId, BatchOperation batchOperation) {
this(context, batchOperation);
public ContactOperations(Context context, long rawContactId, boolean isSyncOperation,
BatchOperation batchOperation) {
this(context, isSyncOperation, batchOperation);
mIsNewContact = false;
mRawContactId = rawContactId;
}
/**
* Adds a contact name
* Adds a contact name. We can take either a full name ("Bob Smith") or separated
* first-name and last-name ("Bob" and "Smith").
*
* @param name Name of contact
* @param nameType type of name: family name, given name, etc.
* @param fullName The full name of the contact - typically from an edit form
* Can be null if firstName/lastName are specified.
* @param firstName The first name of the contact - can be null if fullName
* is specified.
* @param lastName The last name of the contact - can be null if fullName
* is specified.
* @return instance of ContactOperations
*/
public ContactOperations addName(String firstName, String lastName) {
public ContactOperations addName(String fullName, String firstName, String lastName) {
mValues.clear();
if (!TextUtils.isEmpty(firstName)) {
mValues.put(StructuredName.GIVEN_NAME, firstName);
mValues.put(StructuredName.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
}
if (!TextUtils.isEmpty(lastName)) {
mValues.put(StructuredName.FAMILY_NAME, lastName);
if (!TextUtils.isEmpty(fullName)) {
mValues.put(StructuredName.DISPLAY_NAME, fullName);
mValues.put(StructuredName.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
} else {
if (!TextUtils.isEmpty(firstName)) {
mValues.put(StructuredName.GIVEN_NAME, firstName);
mValues.put(StructuredName.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
}
if (!TextUtils.isEmpty(lastName)) {
mValues.put(StructuredName.FAMILY_NAME, lastName);
mValues.put(StructuredName.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
}
}
if (mValues.size() > 0) {
addInsertOp();
@@ -134,7 +153,7 @@ public class ContactOperations {
/**
* Adds an email
*
* @param new email for user
* @param the email address we're adding
* @return instance of ContactOperations
*/
public ContactOperations addEmail(String email) {
@@ -166,6 +185,19 @@ public class ContactOperations {
return this;
}
public ContactOperations addAvatar(String avatarUrl) {
if (avatarUrl != null) {
byte[] avatarBuffer = NetworkUtilities.downloadAvatar(avatarUrl);
if (avatarBuffer != null) {
mValues.clear();
mValues.put(Photo.PHOTO, avatarBuffer);
mValues.put(Photo.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
addInsertOp();
}
}
return this;
}
/**
* Adds a profile action
*
@@ -186,6 +218,20 @@ public class ContactOperations {
return this;
}
/**
* Updates contact's serverId
*
* @param serverId the serverId for this contact
* @param uri Uri for the existing raw contact to be updated
* @return instance of ContactOperations
*/
public ContactOperations updateServerId(long serverId, Uri uri) {
mValues.clear();
mValues.put(RawContacts.SOURCE_ID, serverId);
addUpdateOp(uri);
return this;
}
/**
* Updates contact's email
*
@@ -203,25 +249,38 @@ public class ContactOperations {
}
/**
* Updates contact's name
* Updates contact's name. The caller can either provide first-name
* and last-name fields or a full-name field.
*
* @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
* @param existingFirstName the first name stored in provider
* @param existingLastName the last name stored in provider
* @param existingFullName the full name stored in provider
* @param firstName the new first name to store
* @param lastName the new last name to store
* @param fullName the new full name to store
* @return instance of ContactOperations
*/
public ContactOperations updateName(Uri uri, String existingFirstName, String existingLastName,
String firstName, String lastName) {
public ContactOperations updateName(Uri uri,
String existingFirstName,
String existingLastName,
String existingFullName,
String firstName,
String lastName,
String fullName) {
Log.i("ContactOperations", "ef=" + existingFirstName + "el=" + existingLastName + "f="
+ firstName + "l=" + lastName);
mValues.clear();
if (!TextUtils.equals(existingFirstName, firstName)) {
mValues.put(StructuredName.GIVEN_NAME, firstName);
}
if (!TextUtils.equals(existingLastName, lastName)) {
mValues.put(StructuredName.FAMILY_NAME, lastName);
if (TextUtils.isEmpty(fullName)) {
if (!TextUtils.equals(existingFirstName, firstName)) {
mValues.put(StructuredName.GIVEN_NAME, firstName);
}
if (!TextUtils.equals(existingLastName, lastName)) {
mValues.put(StructuredName.FAMILY_NAME, lastName);
}
} else {
if (!TextUtils.equals(existingFullName, fullName)) {
mValues.put(StructuredName.DISPLAY_NAME, fullName);
}
}
if (mValues.size() > 0) {
addUpdateOp(uri);
@@ -229,6 +288,14 @@ public class ContactOperations {
return this;
}
public ContactOperations updateDirtyFlag(boolean isDirty, Uri uri) {
int isDirtyValue = isDirty ? 1 : 0;
mValues.clear();
mValues.put(RawContacts.DIRTY, isDirtyValue);
addUpdateOp(uri);
return this;
}
/**
* Updates contact's phone
*
@@ -246,6 +313,19 @@ public class ContactOperations {
return this;
}
public ContactOperations updateAvatar(String avatarUrl, Uri uri) {
if (avatarUrl != null) {
byte[] avatarBuffer = NetworkUtilities.downloadAvatar(avatarUrl);
if (avatarBuffer != null) {
mValues.clear();
mValues.put(Photo.PHOTO, avatarBuffer);
mValues.put(Photo.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
addUpdateOp(uri);
}
}
return this;
}
/**
* Updates contact's profile action
*
@@ -268,41 +348,62 @@ public class ContactOperations {
if (!mIsNewContact) {
mValues.put(Phone.RAW_CONTACT_ID, mRawContactId);
}
mBuilder = newInsertCpo(addCallerIsSyncAdapterParameter(Data.CONTENT_URI), mYield);
mBuilder.withValues(mValues);
ContentProviderOperation.Builder builder =
newInsertCpo(Data.CONTENT_URI, mIsSyncOperation, mIsYieldAllowed);
builder.withValues(mValues);
if (mIsNewContact) {
mBuilder.withValueBackReference(Data.RAW_CONTACT_ID, mBackReference);
builder.withValueBackReference(Data.RAW_CONTACT_ID, mBackReference);
}
mYield = false;
mBatchOperation.add(mBuilder.build());
mIsYieldAllowed = false;
mBatchOperation.add(builder.build());
}
/**
* Adds an update operation into the batch
*/
private void addUpdateOp(Uri uri) {
mBuilder = newUpdateCpo(uri, mYield).withValues(mValues);
mYield = false;
mBatchOperation.add(mBuilder.build());
ContentProviderOperation.Builder builder =
newUpdateCpo(uri, mIsSyncOperation, mIsYieldAllowed).withValues(mValues);
mIsYieldAllowed = false;
mBatchOperation.add(builder.build());
}
public static ContentProviderOperation.Builder newInsertCpo(Uri uri, boolean yield) {
return ContentProviderOperation.newInsert(addCallerIsSyncAdapterParameter(uri))
.withYieldAllowed(yield);
public static ContentProviderOperation.Builder newInsertCpo(Uri uri,
boolean isSyncOperation, boolean isYieldAllowed) {
return ContentProviderOperation
.newInsert(addCallerIsSyncAdapterParameter(uri, isSyncOperation))
.withYieldAllowed(isYieldAllowed);
}
public static ContentProviderOperation.Builder newUpdateCpo(Uri uri, boolean yield) {
return ContentProviderOperation.newUpdate(addCallerIsSyncAdapterParameter(uri))
.withYieldAllowed(yield);
public static ContentProviderOperation.Builder newUpdateCpo(Uri uri,
boolean isSyncOperation, boolean isYieldAllowed) {
return ContentProviderOperation
.newUpdate(addCallerIsSyncAdapterParameter(uri, isSyncOperation))
.withYieldAllowed(isYieldAllowed);
}
public static ContentProviderOperation.Builder newDeleteCpo(Uri uri, boolean yield) {
return ContentProviderOperation.newDelete(addCallerIsSyncAdapterParameter(uri))
.withYieldAllowed(yield);
public static ContentProviderOperation.Builder newDeleteCpo(Uri uri,
boolean isSyncOperation, boolean isYieldAllowed) {
return ContentProviderOperation
.newDelete(addCallerIsSyncAdapterParameter(uri, isSyncOperation))
.withYieldAllowed(isYieldAllowed);
}
private static Uri addCallerIsSyncAdapterParameter(Uri uri) {
return uri.buildUpon().appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.build();
private static Uri addCallerIsSyncAdapterParameter(Uri uri, boolean isSyncOperation) {
if (isSyncOperation) {
// If we're in the middle of a real sync-adapter operation, then go ahead
// and tell the Contacts provider that we're the sync adapter. That
// gives us some special permissions - like the ability to really
// delete a contact, and the ability to clear the dirty flag.
//
// If we're not in the middle of a sync operation (for example, we just
// locally created/edited a new contact), then we don't want to use
// the special permissions, and the system will automagically mark
// the contact as 'dirty' for us!
return uri.buildUpon()
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.build();
}
return uri;
}
}

View File

@@ -24,12 +24,12 @@ import android.content.ContentProviderClient;
import android.content.Context;
import android.content.SyncResult;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import com.example.android.samplesync.Constants;
import com.example.android.samplesync.client.NetworkUtilities;
import com.example.android.samplesync.client.User;
import com.example.android.samplesync.client.User.Status;
import com.example.android.samplesync.client.RawContact;
import com.example.android.samplesync.platform.ContactManager;
import org.apache.http.ParseException;
@@ -37,23 +37,27 @@ import org.apache.http.auth.AuthenticationException;
import org.json.JSONException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* SyncAdapter implementation for syncing sample SyncAdapter contacts to the
* platform ContactOperations provider.
* platform ContactOperations provider. This sample shows a basic 2-way
* sync between the client and a sample server. It also contains an
* example of how to update the contacts' status messages, which
* would be useful for a messaging or social networking client.
*/
public class SyncAdapter extends AbstractThreadedSyncAdapter {
private static final String TAG = "SyncAdapter";
private static final String SYNC_MARKER_KEY = "com.example.android.samplesync.marker";
private static final boolean NOTIFY_AUTH_FAILURE = true;
private final AccountManager mAccountManager;
private final Context mContext;
private Date mLastUpdated;
public SyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
mContext = context;
@@ -64,42 +68,101 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter {
public void onPerformSync(Account account, Bundle extras, String authority,
ContentProviderClient provider, SyncResult syncResult) {
List<User> users;
List<Status> statuses;
String authtoken = null;
try {
// use the account manager to request the credentials
authtoken =
mAccountManager
.blockingGetAuthToken(account, Constants.AUTHTOKEN_TYPE, true /* notifyAuthFailure */);
// fetch updates from the sample service over the cloud
users = NetworkUtilities.fetchFriendUpdates(account, authtoken, mLastUpdated);
// update the last synced date.
mLastUpdated = new Date();
// update platform contacts.
// see if we already have a sync-state attached to this account. By handing
// This value to the server, we can just get the contacts that have
// been updated on the server-side since our last sync-up
long lastSyncMarker = getServerSyncMarker(account);
// By default, contacts from a 3rd party provider are hidden in the contacts
// list. So let's set the flag that causes them to be visible, so that users
// can actually see these contacts.
if (lastSyncMarker == 0) {
ContactManager.setAccountContactsVisibility(getContext(), account, true);
}
List<RawContact> dirtyContacts;
List<RawContact> updatedContacts;
// Use the account manager to request the AuthToken we'll need
// to talk to our sample server. If we don't have an AuthToken
// yet, this could involve a round-trip to the server to request
// and AuthToken.
final String authtoken = mAccountManager.blockingGetAuthToken(account,
Constants.AUTHTOKEN_TYPE, NOTIFY_AUTH_FAILURE);
// Find the local 'dirty' contacts that we need to tell the server about...
// Find the local users that need to be sync'd to the server...
dirtyContacts = ContactManager.getDirtyContacts(mContext, account);
// Send the dirty contacts to the server, and retrieve the server-side changes
updatedContacts = NetworkUtilities.syncContacts(account, authtoken,
lastSyncMarker, dirtyContacts);
// Update the local contacts database with the changes. updateContacts()
// returns a syncState value that indicates the high-water-mark for
// the changes we received.
Log.d(TAG, "Calling contactManager's sync contacts");
ContactManager.syncContacts(mContext, account.name, users);
// fetch and update status messages for all the synced users.
statuses = NetworkUtilities.fetchFriendStatuses(account, authtoken);
ContactManager.insertStatuses(mContext, account.name, statuses);
long newSyncState = ContactManager.updateContacts(mContext,
account.name,
updatedContacts,
lastSyncMarker);
// This is a demo of how you can update IM-style status messages
// for contacts on the client. This probably won't apply to
// 2-way contact sync providers - it's more likely that one-way
// sync providers (IM clients, social networking apps, etc) would
// use this feature.
ContactManager.updateStatusMessages(mContext, updatedContacts);
// Save off the new sync marker. On our next sync, we only want to receive
// contacts that have changed since this sync...
setServerSyncMarker(account, newSyncState);
if (dirtyContacts.size() > 0) {
ContactManager.clearSyncFlags(mContext, dirtyContacts);
}
} catch (final AuthenticatorException e) {
syncResult.stats.numParseExceptions++;
Log.e(TAG, "AuthenticatorException", e);
syncResult.stats.numParseExceptions++;
} catch (final OperationCanceledException e) {
Log.e(TAG, "OperationCanceledExcetpion", e);
} catch (final IOException e) {
Log.e(TAG, "IOException", e);
syncResult.stats.numIoExceptions++;
} catch (final AuthenticationException e) {
mAccountManager.invalidateAuthToken(Constants.ACCOUNT_TYPE, authtoken);
syncResult.stats.numAuthExceptions++;
Log.e(TAG, "AuthenticationException", e);
syncResult.stats.numAuthExceptions++;
} catch (final ParseException e) {
syncResult.stats.numParseExceptions++;
Log.e(TAG, "ParseException", e);
} catch (final JSONException e) {
syncResult.stats.numParseExceptions++;
} catch (final JSONException e) {
Log.e(TAG, "JSONException", e);
syncResult.stats.numParseExceptions++;
}
}
/**
* This helper function fetches the last known high-water-mark
* we received from the server - or 0 if we've never synced.
* @param account the account we're syncing
* @return the change high-water-mark
*/
private long getServerSyncMarker(Account account) {
String markerString = mAccountManager.getUserData(account, SYNC_MARKER_KEY);
if (!TextUtils.isEmpty(markerString)) {
return Long.parseLong(markerString);
}
return 0;
}
/**
* Save off the high-water-mark we receive back from the server.
* @param account The account we're syncing
* @param marker The high-water-mark we want to save.
*/
private void setServerSyncMarker(Account account, long marker) {
mAccountManager.setUserData(account, SYNC_MARKER_KEY, Long.toString(marker));
}
}