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:
@@ -6,10 +6,12 @@ LOCAL_MODULE_TAGS := samples tests
|
|||||||
# Only compile source java files in this apk.
|
# Only compile source java files in this apk.
|
||||||
LOCAL_SRC_FILES := $(call all-java-files-under, src)
|
LOCAL_SRC_FILES := $(call all-java-files-under, src)
|
||||||
|
|
||||||
LOCAL_PACKAGE_NAME := Voiper
|
LOCAL_PACKAGE_NAME := SampleSyncAdapter
|
||||||
|
|
||||||
LOCAL_SDK_VERSION := current
|
LOCAL_SDK_VERSION := current
|
||||||
|
|
||||||
|
LOCAL_DX_FLAGS=--target-api=11
|
||||||
|
|
||||||
include $(BUILD_PACKAGE)
|
include $(BUILD_PACKAGE)
|
||||||
|
|
||||||
# Use the folloing include to make our test apk.
|
# Use the folloing include to make our test apk.
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="5" />
|
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="11"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:icon="@drawable/icon"
|
android:icon="@drawable/icon"
|
||||||
@@ -82,11 +82,36 @@
|
|||||||
android:label="@string/ui_activity_title"
|
android:label="@string/ui_activity_title"
|
||||||
android:theme="@android:style/Theme.Dialog"
|
android:theme="@android:style/Theme.Dialog"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
|
android:configChanges="orientation"
|
||||||
>
|
>
|
||||||
<!--
|
<!--
|
||||||
No intent-filter here! This activity is only ever launched by
|
No intent-filter here! This activity is only ever launched by
|
||||||
someone who explicitly knows the class name
|
someone who explicitly knows the class name
|
||||||
-->
|
-->
|
||||||
</activity>
|
</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>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
cloud-based service and synchronize its data with data stored locally in a
|
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
|
content provider. The sample uses two related parts of the Android framework
|
||||||
— the account manager and the synchronization manager (through a sync
|
— 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
|
<p> The <a
|
||||||
href="../../../reference/android/accounts/AccountManager.html">account
|
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>
|
issues a sync operation for that sync adapter. </p>
|
||||||
|
|
||||||
<p> The cloud-based service for this sample application is running at: </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"
|
<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 |
|
account will be added to your phone's account manager. You can go to "Settings |
|
||||||
|
|||||||
6
samples/SampleSyncAdapter/res/drawable/border.xml
Normal file
6
samples/SampleSyncAdapter/res/drawable/border.xml
Normal 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>
|
||||||
BIN
samples/SampleSyncAdapter/res/drawable/done_menu_icon.png
Normal file
BIN
samples/SampleSyncAdapter/res/drawable/done_menu_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 658 B |
43
samples/SampleSyncAdapter/res/layout-xlarge/editor.xml
Normal file
43
samples/SampleSyncAdapter/res/layout-xlarge/editor.xml
Normal 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>
|
||||||
@@ -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>
|
||||||
36
samples/SampleSyncAdapter/res/layout/editor.xml
Normal file
36
samples/SampleSyncAdapter/res/layout/editor.xml
Normal 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>
|
||||||
127
samples/SampleSyncAdapter/res/layout/editor_fields.xml
Normal file
127
samples/SampleSyncAdapter/res/layout/editor_fields.xml
Normal 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>
|
||||||
30
samples/SampleSyncAdapter/res/menu/edit.xml
Normal file
30
samples/SampleSyncAdapter/res/menu/edit.xml
Normal 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>
|
||||||
24
samples/SampleSyncAdapter/res/values-xlarge/dimens.xml
Normal file
24
samples/SampleSyncAdapter/res/values-xlarge/dimens.xml
Normal 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>
|
||||||
25
samples/SampleSyncAdapter/res/values/dimens.xml
Normal file
25
samples/SampleSyncAdapter/res/values/dimens.xml
Normal 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>
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<!-- Label for this package -->
|
<!-- Label for this package -->
|
||||||
<string
|
<string
|
||||||
name="label">SamplesyncAdapter</string>
|
name="label">Sample SyncAdapter</string>
|
||||||
|
|
||||||
<!-- Permission label -->
|
<!-- Permission label -->
|
||||||
<string
|
<string
|
||||||
@@ -90,4 +90,16 @@
|
|||||||
name="profile_action">Sample profile</string>
|
name="profile_action">Sample profile</string>
|
||||||
<string
|
<string
|
||||||
name="view_profile">View Profile</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>
|
</resources>
|
||||||
27
samples/SampleSyncAdapter/res/values/styles.xml
Normal file
27
samples/SampleSyncAdapter/res/values/styles.xml
Normal 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>
|
||||||
33
samples/SampleSyncAdapter/res/xml-v11/contacts.xml
Normal file
33
samples/SampleSyncAdapter/res/xml-v11/contacts.xml
Normal 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>
|
||||||
33
samples/SampleSyncAdapter/res/xml-v11/syncadapter.xml
Normal file
33
samples/SampleSyncAdapter/res/xml-v11/syncadapter.xml
Normal 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"
|
||||||
|
/>
|
||||||
@@ -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
|
<ContactsDataKind
|
||||||
android:mimeType="vnd.android.cursor.item/vnd.samplesyncadapter.profile"
|
android:mimeType="vnd.android.cursor.item/vnd.samplesyncadapter.profile"
|
||||||
|
|||||||
@@ -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"
|
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:contentAuthority="com.android.contacts"
|
android:contentAuthority="com.android.contacts"
|
||||||
android:accountType="com.example.android.samplesync"
|
android:accountType="com.example.android.samplesync"
|
||||||
android:supportsUploading="false"
|
android:supportsUploading="false"
|
||||||
|
android:userVisible="true"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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
|
version: 1
|
||||||
runtime: python
|
runtime: python
|
||||||
api_version: 1
|
api_version: 1
|
||||||
|
|
||||||
handlers:
|
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
|
- url: /auth
|
||||||
script: main.py
|
script: web_services.py
|
||||||
|
|
||||||
- url: /login
|
- url: /sync
|
||||||
script: main.py
|
script: web_services.py
|
||||||
|
|
||||||
- url: /fetch_friend_updates
|
- url: /reset_database
|
||||||
script: main.py
|
script: web_services.py
|
||||||
|
|
||||||
- url: /fetch_status
|
#
|
||||||
script: main.py
|
# Route all page requests to the dashboard.py file
|
||||||
|
#
|
||||||
- url: /add_user
|
- url: /
|
||||||
script: dashboard.py
|
script: dashboard.py
|
||||||
|
|
||||||
- url: /edit_user
|
- url: /add_contact
|
||||||
script: dashboard.py
|
script: dashboard.py
|
||||||
|
|
||||||
- url: /users
|
- url: /edit_contact
|
||||||
script: dashboard.py
|
script: dashboard.py
|
||||||
|
|
||||||
- url: /delete_friend
|
- url: /delete_contact
|
||||||
script: dashboard.py
|
script: dashboard.py
|
||||||
|
|
||||||
- url: /edit_user
|
- url: /avatar
|
||||||
script: dashboard.py
|
script: dashboard.py
|
||||||
|
|
||||||
- url: /add_credentials
|
- url: /edit_avatar
|
||||||
script: dashboard.py
|
|
||||||
|
|
||||||
- url: /user_credentials
|
|
||||||
script: dashboard.py
|
|
||||||
|
|
||||||
- url: /add_friend
|
|
||||||
script: dashboard.py
|
|
||||||
|
|
||||||
- url: /user_friends
|
|
||||||
script: dashboard.py
|
script: dashboard.py
|
||||||
|
|||||||
24
samples/SampleSyncAdapter/samplesyncadapter_server/cron.yaml
Normal file
24
samples/SampleSyncAdapter/samplesyncadapter_server/cron.yaml
Normal 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
|
||||||
@@ -14,8 +14,10 @@
|
|||||||
# License for the specific language governing permissions and limitations under
|
# License for the specific language governing permissions and limitations under
|
||||||
# the License.
|
# 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 cgi
|
||||||
import datetime
|
import datetime
|
||||||
@@ -26,245 +28,178 @@ from google.appengine.ext import webapp
|
|||||||
from google.appengine.ext.webapp import template
|
from google.appengine.ext.webapp import template
|
||||||
from google.appengine.ext.db import djangoforms
|
from google.appengine.ext.db import djangoforms
|
||||||
from model import datastore
|
from model import datastore
|
||||||
|
from google.appengine.api import images
|
||||||
|
|
||||||
import wsgiref.handlers
|
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 = ''
|
||||||
|
|
||||||
|
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:
|
class Meta:
|
||||||
model = datastore.User
|
model = datastore.Contact
|
||||||
|
|
||||||
|
|
||||||
class UserInsertPage(webapp.RequestHandler):
|
class ContactInsertPage(BaseRequestHandler):
|
||||||
"""Inserts new users. GET presents a blank form. POST processes it."""
|
"""
|
||||||
|
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):
|
def get(self):
|
||||||
self.response.out.write('<html><body>'
|
self.send_form('Add Contact', '/add_contact', -1, None, ContactForm())
|
||||||
'<form method="POST" '
|
|
||||||
'action="/add_user">'
|
|
||||||
'<table>')
|
|
||||||
# This generates our shopping list form and writes it in the response
|
|
||||||
self.response.out.write(UserForm())
|
|
||||||
self.response.out.write('</table>'
|
|
||||||
'<input type="submit">'
|
|
||||||
'</form></body></html>')
|
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
data = UserForm(data=self.request.POST)
|
data = ContactForm(data=self.request.POST)
|
||||||
if data.is_valid():
|
if data.is_valid():
|
||||||
# Save the data, and redirect to the view page
|
# Save the data, and redirect to the view page
|
||||||
entity = data.save(commit=False)
|
entity = data.save(commit=False)
|
||||||
entity.put()
|
entity.put()
|
||||||
self.redirect('/users')
|
self.redirect('/')
|
||||||
else:
|
else:
|
||||||
# Reprint the form
|
# Reprint the form
|
||||||
self.response.out.write('<html><body>'
|
self.send_form('Add Contact', '/add_contact', -1, None, data)
|
||||||
'<form method="POST" '
|
|
||||||
'action="/">'
|
|
||||||
'<table>')
|
|
||||||
self.response.out.write(data)
|
|
||||||
self.response.out.write('</table>'
|
|
||||||
'<input type="submit">'
|
|
||||||
'</form></body></html>')
|
|
||||||
|
|
||||||
|
|
||||||
class UserEditPage(webapp.RequestHandler):
|
class ContactEditPage(BaseRequestHandler):
|
||||||
"""Edits users. GET presents a form prefilled with user info
|
"""
|
||||||
from datastore. POST processes it."""
|
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):
|
def get(self):
|
||||||
id = int(self.request.get('user'))
|
id = int(self.request.get('id'))
|
||||||
user = datastore.User.get(db.Key.from_path('User', id))
|
contact = datastore.Contact.get(db.Key.from_path('Contact', id))
|
||||||
self.response.out.write('<html><body>'
|
self.send_form('Edit Contact', '/edit_contact', id, contact.handle,
|
||||||
'<form method="POST" '
|
ContactForm(instance=contact))
|
||||||
'action="/edit_user">'
|
|
||||||
'<table>')
|
|
||||||
# This generates our shopping list form and writes it in the response
|
|
||||||
self.response.out.write(UserForm(instance=user))
|
|
||||||
self.response.out.write('</table>'
|
|
||||||
'<input type="hidden" name="_id" value="%s">'
|
|
||||||
'<input type="submit">'
|
|
||||||
'</form></body></html>' % id)
|
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
id = int(self.request.get('_id'))
|
id = int(self.request.get('id'))
|
||||||
user = datastore.User.get(db.Key.from_path('User', id))
|
contact = datastore.Contact.get(db.Key.from_path('Contact', id))
|
||||||
data = UserForm(data=self.request.POST, instance=user)
|
data = ContactForm(data=self.request.POST, instance=contact)
|
||||||
if data.is_valid():
|
if data.is_valid():
|
||||||
# Save the data, and redirect to the view page
|
# Save the data, and redirect to the view page
|
||||||
entity = data.save(commit=False)
|
entity = data.save(commit=False)
|
||||||
entity.updated = datetime.datetime.utcnow()
|
entity.updated = datetime.datetime.utcnow()
|
||||||
entity.put()
|
entity.put()
|
||||||
self.redirect('/users')
|
self.redirect('/')
|
||||||
else:
|
else:
|
||||||
# Reprint the form
|
# Reprint the form
|
||||||
self.response.out.write('<html><body>'
|
self.send_form('Edit Contact', '/edit_contact', id, contact.handle, data)
|
||||||
'<form method="POST" '
|
|
||||||
'action="/edit_user">'
|
|
||||||
'<table>')
|
|
||||||
self.response.out.write(data)
|
|
||||||
self.response.out.write('</table>'
|
|
||||||
'<input type="hidden" name="_id" value="%s">'
|
|
||||||
'<input type="submit">'
|
|
||||||
'</form></body></html>' % id)
|
|
||||||
|
|
||||||
|
class ContactDeletePage(BaseRequestHandler):
|
||||||
class UsersListPage(webapp.RequestHandler):
|
"""Processes delete contact request."""
|
||||||
"""Lists all Users. In addition displays links for editing user info,
|
|
||||||
viewing user's friends and adding new users."""
|
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
users = datastore.User.all()
|
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()
|
||||||
|
|
||||||
|
self.redirect('/')
|
||||||
|
|
||||||
|
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 = {
|
template_values = {
|
||||||
'users': users
|
'avatar': contact.avatar,
|
||||||
|
'contactId': id
|
||||||
}
|
}
|
||||||
|
|
||||||
path = os.path.join(os.path.dirname(__file__), 'templates', 'users.html')
|
path = os.path.join(os.path.dirname(__file__), 'templates', 'edit_avatar.html')
|
||||||
self.response.out.write(template.render(path, template_values))
|
self.response.out.write(template.render(path, template_values))
|
||||||
|
|
||||||
|
|
||||||
class UserCredentialsForm(djangoforms.ModelForm):
|
|
||||||
"""Represents django form for entering user's credentials."""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = datastore.UserCredentials
|
|
||||||
|
|
||||||
|
|
||||||
class UserCredentialsInsertPage(webapp.RequestHandler):
|
|
||||||
"""Inserts user credentials. GET shows a blank form, POST processes it."""
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
self.response.out.write('<html><body>'
|
|
||||||
'<form method="POST" '
|
|
||||||
'action="/add_credentials">'
|
|
||||||
'<table>')
|
|
||||||
# This generates our shopping list form and writes it in the response
|
|
||||||
self.response.out.write(UserCredentialsForm())
|
|
||||||
self.response.out.write('</table>'
|
|
||||||
'<input type="submit">'
|
|
||||||
'</form></body></html>')
|
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
data = UserCredentialsForm(data=self.request.POST)
|
id = int(self.request.get('id'))
|
||||||
if data.is_valid():
|
contact = datastore.Contact.get(db.Key.from_path('Contact', id))
|
||||||
# Save the data, and redirect to the view page
|
#avatar = images.resize(self.request.get("avatar"), 128, 128)
|
||||||
entity = data.save(commit=False)
|
avatar = self.request.get("avatar")
|
||||||
entity.put()
|
contact.avatar = db.Blob(avatar)
|
||||||
self.redirect('/users')
|
contact.updated = datetime.datetime.utcnow()
|
||||||
else:
|
contact.put()
|
||||||
# Reprint the form
|
self.redirect('/')
|
||||||
self.response.out.write('<html><body>'
|
|
||||||
'<form method="POST" '
|
|
||||||
'action="/add_credentials">'
|
|
||||||
'<table>')
|
|
||||||
self.response.out.write(data)
|
|
||||||
self.response.out.write('</table>'
|
|
||||||
'<input type="submit">'
|
|
||||||
'</form></body></html>')
|
|
||||||
|
|
||||||
|
class AvatarViewPage(BaseRequestHandler):
|
||||||
class UserFriendsForm(djangoforms.ModelForm):
|
"""
|
||||||
"""Represents django form for entering user's friends."""
|
Processes request to view contact's avatar. This is different from
|
||||||
|
the GET AvatarEditPage request in that this doesn't return a page -
|
||||||
class Meta:
|
it just returns the raw image itself.
|
||||||
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):
|
def get(self):
|
||||||
user = self.request.get('user')
|
id = int(self.request.get('id'))
|
||||||
self.response.out.write('<html><body>'
|
contact = datastore.Contact.get(db.Key.from_path('Contact', id))
|
||||||
'<form method="POST" '
|
if (contact.avatar):
|
||||||
'action="/add_friend">'
|
self.response.headers['Content-Type'] = "image/png"
|
||||||
'<table>')
|
self.response.out.write(contact.avatar)
|
||||||
# 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:
|
else:
|
||||||
entity.deleted = False
|
self.redirect(self.request.host_url + '/static/img/default_avatar.gif')
|
||||||
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 ContactsListPage(webapp.RequestHandler):
|
||||||
class UserFriendsListPage(webapp.RequestHandler):
|
"""
|
||||||
"""Lists all friends for a user. In addition displays links for removing
|
Display a page that lists all the contacts associated with
|
||||||
friends and adding new friends."""
|
the specifies user account.
|
||||||
|
"""
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
user = self.request.get('user')
|
contacts = datastore.Contact.all()
|
||||||
query = datastore.UserFriends.all()
|
|
||||||
query.filter('deleted = ', False)
|
|
||||||
query.filter('username = ', user)
|
|
||||||
friends = query.fetch(50)
|
|
||||||
template_values = {
|
template_values = {
|
||||||
'friends': friends,
|
'contacts': contacts,
|
||||||
'user': user
|
'username': 'user'
|
||||||
}
|
}
|
||||||
path = os.path.join(os.path.dirname(__file__),
|
|
||||||
'templates', 'view_friends.html')
|
path = os.path.join(os.path.dirname(__file__), 'templates', 'contacts.html')
|
||||||
self.response.out.write(template.render(path, template_values))
|
self.response.out.write(template.render(path, template_values))
|
||||||
|
|
||||||
|
|
||||||
class DeleteFriendPage(webapp.RequestHandler):
|
|
||||||
"""Processes delete friend request."""
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
user = self.request.get('user')
|
|
||||||
friend = self.request.get('friend')
|
|
||||||
query = datastore.UserFriends.all()
|
|
||||||
query.filter('username =', user)
|
|
||||||
query.filter('friend_handle =', friend)
|
|
||||||
result = query.get()
|
|
||||||
result.deleted = True
|
|
||||||
result.updated = datetime.datetime.utcnow()
|
|
||||||
result.put()
|
|
||||||
|
|
||||||
self.redirect('/user_friends?user=' + user)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
application = webapp.WSGIApplication(
|
application = webapp.WSGIApplication(
|
||||||
[('/add_user', UserInsertPage),
|
[('/', ContactsListPage),
|
||||||
('/users', UsersListPage),
|
('/add_contact', ContactInsertPage),
|
||||||
('/add_credentials', UserCredentialsInsertPage),
|
('/edit_contact', ContactEditPage),
|
||||||
('/add_friend', UserFriendsInsertPage),
|
('/delete_contact', ContactDeletePage),
|
||||||
('/user_friends', UserFriendsListPage),
|
('/avatar', AvatarViewPage),
|
||||||
('/delete_friend', DeleteFriendPage),
|
('/edit_avatar', AvatarEditPage)
|
||||||
('/edit_user', UserEditPage)
|
|
||||||
],
|
],
|
||||||
debug=True)
|
debug=True)
|
||||||
wsgiref.handlers.CGIHandler().run(application)
|
wsgiref.handlers.CGIHandler().run(application)
|
||||||
|
|||||||
@@ -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
|
# AUTOGENERATED
|
||||||
# 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
|
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -14,80 +14,51 @@
|
|||||||
# License for the specific language governing permissions and limitations under
|
# License for the specific language governing permissions and limitations under
|
||||||
# the License.
|
# the License.
|
||||||
|
|
||||||
"""Represents user's contact information, friends and credentials."""
|
"""Represents user's contact information"""
|
||||||
|
|
||||||
from google.appengine.ext import db
|
from google.appengine.ext import db
|
||||||
|
|
||||||
|
|
||||||
class User(db.Model):
|
class Contact(db.Model):
|
||||||
"""Data model class to hold user objects."""
|
"""Data model class to hold user objects."""
|
||||||
|
|
||||||
handle = db.StringProperty(required=True)
|
handle = db.StringProperty(required=True)
|
||||||
firstname = db.TextProperty()
|
firstname = db.StringProperty()
|
||||||
lastname = db.TextProperty()
|
lastname = db.StringProperty()
|
||||||
status = db.TextProperty()
|
|
||||||
phone_home = db.PhoneNumberProperty()
|
phone_home = db.PhoneNumberProperty()
|
||||||
phone_office = db.PhoneNumberProperty()
|
phone_office = db.PhoneNumberProperty()
|
||||||
phone_mobile = db.PhoneNumberProperty()
|
phone_mobile = db.PhoneNumberProperty()
|
||||||
email = db.EmailProperty()
|
email = db.EmailProperty()
|
||||||
|
status = db.TextProperty()
|
||||||
|
avatar = db.BlobProperty()
|
||||||
deleted = db.BooleanProperty()
|
deleted = db.BooleanProperty()
|
||||||
updated = db.DateTimeProperty(auto_now_add=True)
|
updated = db.DateTimeProperty(auto_now_add=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_user_info(cls, username):
|
def get_contact_info(cls, username):
|
||||||
if username not in (None, ''):
|
if username not in (None, ''):
|
||||||
query = cls.gql('WHERE handle = :1', username)
|
query = cls.gql('WHERE handle = :1', username)
|
||||||
return query.get()
|
return query.get()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_user_last_updated(cls, username):
|
def get_contact_last_updated(cls, username):
|
||||||
if username not in (None, ''):
|
if username not in (None, ''):
|
||||||
query = cls.gql('WHERE handle = :1', username)
|
query = cls.gql('WHERE handle = :1', username)
|
||||||
return query.get().updated
|
return query.get().updated
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_user_id(cls, username):
|
def get_contact_id(cls, username):
|
||||||
if username not in (None, ''):
|
if username not in (None, ''):
|
||||||
query = cls.gql('WHERE handle = :1', username)
|
query = cls.gql('WHERE handle = :1', username)
|
||||||
return query.get().key().id()
|
return query.get().key().id()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_user_status(cls, username):
|
def get_contact_status(cls, username):
|
||||||
if username not in (None, ''):
|
if username not in (None, ''):
|
||||||
query = cls.gql('WHERE handle = :1', username)
|
query = cls.gql('WHERE handle = :1', username)
|
||||||
return query.get().status
|
return query.get().status
|
||||||
return None
|
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
|
|
||||||
@@ -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 |
@@ -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> </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>
|
||||||
@@ -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> </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>
|
||||||
@@ -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>
|
||||||
@@ -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 }} {{ user.lastname }} </a>
|
|
||||||
</td><td> <a href="/user_friends?user={{ user.handle }}">Friends</a> </td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<a href = "/add_user"> Insert More </a>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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()
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
* License for the specific language governing permissions and limitations under
|
* License for the specific language governing permissions and limitations under
|
||||||
* the License.
|
* the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.example.android.samplesync.authenticator;
|
package com.example.android.samplesync.authenticator;
|
||||||
|
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
|
|||||||
@@ -13,26 +13,45 @@
|
|||||||
* License for the specific language governing permissions and limitations under
|
* License for the specific language governing permissions and limitations under
|
||||||
* the License.
|
* the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.example.android.samplesync.authenticator;
|
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.AbstractAccountAuthenticator;
|
||||||
import android.accounts.Account;
|
import android.accounts.Account;
|
||||||
import android.accounts.AccountAuthenticatorResponse;
|
import android.accounts.AccountAuthenticatorResponse;
|
||||||
import android.accounts.AccountManager;
|
import android.accounts.AccountManager;
|
||||||
|
import android.accounts.NetworkErrorException;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.text.TextUtils;
|
||||||
import com.example.android.samplesync.Constants;
|
import android.util.Log;
|
||||||
import com.example.android.samplesync.R;
|
|
||||||
import com.example.android.samplesync.client.NetworkUtilities;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is an implementation of AbstractAccountAuthenticator for
|
* 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 {
|
class Authenticator extends AbstractAccountAuthenticator {
|
||||||
|
|
||||||
|
/** The tag used to log to adb console. **/
|
||||||
|
private static final String TAG = "Authenticator";
|
||||||
|
|
||||||
// Authentication Service context
|
// Authentication Service context
|
||||||
private final Context mContext;
|
private final Context mContext;
|
||||||
|
|
||||||
@@ -44,9 +63,8 @@ class Authenticator extends AbstractAccountAuthenticator {
|
|||||||
@Override
|
@Override
|
||||||
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType,
|
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);
|
final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
|
||||||
intent.putExtra(AuthenticatorActivity.PARAM_AUTHTOKEN_TYPE, authTokenType);
|
|
||||||
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
|
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
|
||||||
final Bundle bundle = new Bundle();
|
final Bundle bundle = new Bundle();
|
||||||
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
|
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
|
||||||
@@ -54,54 +72,49 @@ class Authenticator extends AbstractAccountAuthenticator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account,
|
public Bundle confirmCredentials(
|
||||||
Bundle options) {
|
AccountAuthenticatorResponse response, Account account, Bundle options) {
|
||||||
|
Log.v(TAG, "confirmCredentials()");
|
||||||
if (options != null && options.containsKey(AccountManager.KEY_PASSWORD)) {
|
return null;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
|
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
|
||||||
|
Log.v(TAG, "editProperties()");
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
|
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)) {
|
if (!authTokenType.equals(Constants.AUTHTOKEN_TYPE)) {
|
||||||
final Bundle result = new Bundle();
|
final Bundle result = new Bundle();
|
||||||
result.putString(AccountManager.KEY_ERROR_MESSAGE, "invalid authTokenType");
|
result.putString(AccountManager.KEY_ERROR_MESSAGE, "invalid authTokenType");
|
||||||
return result;
|
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 AccountManager am = AccountManager.get(mContext);
|
||||||
final String password = am.getPassword(account);
|
final String password = am.getPassword(account);
|
||||||
if (password != null) {
|
if (password != null) {
|
||||||
final boolean verified = onlineConfirmPassword(account.name, password);
|
final String authToken = NetworkUtilities.authenticate(account.name, password);
|
||||||
if (verified) {
|
if (!TextUtils.isEmpty(authToken)) {
|
||||||
final Bundle result = new Bundle();
|
final Bundle result = new Bundle();
|
||||||
result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
|
result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
|
||||||
result.putString(AccountManager.KEY_ACCOUNT_TYPE, Constants.ACCOUNT_TYPE);
|
result.putString(AccountManager.KEY_ACCOUNT_TYPE, Constants.ACCOUNT_TYPE);
|
||||||
result.putString(AccountManager.KEY_AUTHTOKEN, password);
|
result.putString(AccountManager.KEY_AUTHTOKEN, authToken);
|
||||||
return result;
|
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);
|
final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
|
||||||
intent.putExtra(AuthenticatorActivity.PARAM_USERNAME, account.name);
|
intent.putExtra(AuthenticatorActivity.PARAM_USERNAME, account.name);
|
||||||
intent.putExtra(AuthenticatorActivity.PARAM_AUTHTOKEN_TYPE, authTokenType);
|
intent.putExtra(AuthenticatorActivity.PARAM_AUTHTOKEN_TYPE, authTokenType);
|
||||||
@@ -113,39 +126,27 @@ class Authenticator extends AbstractAccountAuthenticator {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getAuthTokenLabel(String authTokenType) {
|
public String getAuthTokenLabel(String authTokenType) {
|
||||||
if (Constants.AUTHTOKEN_TYPE.equals(authTokenType)) {
|
// null means we don't support multiple authToken types
|
||||||
return mContext.getString(R.string.label);
|
Log.v(TAG, "getAuthTokenLabel()");
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account,
|
public Bundle hasFeatures(
|
||||||
String[] features) {
|
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();
|
final Bundle result = new Bundle();
|
||||||
result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
|
result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
|
||||||
return result;
|
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
|
@Override
|
||||||
public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account,
|
public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account,
|
||||||
String authTokenType, Bundle loginOptions) {
|
String authTokenType, Bundle loginOptions) {
|
||||||
|
Log.v(TAG, "updateCredentials()");
|
||||||
final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
|
return null;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,13 @@
|
|||||||
* License for the specific language governing permissions and limitations under
|
* License for the specific language governing permissions and limitations under
|
||||||
* the License.
|
* the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.example.android.samplesync.authenticator;
|
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.Account;
|
||||||
import android.accounts.AccountAuthenticatorActivity;
|
import android.accounts.AccountAuthenticatorActivity;
|
||||||
import android.accounts.AccountManager;
|
import android.accounts.AccountManager;
|
||||||
@@ -23,6 +28,7 @@ import android.app.ProgressDialog;
|
|||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.provider.ContactsContract;
|
import android.provider.ContactsContract;
|
||||||
@@ -33,41 +39,36 @@ import android.view.Window;
|
|||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.TextView;
|
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.
|
* Activity which displays login screen to the user.
|
||||||
*/
|
*/
|
||||||
public class AuthenticatorActivity extends AccountAuthenticatorActivity {
|
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";
|
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";
|
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";
|
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";
|
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 static final String TAG = "AuthenticatorActivity";
|
||||||
|
|
||||||
private AccountManager mAccountManager;
|
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;
|
/** Keep track of the progress dialog so we can dismiss it */
|
||||||
|
private ProgressDialog mProgressDialog = null;
|
||||||
private String mAuthtokenType;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If set we are just checking that the user knows their credentials; this
|
* 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;
|
private Boolean mConfirmCredentials = false;
|
||||||
|
|
||||||
@@ -99,14 +100,13 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity {
|
|||||||
Log.i(TAG, "loading data from Intent");
|
Log.i(TAG, "loading data from Intent");
|
||||||
final Intent intent = getIntent();
|
final Intent intent = getIntent();
|
||||||
mUsername = intent.getStringExtra(PARAM_USERNAME);
|
mUsername = intent.getStringExtra(PARAM_USERNAME);
|
||||||
mAuthtokenType = intent.getStringExtra(PARAM_AUTHTOKEN_TYPE);
|
|
||||||
mRequestNewAccount = mUsername == null;
|
mRequestNewAccount = mUsername == null;
|
||||||
mConfirmCredentials = intent.getBooleanExtra(PARAM_CONFIRM_CREDENTIALS, false);
|
mConfirmCredentials = intent.getBooleanExtra(PARAM_CONFIRM_CREDENTIALS, false);
|
||||||
Log.i(TAG, " request new: " + mRequestNewAccount);
|
Log.i(TAG, " request new: " + mRequestNewAccount);
|
||||||
requestWindowFeature(Window.FEATURE_LEFT_ICON);
|
requestWindowFeature(Window.FEATURE_LEFT_ICON);
|
||||||
setContentView(R.layout.login_activity);
|
setContentView(R.layout.login_activity);
|
||||||
getWindow().setFeatureDrawableResource(Window.FEATURE_LEFT_ICON,
|
getWindow().setFeatureDrawableResource(
|
||||||
android.R.drawable.ic_dialog_alert);
|
Window.FEATURE_LEFT_ICON, android.R.drawable.ic_dialog_alert);
|
||||||
mMessage = (TextView) findViewById(R.id.message);
|
mMessage = (TextView) findViewById(R.id.message);
|
||||||
mUsernameEdit = (EditText) findViewById(R.id.username_edit);
|
mUsernameEdit = (EditText) findViewById(R.id.username_edit);
|
||||||
mPasswordEdit = (EditText) findViewById(R.id.password_edit);
|
mPasswordEdit = (EditText) findViewById(R.id.password_edit);
|
||||||
@@ -118,26 +118,30 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity {
|
|||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected Dialog onCreateDialog(int id) {
|
protected Dialog onCreateDialog(int id, Bundle args) {
|
||||||
final ProgressDialog dialog = new ProgressDialog(this);
|
final ProgressDialog dialog = new ProgressDialog(this);
|
||||||
dialog.setMessage(getText(R.string.ui_activity_authenticating));
|
dialog.setMessage(getText(R.string.ui_activity_authenticating));
|
||||||
dialog.setIndeterminate(true);
|
dialog.setIndeterminate(true);
|
||||||
dialog.setCancelable(true);
|
dialog.setCancelable(true);
|
||||||
dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||||
public void onCancel(DialogInterface dialog) {
|
public void onCancel(DialogInterface dialog) {
|
||||||
Log.i(TAG, "dialog cancel has been invoked");
|
Log.i(TAG, "user cancelling authentication");
|
||||||
if (mAuthThread != null) {
|
if (mAuthTask != null) {
|
||||||
mAuthThread.interrupt();
|
mAuthTask.cancel(true);
|
||||||
finish();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// 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;
|
return dialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles onClick event on the Submit button. Sends username/password to
|
* 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
|
* @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)) {
|
if (TextUtils.isEmpty(mUsername) || TextUtils.isEmpty(mPassword)) {
|
||||||
mMessage.setText(getMessage());
|
mMessage.setText(getMessage());
|
||||||
} else {
|
} else {
|
||||||
|
// Show a progress dialog, and kick off a background task to perform
|
||||||
|
// the user login attempt.
|
||||||
showProgress();
|
showProgress();
|
||||||
// Start authenticating...
|
mAuthTask = new UserLoginTask();
|
||||||
mAuthThread =
|
mAuthTask.execute();
|
||||||
NetworkUtilities.attemptAuth(mUsername, mPassword, mHandler,
|
|
||||||
AuthenticatorActivity.this);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +166,7 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity {
|
|||||||
* request. See onAuthenticationResult(). Sets the
|
* request. See onAuthenticationResult(). Sets the
|
||||||
* AccountAuthenticatorResult which is sent back to the caller.
|
* AccountAuthenticatorResult which is sent back to the caller.
|
||||||
*
|
*
|
||||||
* @param the confirmCredentials result.
|
* @param result the confirmCredentials result.
|
||||||
*/
|
*/
|
||||||
private void finishConfirmCredentials(boolean result) {
|
private void finishConfirmCredentials(boolean result) {
|
||||||
Log.i(TAG, "finishConfirmCredentials()");
|
Log.i(TAG, "finishConfirmCredentials()");
|
||||||
@@ -178,12 +182,13 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity {
|
|||||||
/**
|
/**
|
||||||
* Called when response is received from the server for authentication
|
* Called when response is received from the server for authentication
|
||||||
* request. See onAuthenticationResult(). Sets the
|
* request. See onAuthenticationResult(). Sets the
|
||||||
* AccountAuthenticatorResult which is sent back to the caller. Also sets
|
* AccountAuthenticatorResult which is sent back to the caller. We store the
|
||||||
* the authToken in AccountManager for this account.
|
* 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()");
|
Log.i(TAG, "finishLogin()");
|
||||||
final Account account = new Account(mUsername, Constants.ACCOUNT_TYPE);
|
final Account account = new Account(mUsername, Constants.ACCOUNT_TYPE);
|
||||||
@@ -195,37 +200,35 @@ public class AuthenticatorActivity extends AccountAuthenticatorActivity {
|
|||||||
mAccountManager.setPassword(account, mPassword);
|
mAccountManager.setPassword(account, mPassword);
|
||||||
}
|
}
|
||||||
final Intent intent = new Intent();
|
final Intent intent = new Intent();
|
||||||
mAuthtoken = mPassword;
|
|
||||||
intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, mUsername);
|
intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, mUsername);
|
||||||
intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, Constants.ACCOUNT_TYPE);
|
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());
|
setAccountAuthenticatorResult(intent.getExtras());
|
||||||
setResult(RESULT_OK, intent);
|
setResult(RESULT_OK, intent);
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Hides the progress UI for a lengthy operation.
|
|
||||||
*/
|
|
||||||
private void hideProgress() {
|
|
||||||
dismissDialog(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the authentication process completes (see attemptLogin()).
|
* 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
|
// Hide the progress dialog
|
||||||
hideProgress();
|
hideProgress();
|
||||||
if (result) {
|
|
||||||
|
if (success) {
|
||||||
if (!mConfirmCredentials) {
|
if (!mConfirmCredentials) {
|
||||||
finishLogin();
|
finishLogin(authToken);
|
||||||
} else {
|
} else {
|
||||||
finishConfirmCredentials(true);
|
finishConfirmCredentials(success);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.e(TAG, "onAuthenticationResult: failed to authenticate");
|
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.
|
* 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() {
|
private void showProgress() {
|
||||||
showDialog(0);
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,15 @@
|
|||||||
* License for the specific language governing permissions and limitations under
|
* License for the specific language governing permissions and limitations under
|
||||||
* the License.
|
* the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.example.android.samplesync.client;
|
package com.example.android.samplesync.client;
|
||||||
|
|
||||||
import android.accounts.Account;
|
import android.accounts.Account;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.example.android.samplesync.authenticator.AuthenticatorActivity;
|
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.HttpConnectionParams;
|
||||||
import org.apache.http.params.HttpParams;
|
import org.apache.http.params.HttpParams;
|
||||||
import org.apache.http.util.EntityUtils;
|
import org.apache.http.util.EntityUtils;
|
||||||
|
import org.json.JSONObject;
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@@ -52,31 +64,26 @@ import java.util.TimeZone;
|
|||||||
* Provides utility methods for communicating with the server.
|
* Provides utility methods for communicating with the server.
|
||||||
*/
|
*/
|
||||||
final public class NetworkUtilities {
|
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";
|
private static final String TAG = "NetworkUtilities";
|
||||||
|
/** POST parameter name for the user's account name */
|
||||||
/** The Intent extra to store password. **/
|
|
||||||
public static final String PARAM_PASSWORD = "password";
|
|
||||||
|
|
||||||
/** The Intent extra to store username. **/
|
|
||||||
public static final String PARAM_USERNAME = "username";
|
public static final String PARAM_USERNAME = "username";
|
||||||
|
/** POST parameter name for the user's password */
|
||||||
public static final String PARAM_UPDATED = "timestamp";
|
public static final String PARAM_PASSWORD = "password";
|
||||||
|
/** POST parameter name for the user's authentication token */
|
||||||
public static final String USER_AGENT = "AuthenticationService/1.0";
|
public static final String PARAM_AUTH_TOKEN = "authtoken";
|
||||||
|
/** POST parameter name for the client's last-known sync state */
|
||||||
public static final int REGISTRATION_TIMEOUT_MS = 30 * 1000; // ms
|
public static final String PARAM_SYNC_STATE = "syncstate";
|
||||||
|
/** POST parameter name for the sending client-edited contact info */
|
||||||
public static final String BASE_URL = "https://samplesyncadapter.appspot.com";
|
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 AUTH_URI = BASE_URL + "/auth";
|
||||||
|
/** URI for sync service */
|
||||||
public static final String FETCH_FRIEND_UPDATES_URI = BASE_URL + "/fetch_friend_updates";
|
public static final String SYNC_CONTACTS_URI = BASE_URL + "/sync";
|
||||||
|
|
||||||
public static final String FETCH_STATUS_URI = BASE_URL + "/fetch_status";
|
|
||||||
|
|
||||||
private static HttpClient mHttpClient;
|
|
||||||
|
|
||||||
private NetworkUtilities() {
|
private NetworkUtilities() {
|
||||||
}
|
}
|
||||||
@@ -84,224 +91,188 @@ final public class NetworkUtilities {
|
|||||||
/**
|
/**
|
||||||
* Configures the httpClient to connect to the URL provided.
|
* Configures the httpClient to connect to the URL provided.
|
||||||
*/
|
*/
|
||||||
public static void maybeCreateHttpClient() {
|
public static HttpClient getHttpClient() {
|
||||||
if (mHttpClient == null) {
|
HttpClient httpClient = new DefaultHttpClient();
|
||||||
mHttpClient = new DefaultHttpClient();
|
final HttpParams params = httpClient.getParams();
|
||||||
final HttpParams params = mHttpClient.getParams();
|
HttpConnectionParams.setConnectionTimeout(params, HTTP_REQUEST_TIMEOUT_MS);
|
||||||
HttpConnectionParams.setConnectionTimeout(params, REGISTRATION_TIMEOUT_MS);
|
HttpConnectionParams.setSoTimeout(params, HTTP_REQUEST_TIMEOUT_MS);
|
||||||
HttpConnectionParams.setSoTimeout(params, REGISTRATION_TIMEOUT_MS);
|
ConnManagerParams.setTimeout(params, HTTP_REQUEST_TIMEOUT_MS);
|
||||||
ConnManagerParams.setTimeout(params, REGISTRATION_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
|
* @param username The server account username
|
||||||
* be executed.
|
* @param password The server account password
|
||||||
|
* @return String The authentication token returned by the server (or null)
|
||||||
*/
|
*/
|
||||||
public static Thread performOnBackgroundThread(final Runnable runnable) {
|
public static String authenticate(String username, String password) {
|
||||||
final Thread t = new Thread() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
try {
|
|
||||||
runnable.run();
|
|
||||||
} finally {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
t.start();
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connects to the Voiper server, authenticates the provided username and
|
|
||||||
* password.
|
|
||||||
*
|
|
||||||
* @param username The user's username
|
|
||||||
* @param password The user's password
|
|
||||||
* @param handler The hander instance from the calling UI thread.
|
|
||||||
* @param context The context of the calling Activity.
|
|
||||||
* @return boolean The boolean result indicating whether the user was
|
|
||||||
* successfully authenticated.
|
|
||||||
*/
|
|
||||||
public static boolean authenticate(String username, String password, Handler handler,
|
|
||||||
final Context context) {
|
|
||||||
|
|
||||||
final HttpResponse resp;
|
final HttpResponse resp;
|
||||||
final ArrayList<NameValuePair> params = new ArrayList<NameValuePair>();
|
final ArrayList<NameValuePair> params = new ArrayList<NameValuePair>();
|
||||||
params.add(new BasicNameValuePair(PARAM_USERNAME, username));
|
params.add(new BasicNameValuePair(PARAM_USERNAME, username));
|
||||||
params.add(new BasicNameValuePair(PARAM_PASSWORD, password));
|
params.add(new BasicNameValuePair(PARAM_PASSWORD, password));
|
||||||
HttpEntity entity = null;
|
final HttpEntity entity;
|
||||||
try {
|
try {
|
||||||
entity = new UrlEncodedFormEntity(params);
|
entity = new UrlEncodedFormEntity(params);
|
||||||
} catch (final UnsupportedEncodingException e) {
|
} catch (final UnsupportedEncodingException e) {
|
||||||
// this should never happen.
|
// 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);
|
final HttpPost post = new HttpPost(AUTH_URI);
|
||||||
post.addHeader(entity.getContentType());
|
post.addHeader(entity.getContentType());
|
||||||
post.setEntity(entity);
|
post.setEntity(entity);
|
||||||
maybeCreateHttpClient();
|
|
||||||
try {
|
try {
|
||||||
resp = mHttpClient.execute(post);
|
resp = getHttpClient().execute(post);
|
||||||
|
String authToken = null;
|
||||||
if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
|
if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
|
||||||
if (Log.isLoggable(TAG, Log.VERBOSE)) {
|
InputStream istream = (resp.getEntity() != null) ? resp.getEntity().getContent()
|
||||||
|
: null;
|
||||||
|
if (istream != null) {
|
||||||
|
BufferedReader ireader = new BufferedReader(new InputStreamReader(istream));
|
||||||
|
authToken = ireader.readLine().trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((authToken != null) && (authToken.length() > 0)) {
|
||||||
Log.v(TAG, "Successful authentication");
|
Log.v(TAG, "Successful authentication");
|
||||||
}
|
return authToken;
|
||||||
sendResult(true, handler, context);
|
|
||||||
return true;
|
|
||||||
} else {
|
} else {
|
||||||
if (Log.isLoggable(TAG, Log.VERBOSE)) {
|
Log.e(TAG, "Error authenticating" + resp.getStatusLine());
|
||||||
Log.v(TAG, "Error authenticating" + resp.getStatusLine());
|
return null;
|
||||||
}
|
|
||||||
sendResult(false, handler, context);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
if (Log.isLoggable(TAG, Log.VERBOSE)) {
|
Log.e(TAG, "IOException when getting authtoken", e);
|
||||||
Log.v(TAG, "IOException when getting authtoken", e);
|
return null;
|
||||||
}
|
|
||||||
sendResult(false, handler, context);
|
|
||||||
return false;
|
|
||||||
} finally {
|
} 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
|
* Perform 2-way sync with the server-side contacts. We send a request that
|
||||||
* thread through its handler.
|
* 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 account The account being synced
|
||||||
* @param handler The main UI thread's handler instance.
|
* @param authtoken The authtoken stored in the AccountManager for this
|
||||||
* @param context The caller Activity's context.
|
* 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,
|
public static List<RawContact> syncContacts(
|
||||||
final Context context) {
|
Account account, String authtoken, long serverSyncState, List<RawContact> dirtyContacts)
|
||||||
if (handler == null || context == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handler.post(new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
((AuthenticatorActivity) context).onAuthenticationResult(result);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to authenticate the user credentials on the server.
|
|
||||||
*
|
|
||||||
* @param username The user's username
|
|
||||||
* @param password The user's password to be authenticated
|
|
||||||
* @param handler The main UI thread's handler instance.
|
|
||||||
* @param context The caller Activity's context
|
|
||||||
* @return Thread The thread on which the network mOperations are executed.
|
|
||||||
*/
|
|
||||||
public static Thread attemptAuth(final String username, final String password,
|
|
||||||
final Handler handler, final Context context) {
|
|
||||||
|
|
||||||
final Runnable runnable = new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
authenticate(username, password, handler, context);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// run on background thread.
|
|
||||||
return NetworkUtilities.performOnBackgroundThread(runnable);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches the list of friend data updates from the server
|
|
||||||
*
|
|
||||||
* @param account The account being synced.
|
|
||||||
* @param authtoken The authtoken stored in AccountManager for this account
|
|
||||||
* @param lastUpdated The last time that sync was performed
|
|
||||||
* @return list The list of updates received from the server.
|
|
||||||
*/
|
|
||||||
public static List<User> fetchFriendUpdates(Account account, String authtoken, Date lastUpdated)
|
|
||||||
throws JSONException, ParseException, IOException, AuthenticationException {
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
final ArrayList<User> friendList = new ArrayList<User>();
|
// Create a special JSONArray of our JSON contacts
|
||||||
|
JSONArray buffer = new JSONArray(jsonContacts);
|
||||||
|
|
||||||
|
// 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>();
|
||||||
|
|
||||||
|
// Prepare our POST data
|
||||||
final ArrayList<NameValuePair> params = new ArrayList<NameValuePair>();
|
final ArrayList<NameValuePair> params = new ArrayList<NameValuePair>();
|
||||||
params.add(new BasicNameValuePair(PARAM_USERNAME, account.name));
|
params.add(new BasicNameValuePair(PARAM_USERNAME, account.name));
|
||||||
params.add(new BasicNameValuePair(PARAM_PASSWORD, authtoken));
|
params.add(new BasicNameValuePair(PARAM_AUTH_TOKEN, authtoken));
|
||||||
if (lastUpdated != null) {
|
params.add(new BasicNameValuePair(PARAM_CONTACTS_DATA, buffer.toString()));
|
||||||
final SimpleDateFormat formatter = new SimpleDateFormat("yyyy/MM/dd HH:mm");
|
if (serverSyncState > 0) {
|
||||||
formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
|
params.add(new BasicNameValuePair(PARAM_SYNC_STATE, Long.toString(serverSyncState)));
|
||||||
params.add(new BasicNameValuePair(PARAM_UPDATED, formatter.format(lastUpdated)));
|
|
||||||
}
|
}
|
||||||
Log.i(TAG, params.toString());
|
Log.i(TAG, params.toString());
|
||||||
HttpEntity entity = null;
|
HttpEntity entity = new UrlEncodedFormEntity(params);
|
||||||
entity = new UrlEncodedFormEntity(params);
|
|
||||||
final HttpPost post = new HttpPost(FETCH_FRIEND_UPDATES_URI);
|
// 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.addHeader(entity.getContentType());
|
||||||
post.setEntity(entity);
|
post.setEntity(entity);
|
||||||
maybeCreateHttpClient();
|
final HttpResponse resp = getHttpClient().execute(post);
|
||||||
final HttpResponse resp = mHttpClient.execute(post);
|
|
||||||
final String response = EntityUtils.toString(resp.getEntity());
|
final String response = EntityUtils.toString(resp.getEntity());
|
||||||
if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
|
if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
|
||||||
// Succesfully connected to the samplesyncadapter server and
|
// Our request to the server was successful - so we assume
|
||||||
// authenticated.
|
// that they accepted all the changes we sent up, and
|
||||||
// Extract friends data in json format.
|
// that the response includes the contacts that we need
|
||||||
final JSONArray friends = new JSONArray(response);
|
// to update on our side...
|
||||||
|
final JSONArray serverContacts = new JSONArray(response);
|
||||||
Log.d(TAG, response);
|
Log.d(TAG, response);
|
||||||
for (int i = 0; i < friends.length(); i++) {
|
for (int i = 0; i < serverContacts.length(); i++) {
|
||||||
friendList.add(User.valueOf(friends.getJSONObject(i)));
|
RawContact rawContact = RawContact.valueOf(serverContacts.getJSONObject(i));
|
||||||
|
if (rawContact != null) {
|
||||||
|
serverDirtyList.add(rawContact);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
|
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();
|
throw new AuthenticationException();
|
||||||
} else {
|
} 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();
|
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 avatarUrl the URL pointing to the avatar image
|
||||||
* @param authtoken The authtoken stored in the AccountManager for the
|
* @return a byte array with the raw JPEG avatar image
|
||||||
* account
|
|
||||||
* @return list The list of status messages received from the server.
|
|
||||||
*/
|
*/
|
||||||
public static List<User.Status> fetchFriendStatuses(Account account, String authtoken)
|
public static byte[] downloadAvatar(final String avatarUrl) {
|
||||||
throws JSONException, ParseException, IOException, AuthenticationException {
|
// If there is no avatar, we're done
|
||||||
|
if (TextUtils.isEmpty(avatarUrl)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
final ArrayList<User.Status> statusList = new ArrayList<User.Status>();
|
try {
|
||||||
final ArrayList<NameValuePair> params = new ArrayList<NameValuePair>();
|
Log.i(TAG, "Downloading avatar: " + avatarUrl);
|
||||||
params.add(new BasicNameValuePair(PARAM_USERNAME, account.name));
|
// Request the avatar image from the server, and create a bitmap
|
||||||
params.add(new BasicNameValuePair(PARAM_PASSWORD, authtoken));
|
// object from the stream we get back.
|
||||||
HttpEntity entity = null;
|
URL url = new URL(avatarUrl);
|
||||||
entity = new UrlEncodedFormEntity(params);
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
final HttpPost post = new HttpPost(FETCH_STATUS_URI);
|
connection.connect();
|
||||||
post.addHeader(entity.getContentType());
|
try {
|
||||||
post.setEntity(entity);
|
final BitmapFactory.Options options = new BitmapFactory.Options();
|
||||||
maybeCreateHttpClient();
|
final Bitmap avatar = BitmapFactory.decodeStream(connection.getInputStream(),
|
||||||
final HttpResponse resp = mHttpClient.execute(post);
|
null, options);
|
||||||
final String response = EntityUtils.toString(resp.getEntity());
|
|
||||||
if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
|
// Take the image we received from the server, whatever format it
|
||||||
// Succesfully connected to the samplesyncadapter server and
|
// happens to be in, and convert it to a JPEG image. Note: we're
|
||||||
// authenticated.
|
// not resizing the avatar - we assume that the image we get from
|
||||||
// Extract friends data in json format.
|
// the server is a reasonable size...
|
||||||
final JSONArray statuses = new JSONArray(response);
|
Log.i(TAG, "Converting avatar to JPEG");
|
||||||
for (int i = 0; i < statuses.length(); i++) {
|
ByteArrayOutputStream convertStream = new ByteArrayOutputStream(
|
||||||
statusList.add(User.Status.valueOf(statuses.getJSONObject(i)));
|
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();
|
||||||
}
|
}
|
||||||
} else {
|
} catch (MalformedURLException muex) {
|
||||||
if (resp.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
|
// A bad URL - nothing we can really do about it here...
|
||||||
Log.e(TAG, "Authentication exception in fetching friend status list");
|
Log.e(TAG, "Malformed avatar URL: " + avatarUrl);
|
||||||
throw new AuthenticationException();
|
} catch (IOException ioex) {
|
||||||
} else {
|
// If we're unable to download the avatar, it's a bummer but not the
|
||||||
Log.e(TAG, "Server error in fetching friend status list");
|
// end of the world. We'll try to get it next time we sync.
|
||||||
throw new IOException();
|
Log.e(TAG, "Failed to download user avatar: " + avatarUrl);
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return statusList;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,9 +16,11 @@
|
|||||||
package com.example.android.samplesync.platform;
|
package com.example.android.samplesync.platform;
|
||||||
|
|
||||||
import android.content.ContentProviderOperation;
|
import android.content.ContentProviderOperation;
|
||||||
|
import android.content.ContentProviderResult;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.OperationApplicationException;
|
import android.content.OperationApplicationException;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.provider.ContactsContract;
|
import android.provider.ContactsContract;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@@ -50,19 +52,24 @@ final public class BatchOperation {
|
|||||||
mOperations.add(cpo);
|
mOperations.add(cpo);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void execute() {
|
public Uri execute() {
|
||||||
|
Uri result = null;
|
||||||
|
|
||||||
if (mOperations.size() == 0) {
|
if (mOperations.size() == 0) {
|
||||||
return;
|
return result;
|
||||||
}
|
}
|
||||||
// Apply the mOperations to the content provider
|
// Apply the mOperations to the content provider
|
||||||
try {
|
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) {
|
} catch (final OperationApplicationException e1) {
|
||||||
Log.e(TAG, "storing contact data failed", e1);
|
Log.e(TAG, "storing contact data failed", e1);
|
||||||
} catch (final RemoteException e2) {
|
} catch (final RemoteException e2) {
|
||||||
Log.e(TAG, "storing contact data failed", e2);
|
Log.e(TAG, "storing contact data failed", e2);
|
||||||
}
|
}
|
||||||
mOperations.clear();
|
mOperations.clear();
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,25 +15,30 @@
|
|||||||
*/
|
*/
|
||||||
package com.example.android.samplesync.platform;
|
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.ContentResolver;
|
||||||
import android.content.ContentUris;
|
import android.content.ContentUris;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.provider.ContactsContract.Data;
|
import android.provider.ContactsContract;
|
||||||
import android.provider.ContactsContract.RawContacts;
|
|
||||||
import android.provider.ContactsContract.StatusUpdates;
|
|
||||||
import android.provider.ContactsContract.CommonDataKinds.Email;
|
import android.provider.ContactsContract.CommonDataKinds.Email;
|
||||||
import android.provider.ContactsContract.CommonDataKinds.Im;
|
import android.provider.ContactsContract.CommonDataKinds.Im;
|
||||||
import android.provider.ContactsContract.CommonDataKinds.Phone;
|
import android.provider.ContactsContract.CommonDataKinds.Phone;
|
||||||
|
import android.provider.ContactsContract.CommonDataKinds.Photo;
|
||||||
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
|
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 android.util.Log;
|
||||||
|
|
||||||
import com.example.android.samplesync.Constants;
|
import java.util.ArrayList;
|
||||||
import com.example.android.samplesync.R;
|
|
||||||
import com.example.android.samplesync.client.User;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,81 +54,172 @@ public class ContactManager {
|
|||||||
private static final String TAG = "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 context The context of Authenticator Activity
|
||||||
* @param account The username for the account
|
* @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 currentSyncMarker = lastSyncMarker;
|
||||||
long rawContactId = 0;
|
|
||||||
final ContentResolver resolver = context.getContentResolver();
|
final ContentResolver resolver = context.getContentResolver();
|
||||||
final BatchOperation batchOperation = new BatchOperation(context, resolver);
|
final BatchOperation batchOperation = new BatchOperation(context, resolver);
|
||||||
|
final List<RawContact> newUsers = new ArrayList<RawContact>();
|
||||||
|
|
||||||
Log.d(TAG, "In SyncContacts");
|
Log.d(TAG, "In SyncContacts");
|
||||||
for (final User user : users) {
|
for (final RawContact rawContact : rawContacts) {
|
||||||
userId = user.getUserId();
|
// The server returns a syncState (x) value with each contact record.
|
||||||
// Check to see if the contact needs to be inserted or updated
|
// The syncState is sequential, so higher values represent more recent
|
||||||
rawContactId = lookupRawContact(resolver, userId);
|
// changes than lower values. We keep track of the highest value we
|
||||||
if (rawContactId != 0) {
|
// see, and consider that a "high water mark" for the changes we've
|
||||||
if (!user.isDeleted()) {
|
// received from the server. That way, on our next sync, we can just
|
||||||
// update contact
|
// ask for changes that have occurred since that most-recent change.
|
||||||
updateContact(context, resolver, account, user, rawContactId, batchOperation);
|
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 (!rawContact.isDeleted()) {
|
||||||
|
updateContact(context, resolver, rawContact, updateServerId,
|
||||||
|
true, true, true, rawContactId, batchOperation);
|
||||||
} else {
|
} else {
|
||||||
// delete contact
|
|
||||||
deleteContact(context, rawContactId, batchOperation);
|
deleteContact(context, rawContactId, batchOperation);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// add new contact
|
|
||||||
Log.d(TAG, "In addContact");
|
Log.d(TAG, "In addContact");
|
||||||
if (!user.isDeleted()) {
|
if (!rawContact.isDeleted()) {
|
||||||
addContact(context, account, user, batchOperation);
|
newUsers.add(rawContact);
|
||||||
|
addContact(context, account, rawContact, true, batchOperation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// A sync adapter should batch operations on multiple contacts,
|
// A sync adapter should batch operations on multiple contacts,
|
||||||
// because it will make a dramatic performance difference.
|
// because it will make a dramatic performance difference.
|
||||||
|
// (UI updates, etc)
|
||||||
if (batchOperation.size() >= 50) {
|
if (batchOperation.size() >= 50) {
|
||||||
batchOperation.execute();
|
batchOperation.execute();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
batchOperation.execute();
|
batchOperation.execute();
|
||||||
|
|
||||||
|
return currentSyncMarker;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a list of the local contacts that have been marked as
|
||||||
|
* "dirty", and need syncing to the SampleSync server.
|
||||||
|
*
|
||||||
|
* @param context The context of Authenticator Activity
|
||||||
|
* @param account The account that we're interested in syncing
|
||||||
|
* @return a list of Users that are considered "dirty"
|
||||||
|
*/
|
||||||
|
public static List<RawContact> getDirtyContacts(Context context, Account account) {
|
||||||
|
Log.i(TAG, "*** Looking for local dirty contacts");
|
||||||
|
List<RawContact> dirtyContacts = new ArrayList<RawContact>();
|
||||||
|
|
||||||
|
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 (RawContact rawContact : rawContacts) {
|
||||||
|
updateContactStatus(context, rawContact, batchOperation);
|
||||||
|
}
|
||||||
|
batchOperation.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a list of status messages to the contacts provider.
|
* 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 to use
|
* @param context The context of Authenticator Activity
|
||||||
* @param accountName the username of the logged in user
|
* @param dirtyContacts The list of contacts that we're cleaning up
|
||||||
* @param statuses the list of statuses to store
|
|
||||||
*/
|
*/
|
||||||
public static void insertStatuses(Context context, String username, List<User.Status> list) {
|
public static void clearSyncFlags(Context context, List<RawContact> dirtyContacts) {
|
||||||
|
Log.i(TAG, "*** Clearing Sync-related Flags");
|
||||||
final ContentValues values = new ContentValues();
|
|
||||||
final ContentResolver resolver = context.getContentResolver();
|
final ContentResolver resolver = context.getContentResolver();
|
||||||
final BatchOperation batchOperation = new BatchOperation(context, resolver);
|
final BatchOperation batchOperation = new BatchOperation(context, resolver);
|
||||||
for (final User.Status status : list) {
|
for (RawContact rawContact : dirtyContacts) {
|
||||||
// Look up the user's sample SyncAdapter data row
|
if (rawContact.isDeleted()) {
|
||||||
final long userId = status.getUserId();
|
Log.i(TAG, "Deleting contact: " + Long.toString(rawContact.getRawContactId()));
|
||||||
final long profileId = lookupProfile(resolver, userId);
|
deleteContact(context, rawContact.getRawContactId(), batchOperation);
|
||||||
// Insert the activity into the stream
|
} else if (rawContact.isDirty()) {
|
||||||
if (profileId > 0) {
|
Log.i(TAG, "Clearing dirty flag for: " + rawContact.getBestName());
|
||||||
values.put(StatusUpdates.DATA_ID, profileId);
|
clearDirtyFlag(context, rawContact.getRawContactId(), batchOperation);
|
||||||
values.put(StatusUpdates.STATUS, status.getStatus());
|
|
||||||
values.put(StatusUpdates.PROTOCOL, Im.PROTOCOL_CUSTOM);
|
|
||||||
values.put(StatusUpdates.CUSTOM_PROTOCOL, CUSTOM_IM_PROTOCOL);
|
|
||||||
values.put(StatusUpdates.IM_ACCOUNT, username);
|
|
||||||
values.put(StatusUpdates.IM_HANDLE, status.getUserId());
|
|
||||||
values.put(StatusUpdates.STATUS_RES_PACKAGE, context.getPackageName());
|
|
||||||
values.put(StatusUpdates.STATUS_ICON, R.drawable.icon);
|
|
||||||
values.put(StatusUpdates.STATUS_LABEL, R.string.label);
|
|
||||||
batchOperation.add(ContactOperations.newInsertCpo(StatusUpdates.CONTENT_URI, true)
|
|
||||||
.withValues(values).build());
|
|
||||||
// A sync adapter should batch operations on multiple contacts,
|
|
||||||
// because it will make a dramatic performance difference.
|
|
||||||
if (batchOperation.size() >= 50) {
|
|
||||||
batchOperation.execute();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
batchOperation.execute();
|
batchOperation.execute();
|
||||||
@@ -131,88 +227,311 @@ public class ContactManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a single contact to the platform contacts provider.
|
* 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 context the Authenticator Activity context
|
||||||
* @param accountName the account the contact belongs to
|
* @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,
|
public static void addContact(Context context, String accountName, RawContact rawContact,
|
||||||
BatchOperation batchOperation) {
|
boolean inSync, BatchOperation batchOperation) {
|
||||||
|
|
||||||
// Put the data in the contacts provider
|
// Put the data in the contacts provider
|
||||||
final ContactOperations contactOp =
|
final ContactOperations contactOp = ContactOperations.createNewContact(
|
||||||
ContactOperations.createNewContact(context, user.getUserId(), accountName,
|
context, rawContact.getServerContactId(), accountName, inSync, batchOperation);
|
||||||
batchOperation);
|
|
||||||
contactOp.addName(user.getFirstName(), user.getLastName()).addEmail(user.getEmail())
|
contactOp.addName(rawContact.getFullName(), rawContact.getFirstName(),
|
||||||
.addPhone(user.getCellPhone(), Phone.TYPE_MOBILE).addPhone(user.getHomePhone(),
|
rawContact.getLastName())
|
||||||
Phone.TYPE_OTHER).addProfileAction(user.getUserId());
|
.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.
|
* 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 context the Authenticator Activity context
|
||||||
* @param resolver the ContentResolver to use
|
* @param resolver the ContentResolver to use
|
||||||
* @param accountName the account the contact belongs to
|
* @param rawContact the sample SyncAdapter contact object
|
||||||
* @param user 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
|
* @param rawContactId the unique Id for this rawContact in contacts
|
||||||
* provider
|
* provider
|
||||||
|
* @param batchOperation allow us to batch together multiple operations
|
||||||
|
* into a single provider call
|
||||||
*/
|
*/
|
||||||
private static void updateContact(Context context, ContentResolver resolver,
|
public static void updateContact(Context context, ContentResolver resolver,
|
||||||
String accountName, User user, long rawContactId, BatchOperation batchOperation) {
|
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 =
|
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);
|
new String[] {String.valueOf(rawContactId)}, null);
|
||||||
final ContactOperations contactOp =
|
final ContactOperations contactOp =
|
||||||
ContactOperations.updateExistingContact(context, rawContactId, batchOperation);
|
ContactOperations.updateExistingContact(context, rawContactId,
|
||||||
|
inSync, batchOperation);
|
||||||
try {
|
try {
|
||||||
|
// Iterate over the existing rows of data, and update each one
|
||||||
|
// with the information we received from the server.
|
||||||
while (c.moveToNext()) {
|
while (c.moveToNext()) {
|
||||||
final long id = c.getLong(DataQuery.COLUMN_ID);
|
final long id = c.getLong(DataQuery.COLUMN_ID);
|
||||||
final String mimeType = c.getString(DataQuery.COLUMN_MIMETYPE);
|
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)) {
|
if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) {
|
||||||
final String lastName = c.getString(DataQuery.COLUMN_FAMILY_NAME);
|
contactOp.updateName(uri,
|
||||||
final String firstName = c.getString(DataQuery.COLUMN_GIVEN_NAME);
|
c.getString(DataQuery.COLUMN_GIVEN_NAME),
|
||||||
contactOp.updateName(uri, firstName, lastName, user.getFirstName(), user
|
c.getString(DataQuery.COLUMN_FAMILY_NAME),
|
||||||
.getLastName());
|
c.getString(DataQuery.COLUMN_FULL_NAME),
|
||||||
|
rawContact.getFirstName(),
|
||||||
|
rawContact.getLastName(),
|
||||||
|
rawContact.getFullName());
|
||||||
} else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) {
|
} else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) {
|
||||||
final int type = c.getInt(DataQuery.COLUMN_PHONE_TYPE);
|
final int type = c.getInt(DataQuery.COLUMN_PHONE_TYPE);
|
||||||
if (type == Phone.TYPE_MOBILE) {
|
if (type == Phone.TYPE_MOBILE) {
|
||||||
cellPhone = c.getString(DataQuery.COLUMN_PHONE_NUMBER);
|
existingCellPhone = true;
|
||||||
contactOp.updatePhone(cellPhone, user.getCellPhone(), uri);
|
contactOp.updatePhone(c.getString(DataQuery.COLUMN_PHONE_NUMBER),
|
||||||
} else if (type == Phone.TYPE_OTHER) {
|
rawContact.getCellPhone(), uri);
|
||||||
otherPhone = c.getString(DataQuery.COLUMN_PHONE_NUMBER);
|
} else if (type == Phone.TYPE_HOME) {
|
||||||
contactOp.updatePhone(otherPhone, user.getHomePhone(), uri);
|
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)) {
|
} else if (mimeType.equals(Email.CONTENT_ITEM_TYPE)) {
|
||||||
email = c.getString(DataQuery.COLUMN_EMAIL_ADDRESS);
|
existingEmail = true;
|
||||||
contactOp.updateEmail(user.getEmail(), email, uri);
|
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
|
} // while
|
||||||
} finally {
|
} finally {
|
||||||
c.close();
|
c.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the cell phone, if present and not updated above
|
// Add the cell phone, if present and not updated above
|
||||||
if (cellPhone == null) {
|
if (!existingCellPhone) {
|
||||||
contactOp.addPhone(user.getCellPhone(), Phone.TYPE_MOBILE);
|
contactOp.addPhone(rawContact.getCellPhone(), Phone.TYPE_MOBILE);
|
||||||
}
|
}
|
||||||
// Add the other phone, if present and not updated above
|
// Add the home phone, if present and not updated above
|
||||||
if (otherPhone == null) {
|
if (!existingHomePhone) {
|
||||||
contactOp.addPhone(user.getHomePhone(), Phone.TYPE_OTHER);
|
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
|
// Add the email address, if present and not updated above
|
||||||
if (email == null) {
|
if (!existingEmail) {
|
||||||
contactOp.addEmail(user.getEmail());
|
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 context the Authenticator Activity context
|
||||||
* @param rawContactId the unique Id for this rawContact in contacts
|
* @param rawContactId the unique Id for this rawContact in contacts
|
||||||
@@ -222,33 +541,37 @@ public class ContactManager {
|
|||||||
BatchOperation batchOperation) {
|
BatchOperation batchOperation) {
|
||||||
|
|
||||||
batchOperation.add(ContactOperations.newDeleteCpo(
|
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
|
* Returns the RawContact id for a sample SyncAdapter contact, or 0 if the
|
||||||
* sample SyncAdapter user isn't found.
|
* sample SyncAdapter user isn't found.
|
||||||
*
|
*
|
||||||
* @param context the Authenticator Activity context
|
* @param resolver the content resolver to use
|
||||||
* @param userId the sample SyncAdapter user ID to lookup
|
* @param serverContactId the sample SyncAdapter user ID to lookup
|
||||||
* @return the RawContact id, or 0 if not found
|
* @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;
|
long rawContactId = 0;
|
||||||
final Cursor c =
|
final Cursor c = resolver.query(
|
||||||
resolver.query(RawContacts.CONTENT_URI, UserIdQuery.PROJECTION, UserIdQuery.SELECTION,
|
UserIdQuery.CONTENT_URI,
|
||||||
new String[] {String.valueOf(userId)}, null);
|
UserIdQuery.PROJECTION,
|
||||||
|
UserIdQuery.SELECTION,
|
||||||
|
new String[] {String.valueOf(serverContactId)},
|
||||||
|
null);
|
||||||
try {
|
try {
|
||||||
if (c.moveToFirst()) {
|
if ((c != null) && c.moveToFirst()) {
|
||||||
authorId = c.getLong(UserIdQuery.COLUMN_ID);
|
rawContactId = c.getLong(UserIdQuery.COLUMN_RAW_CONTACT_ID);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (c != null) {
|
if (c != null) {
|
||||||
c.close();
|
c.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return authorId;
|
return rawContactId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -266,7 +589,7 @@ public class ContactManager {
|
|||||||
resolver.query(Data.CONTENT_URI, ProfileQuery.PROJECTION, ProfileQuery.SELECTION,
|
resolver.query(Data.CONTENT_URI, ProfileQuery.PROJECTION, ProfileQuery.SELECTION,
|
||||||
new String[] {String.valueOf(userId)}, null);
|
new String[] {String.valueOf(userId)}, null);
|
||||||
try {
|
try {
|
||||||
if (c != null && c.moveToFirst()) {
|
if ((c != null) && c.moveToFirst()) {
|
||||||
profileId = c.getLong(ProfileQuery.COLUMN_ID);
|
profileId = c.getLong(ProfileQuery.COLUMN_ID);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -277,6 +600,46 @@ public class ContactManager {
|
|||||||
return profileId;
|
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
|
* Constants for a query to find a contact given a sample SyncAdapter user
|
||||||
* ID.
|
* ID.
|
||||||
@@ -304,15 +667,55 @@ public class ContactManager {
|
|||||||
private UserIdQuery() {
|
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 =
|
public static final String SELECTION =
|
||||||
RawContacts.ACCOUNT_TYPE + "='" + Constants.ACCOUNT_TYPE + "' AND "
|
RawContacts.ACCOUNT_TYPE + "='" + Constants.ACCOUNT_TYPE + "' AND "
|
||||||
+ RawContacts.SOURCE_ID + "=?";
|
+ 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
|
* Constants for a query to get contact data for a given rawContactId
|
||||||
*/
|
*/
|
||||||
@@ -322,29 +725,29 @@ public class ContactManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static final String[] PROJECTION =
|
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_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 Uri CONTENT_URI = Data.CONTENT_URI;
|
||||||
|
|
||||||
public static final int COLUMN_DATA1 = 2;
|
|
||||||
|
|
||||||
public static final int COLUMN_DATA2 = 3;
|
|
||||||
|
|
||||||
public static final int COLUMN_DATA3 = 4;
|
|
||||||
|
|
||||||
public static final int COLUMN_PHONE_NUMBER = COLUMN_DATA1;
|
public static final int COLUMN_PHONE_NUMBER = COLUMN_DATA1;
|
||||||
|
|
||||||
public static final int COLUMN_PHONE_TYPE = COLUMN_DATA2;
|
public static final int COLUMN_PHONE_TYPE = COLUMN_DATA2;
|
||||||
|
|
||||||
public static final int COLUMN_EMAIL_ADDRESS = COLUMN_DATA1;
|
public static final int COLUMN_EMAIL_ADDRESS = COLUMN_DATA1;
|
||||||
|
|
||||||
public static final int COLUMN_EMAIL_TYPE = COLUMN_DATA2;
|
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_GIVEN_NAME = COLUMN_DATA2;
|
||||||
|
|
||||||
public static final int COLUMN_FAMILY_NAME = COLUMN_DATA3;
|
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 + "=?";
|
public static final String SELECTION = Data.RAW_CONTACT_ID + "=?";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,56 +15,62 @@
|
|||||||
*/
|
*/
|
||||||
package com.example.android.samplesync.platform;
|
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.ContentProviderOperation;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.provider.ContactsContract;
|
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.Email;
|
||||||
import android.provider.ContactsContract.CommonDataKinds.Phone;
|
import android.provider.ContactsContract.CommonDataKinds.Phone;
|
||||||
|
import android.provider.ContactsContract.CommonDataKinds.Photo;
|
||||||
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
|
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
|
||||||
|
import android.provider.ContactsContract.Data;
|
||||||
|
import android.provider.ContactsContract.RawContacts;
|
||||||
import android.text.TextUtils;
|
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.
|
* Helper class for storing data in the platform content providers.
|
||||||
*/
|
*/
|
||||||
public class ContactOperations {
|
public class ContactOperations {
|
||||||
|
|
||||||
private final ContentValues mValues;
|
private final ContentValues mValues;
|
||||||
|
|
||||||
private ContentProviderOperation.Builder mBuilder;
|
|
||||||
|
|
||||||
private final BatchOperation mBatchOperation;
|
private final BatchOperation mBatchOperation;
|
||||||
|
|
||||||
private final Context mContext;
|
private final Context mContext;
|
||||||
|
private boolean mIsSyncOperation;
|
||||||
private boolean mYield;
|
|
||||||
|
|
||||||
private long mRawContactId;
|
private long mRawContactId;
|
||||||
|
|
||||||
private int mBackReference;
|
private int mBackReference;
|
||||||
|
|
||||||
private boolean mIsNewContact;
|
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
|
* Returns an instance of ContactOperations instance for adding new contact
|
||||||
* to the platform contacts provider.
|
* to the platform contacts provider.
|
||||||
*
|
*
|
||||||
* @param context the Authenticator Activity context
|
* @param context the Authenticator Activity context
|
||||||
* @param userId the userId of the sample SyncAdapter user object
|
* @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
|
* @return instance of ContactOperations
|
||||||
*/
|
*/
|
||||||
public static ContactOperations createNewContact(Context context, int userId,
|
public static ContactOperations createNewContact(Context context, long userId,
|
||||||
String accountName, BatchOperation batchOperation) {
|
String accountName, boolean isSyncOperation, BatchOperation batchOperation) {
|
||||||
|
return new ContactOperations(context, userId, accountName, isSyncOperation, batchOperation);
|
||||||
return new ContactOperations(context, userId, accountName, batchOperation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,50 +79,62 @@ public class ContactOperations {
|
|||||||
*
|
*
|
||||||
* @param context the Authenticator Activity context
|
* @param context the Authenticator Activity context
|
||||||
* @param rawContactId the unique Id of the existing rawContact
|
* @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
|
* @return instance of ContactOperations
|
||||||
*/
|
*/
|
||||||
public static ContactOperations updateExistingContact(Context context, long rawContactId,
|
public static ContactOperations updateExistingContact(Context context, long rawContactId,
|
||||||
BatchOperation batchOperation) {
|
boolean isSyncOperation, BatchOperation batchOperation) {
|
||||||
|
return new ContactOperations(context, rawContactId, isSyncOperation, batchOperation);
|
||||||
return new ContactOperations(context, rawContactId, batchOperation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ContactOperations(Context context, BatchOperation batchOperation) {
|
public ContactOperations(Context context, boolean isSyncOperation,
|
||||||
|
BatchOperation batchOperation) {
|
||||||
mValues = new ContentValues();
|
mValues = new ContentValues();
|
||||||
mYield = true;
|
mIsYieldAllowed = true;
|
||||||
|
mIsSyncOperation = isSyncOperation;
|
||||||
mContext = context;
|
mContext = context;
|
||||||
mBatchOperation = batchOperation;
|
mBatchOperation = batchOperation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ContactOperations(Context context, int userId, String accountName,
|
public ContactOperations(Context context, long userId, String accountName,
|
||||||
BatchOperation batchOperation) {
|
boolean isSyncOperation, BatchOperation batchOperation) {
|
||||||
|
this(context, isSyncOperation, batchOperation);
|
||||||
this(context, batchOperation);
|
|
||||||
mBackReference = mBatchOperation.size();
|
mBackReference = mBatchOperation.size();
|
||||||
mIsNewContact = true;
|
mIsNewContact = true;
|
||||||
mValues.put(RawContacts.SOURCE_ID, userId);
|
mValues.put(RawContacts.SOURCE_ID, userId);
|
||||||
mValues.put(RawContacts.ACCOUNT_TYPE, Constants.ACCOUNT_TYPE);
|
mValues.put(RawContacts.ACCOUNT_TYPE, Constants.ACCOUNT_TYPE);
|
||||||
mValues.put(RawContacts.ACCOUNT_NAME, accountName);
|
mValues.put(RawContacts.ACCOUNT_NAME, accountName);
|
||||||
mBuilder = newInsertCpo(RawContacts.CONTENT_URI, true).withValues(mValues);
|
ContentProviderOperation.Builder builder =
|
||||||
mBatchOperation.add(mBuilder.build());
|
newInsertCpo(RawContacts.CONTENT_URI, mIsSyncOperation, true).withValues(mValues);
|
||||||
|
mBatchOperation.add(builder.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
public ContactOperations(Context context, long rawContactId, BatchOperation batchOperation) {
|
public ContactOperations(Context context, long rawContactId, boolean isSyncOperation,
|
||||||
this(context, batchOperation);
|
BatchOperation batchOperation) {
|
||||||
|
this(context, isSyncOperation, batchOperation);
|
||||||
mIsNewContact = false;
|
mIsNewContact = false;
|
||||||
mRawContactId = rawContactId;
|
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 fullName The full name of the contact - typically from an edit form
|
||||||
* @param nameType type of name: family name, given name, etc.
|
* 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
|
* @return instance of ContactOperations
|
||||||
*/
|
*/
|
||||||
public ContactOperations addName(String firstName, String lastName) {
|
public ContactOperations addName(String fullName, String firstName, String lastName) {
|
||||||
|
|
||||||
mValues.clear();
|
mValues.clear();
|
||||||
|
|
||||||
|
if (!TextUtils.isEmpty(fullName)) {
|
||||||
|
mValues.put(StructuredName.DISPLAY_NAME, fullName);
|
||||||
|
mValues.put(StructuredName.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
|
||||||
|
} else {
|
||||||
if (!TextUtils.isEmpty(firstName)) {
|
if (!TextUtils.isEmpty(firstName)) {
|
||||||
mValues.put(StructuredName.GIVEN_NAME, firstName);
|
mValues.put(StructuredName.GIVEN_NAME, firstName);
|
||||||
mValues.put(StructuredName.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
|
mValues.put(StructuredName.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
|
||||||
@@ -125,6 +143,7 @@ public class ContactOperations {
|
|||||||
mValues.put(StructuredName.FAMILY_NAME, lastName);
|
mValues.put(StructuredName.FAMILY_NAME, lastName);
|
||||||
mValues.put(StructuredName.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
|
mValues.put(StructuredName.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (mValues.size() > 0) {
|
if (mValues.size() > 0) {
|
||||||
addInsertOp();
|
addInsertOp();
|
||||||
}
|
}
|
||||||
@@ -134,7 +153,7 @@ public class ContactOperations {
|
|||||||
/**
|
/**
|
||||||
* Adds an email
|
* Adds an email
|
||||||
*
|
*
|
||||||
* @param new email for user
|
* @param the email address we're adding
|
||||||
* @return instance of ContactOperations
|
* @return instance of ContactOperations
|
||||||
*/
|
*/
|
||||||
public ContactOperations addEmail(String email) {
|
public ContactOperations addEmail(String email) {
|
||||||
@@ -166,6 +185,19 @@ public class ContactOperations {
|
|||||||
return this;
|
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
|
* Adds a profile action
|
||||||
*
|
*
|
||||||
@@ -186,6 +218,20 @@ public class ContactOperations {
|
|||||||
return this;
|
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
|
* Updates contact's email
|
||||||
*
|
*
|
||||||
@@ -203,32 +249,53 @@ 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 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
|
* @return instance of ContactOperations
|
||||||
*/
|
*/
|
||||||
public ContactOperations updateName(Uri uri, String existingFirstName, String existingLastName,
|
public ContactOperations updateName(Uri uri,
|
||||||
String firstName, String lastName) {
|
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();
|
mValues.clear();
|
||||||
|
if (TextUtils.isEmpty(fullName)) {
|
||||||
if (!TextUtils.equals(existingFirstName, firstName)) {
|
if (!TextUtils.equals(existingFirstName, firstName)) {
|
||||||
mValues.put(StructuredName.GIVEN_NAME, firstName);
|
mValues.put(StructuredName.GIVEN_NAME, firstName);
|
||||||
}
|
}
|
||||||
if (!TextUtils.equals(existingLastName, lastName)) {
|
if (!TextUtils.equals(existingLastName, lastName)) {
|
||||||
mValues.put(StructuredName.FAMILY_NAME, lastName);
|
mValues.put(StructuredName.FAMILY_NAME, lastName);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (!TextUtils.equals(existingFullName, fullName)) {
|
||||||
|
mValues.put(StructuredName.DISPLAY_NAME, fullName);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (mValues.size() > 0) {
|
if (mValues.size() > 0) {
|
||||||
addUpdateOp(uri);
|
addUpdateOp(uri);
|
||||||
}
|
}
|
||||||
return this;
|
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
|
* Updates contact's phone
|
||||||
*
|
*
|
||||||
@@ -246,6 +313,19 @@ public class ContactOperations {
|
|||||||
return this;
|
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
|
* Updates contact's profile action
|
||||||
*
|
*
|
||||||
@@ -268,41 +348,62 @@ public class ContactOperations {
|
|||||||
if (!mIsNewContact) {
|
if (!mIsNewContact) {
|
||||||
mValues.put(Phone.RAW_CONTACT_ID, mRawContactId);
|
mValues.put(Phone.RAW_CONTACT_ID, mRawContactId);
|
||||||
}
|
}
|
||||||
mBuilder = newInsertCpo(addCallerIsSyncAdapterParameter(Data.CONTENT_URI), mYield);
|
ContentProviderOperation.Builder builder =
|
||||||
mBuilder.withValues(mValues);
|
newInsertCpo(Data.CONTENT_URI, mIsSyncOperation, mIsYieldAllowed);
|
||||||
|
builder.withValues(mValues);
|
||||||
if (mIsNewContact) {
|
if (mIsNewContact) {
|
||||||
mBuilder.withValueBackReference(Data.RAW_CONTACT_ID, mBackReference);
|
builder.withValueBackReference(Data.RAW_CONTACT_ID, mBackReference);
|
||||||
}
|
}
|
||||||
mYield = false;
|
mIsYieldAllowed = false;
|
||||||
mBatchOperation.add(mBuilder.build());
|
mBatchOperation.add(builder.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds an update operation into the batch
|
* Adds an update operation into the batch
|
||||||
*/
|
*/
|
||||||
private void addUpdateOp(Uri uri) {
|
private void addUpdateOp(Uri uri) {
|
||||||
mBuilder = newUpdateCpo(uri, mYield).withValues(mValues);
|
ContentProviderOperation.Builder builder =
|
||||||
mYield = false;
|
newUpdateCpo(uri, mIsSyncOperation, mIsYieldAllowed).withValues(mValues);
|
||||||
mBatchOperation.add(mBuilder.build());
|
mIsYieldAllowed = false;
|
||||||
|
mBatchOperation.add(builder.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ContentProviderOperation.Builder newInsertCpo(Uri uri, boolean yield) {
|
public static ContentProviderOperation.Builder newInsertCpo(Uri uri,
|
||||||
return ContentProviderOperation.newInsert(addCallerIsSyncAdapterParameter(uri))
|
boolean isSyncOperation, boolean isYieldAllowed) {
|
||||||
.withYieldAllowed(yield);
|
return ContentProviderOperation
|
||||||
|
.newInsert(addCallerIsSyncAdapterParameter(uri, isSyncOperation))
|
||||||
|
.withYieldAllowed(isYieldAllowed);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ContentProviderOperation.Builder newUpdateCpo(Uri uri, boolean yield) {
|
public static ContentProviderOperation.Builder newUpdateCpo(Uri uri,
|
||||||
return ContentProviderOperation.newUpdate(addCallerIsSyncAdapterParameter(uri))
|
boolean isSyncOperation, boolean isYieldAllowed) {
|
||||||
.withYieldAllowed(yield);
|
return ContentProviderOperation
|
||||||
|
.newUpdate(addCallerIsSyncAdapterParameter(uri, isSyncOperation))
|
||||||
|
.withYieldAllowed(isYieldAllowed);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ContentProviderOperation.Builder newDeleteCpo(Uri uri, boolean yield) {
|
public static ContentProviderOperation.Builder newDeleteCpo(Uri uri,
|
||||||
return ContentProviderOperation.newDelete(addCallerIsSyncAdapterParameter(uri))
|
boolean isSyncOperation, boolean isYieldAllowed) {
|
||||||
.withYieldAllowed(yield);
|
return ContentProviderOperation
|
||||||
|
.newDelete(addCallerIsSyncAdapterParameter(uri, isSyncOperation))
|
||||||
|
.withYieldAllowed(isYieldAllowed);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Uri addCallerIsSyncAdapterParameter(Uri uri) {
|
private static Uri addCallerIsSyncAdapterParameter(Uri uri, boolean isSyncOperation) {
|
||||||
return uri.buildUpon().appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
|
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();
|
.build();
|
||||||
}
|
}
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,12 +24,12 @@ import android.content.ContentProviderClient;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SyncResult;
|
import android.content.SyncResult;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import com.example.android.samplesync.Constants;
|
import com.example.android.samplesync.Constants;
|
||||||
import com.example.android.samplesync.client.NetworkUtilities;
|
import com.example.android.samplesync.client.NetworkUtilities;
|
||||||
import com.example.android.samplesync.client.User;
|
import com.example.android.samplesync.client.RawContact;
|
||||||
import com.example.android.samplesync.client.User.Status;
|
|
||||||
import com.example.android.samplesync.platform.ContactManager;
|
import com.example.android.samplesync.platform.ContactManager;
|
||||||
|
|
||||||
import org.apache.http.ParseException;
|
import org.apache.http.ParseException;
|
||||||
@@ -37,23 +37,27 @@ import org.apache.http.auth.AuthenticationException;
|
|||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SyncAdapter implementation for syncing sample SyncAdapter contacts to the
|
* 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 {
|
public class SyncAdapter extends AbstractThreadedSyncAdapter {
|
||||||
|
|
||||||
private static final String TAG = "SyncAdapter";
|
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 AccountManager mAccountManager;
|
||||||
|
|
||||||
private final Context mContext;
|
private final Context mContext;
|
||||||
|
|
||||||
private Date mLastUpdated;
|
|
||||||
|
|
||||||
public SyncAdapter(Context context, boolean autoInitialize) {
|
public SyncAdapter(Context context, boolean autoInitialize) {
|
||||||
super(context, autoInitialize);
|
super(context, autoInitialize);
|
||||||
mContext = context;
|
mContext = context;
|
||||||
@@ -64,42 +68,101 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter {
|
|||||||
public void onPerformSync(Account account, Bundle extras, String authority,
|
public void onPerformSync(Account account, Bundle extras, String authority,
|
||||||
ContentProviderClient provider, SyncResult syncResult) {
|
ContentProviderClient provider, SyncResult syncResult) {
|
||||||
|
|
||||||
List<User> users;
|
|
||||||
List<Status> statuses;
|
|
||||||
String authtoken = null;
|
|
||||||
try {
|
try {
|
||||||
// use the account manager to request the credentials
|
// see if we already have a sync-state attached to this account. By handing
|
||||||
authtoken =
|
// This value to the server, we can just get the contacts that have
|
||||||
mAccountManager
|
// been updated on the server-side since our last sync-up
|
||||||
.blockingGetAuthToken(account, Constants.AUTHTOKEN_TYPE, true /* notifyAuthFailure */);
|
long lastSyncMarker = getServerSyncMarker(account);
|
||||||
// fetch updates from the sample service over the cloud
|
|
||||||
users = NetworkUtilities.fetchFriendUpdates(account, authtoken, mLastUpdated);
|
// By default, contacts from a 3rd party provider are hidden in the contacts
|
||||||
// update the last synced date.
|
// list. So let's set the flag that causes them to be visible, so that users
|
||||||
mLastUpdated = new Date();
|
// can actually see these contacts.
|
||||||
// update platform 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");
|
Log.d(TAG, "Calling contactManager's sync contacts");
|
||||||
ContactManager.syncContacts(mContext, account.name, users);
|
long newSyncState = ContactManager.updateContacts(mContext,
|
||||||
// fetch and update status messages for all the synced users.
|
account.name,
|
||||||
statuses = NetworkUtilities.fetchFriendStatuses(account, authtoken);
|
updatedContacts,
|
||||||
ContactManager.insertStatuses(mContext, account.name, statuses);
|
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) {
|
} catch (final AuthenticatorException e) {
|
||||||
syncResult.stats.numParseExceptions++;
|
|
||||||
Log.e(TAG, "AuthenticatorException", e);
|
Log.e(TAG, "AuthenticatorException", e);
|
||||||
|
syncResult.stats.numParseExceptions++;
|
||||||
} catch (final OperationCanceledException e) {
|
} catch (final OperationCanceledException e) {
|
||||||
Log.e(TAG, "OperationCanceledExcetpion", e);
|
Log.e(TAG, "OperationCanceledExcetpion", e);
|
||||||
} catch (final IOException e) {
|
} catch (final IOException e) {
|
||||||
Log.e(TAG, "IOException", e);
|
Log.e(TAG, "IOException", e);
|
||||||
syncResult.stats.numIoExceptions++;
|
syncResult.stats.numIoExceptions++;
|
||||||
} catch (final AuthenticationException e) {
|
} catch (final AuthenticationException e) {
|
||||||
mAccountManager.invalidateAuthToken(Constants.ACCOUNT_TYPE, authtoken);
|
|
||||||
syncResult.stats.numAuthExceptions++;
|
|
||||||
Log.e(TAG, "AuthenticationException", e);
|
Log.e(TAG, "AuthenticationException", e);
|
||||||
|
syncResult.stats.numAuthExceptions++;
|
||||||
} catch (final ParseException e) {
|
} catch (final ParseException e) {
|
||||||
syncResult.stats.numParseExceptions++;
|
|
||||||
Log.e(TAG, "ParseException", e);
|
Log.e(TAG, "ParseException", e);
|
||||||
} catch (final JSONException e) {
|
|
||||||
syncResult.stats.numParseExceptions++;
|
syncResult.stats.numParseExceptions++;
|
||||||
|
} catch (final JSONException e) {
|
||||||
Log.e(TAG, "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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user