eclair snapshot
diff --git a/Android.mk b/Android.mk
index 6ca4633..277d0e5 100644
--- a/Android.mk
+++ b/Android.mk
@@ -16,6 +16,10 @@
include $(CLEAR_VARS)
LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_SRC_FILES += \
+ src/com/android/exchange/IEmailService.aidl \
+ src/com/android/exchange/IEmailServiceCallback.aidl
+
LOCAL_PACKAGE_NAME := Email
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 6f2e087..9b376af 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -23,17 +23,34 @@
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-
+ <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+ <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
+ <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
+ <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
+ <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
+
+ <!-- For EAS purposes; could be removed when EAS has a permanent home -->
+ <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
+
<!-- Only required if a store implements push mail and needs to keep network open -->
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
-
+
+ <!-- Grant permission to other apps to view attachments -->
<permission android:name="com.android.email.permission.READ_ATTACHMENT"
android:permissionGroup="android.permission-group.MESSAGES"
android:protectionLevel="dangerous"
android:label="@string/read_attachment_label"
android:description="@string/read_attachment_desc"/>
<uses-permission android:name="com.android.email.permission.READ_ATTACHMENT"/>
+
+ <!-- Grant permission to system apps to access provider (see provider below) -->
+ <permission android:name="com.android.email.permission.ACCESS_PROVIDER"
+ android:protectionLevel="signatureOrSystem"
+ android:label="@string/permission_access_provider_label"
+ android:description="@string/permission_access_provider_desc"/>
+ <uses-permission android:name="com.android.email.permission.ACCESS_PROVIDER"/>
+
<application android:icon="@drawable/icon" android:label="@string/app_name"
android:name="Email">
<activity android:name=".activity.Welcome">
@@ -44,9 +61,11 @@
</intent-filter>
</activity>
+ <!-- Must be exported in order for the AccountManager to launch it -->
<activity
android:name=".activity.setup.AccountSetupBasics"
android:label="@string/account_setup_basics_title"
+ android:exported="true"
>
</activity>
<activity
@@ -98,8 +117,7 @@
android:label="@string/debug_title">
</activity>
<activity
- android:name=".activity.Accounts"
- android:label="@string/accounts_title"
+ android:name=".activity.AccountFolderList"
android:launchMode="singleTop" >
</activity>
@@ -115,12 +133,17 @@
</activity>
<activity
- android:name=".activity.FolderMessageList">
+ android:name=".activity.MailboxList">
+ </activity>
+
+ <activity
+ android:name=".activity.MessageList">
<intent-filter>
<!-- This action is only to allow an entry point for launcher shortcuts -->
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>
+
<activity
android:name=".activity.MessageView"
android:theme="@android:style/Theme.NoTitleBar" >
@@ -139,15 +162,24 @@
</intent-filter>
<intent-filter android:label="@string/app_name">
<action android:name="android.intent.action.SEND" />
- <data android:mimeType="text/plain" />
- <data android:mimeType="image/*" />
- <data android:mimeType="video/*" />
+ <data android:mimeType="*/*" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ <intent-filter android:label="@string/app_name">
+ <action android:name="android.intent.action.SEND_MULTIPLE" />
+ <data android:mimeType="*/*" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
- <receiver android:name=".service.BootReceiver"
- android:enabled="false"
- >
+ <receiver android:name="com.android.exchange.EmailSyncAlarmReceiver"/>
+ <receiver android:name="com.android.exchange.MailboxAlarmReceiver"/>
+ <receiver android:name="com.android.exchange.BootReceiver" android:enabled="true">
+ <intent-filter>
+ <action android:name="android.intent.action.BOOT_COMPLETED" />
+ </intent-filter>
+ </receiver>
+
+ <receiver android:name=".service.BootReceiver" android:enabled="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
@@ -158,11 +190,40 @@
<action android:name="android.intent.action.DEVICE_STORAGE_OK" />
</intent-filter>
</receiver>
+
<service
android:name=".service.MailService"
android:enabled="false"
>
</service>
+
+ <!--Required stanza to register the ContactsSyncAdapterService with SyncManager -->
+ <service
+ android:name="com.android.exchange.ContactsSyncAdapterService"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.content.SyncAdapter" />
+ </intent-filter>
+ <meta-data android:name="android.content.SyncAdapter"
+ android:resource="@xml/syncadapter_contacts" />
+ </service>
+
+ <!-- Add android:process=":remote" below to enable SyncManager as a separate process -->
+ <service
+ android:name="com.android.exchange.SyncManager"
+ android:enabled="true"
+ >
+ </service>
+
+ <!--Required stanza to register the EasAuthenticatorService with AccountManager -->
+ <service android:name=".service.EasAuthenticatorService" android:exported="true">
+ <intent-filter>
+ <action android:name="android.accounts.AccountAuthenticator" />
+ </intent-filter>
+ <meta-data android:name="android.accounts.AccountAuthenticator"
+ android:resource="@xml/authenticator" />
+ </service>
+
<provider
android:name=".provider.AttachmentProvider"
android:authorities="com.android.email.attachmentprovider"
@@ -170,5 +231,14 @@
android:grantUriPermissions="true"
android:readPermission="com.android.email.permission.READ_ATTACHMENT"
/>
+
+ <!-- This provider MUST be protected by strict permissions, as granting access to
+ it exposes user passwords and other confidential information. -->
+ <provider
+ android:name=".provider.EmailProvider"
+ android:authorities="com.android.email.provider"
+ android:multiprocess="true"
+ android:permission="com.android.email.permission.ACCESS_PROVIDER"
+ />
</application>
</manifest>
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_10.9.png b/res/drawable-hdpi/appointment_indicator_leftside_10.9.png
new file mode 100644
index 0000000..ff09049
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_10.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_11.9.png b/res/drawable-hdpi/appointment_indicator_leftside_11.9.png
new file mode 100644
index 0000000..6a2e4f2
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_11.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_12.9.png b/res/drawable-hdpi/appointment_indicator_leftside_12.9.png
new file mode 100644
index 0000000..0f19c83
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_12.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_13.9.png b/res/drawable-hdpi/appointment_indicator_leftside_13.9.png
new file mode 100644
index 0000000..7501e35
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_13.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_14.9.png b/res/drawable-hdpi/appointment_indicator_leftside_14.9.png
new file mode 100644
index 0000000..53f97a6
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_14.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_15.9.png b/res/drawable-hdpi/appointment_indicator_leftside_15.9.png
new file mode 100644
index 0000000..846f6f8
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_15.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_16.9.png b/res/drawable-hdpi/appointment_indicator_leftside_16.9.png
new file mode 100644
index 0000000..1707540
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_16.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_17.9.png b/res/drawable-hdpi/appointment_indicator_leftside_17.9.png
new file mode 100644
index 0000000..7fd945d
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_17.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_18.9.png b/res/drawable-hdpi/appointment_indicator_leftside_18.9.png
new file mode 100644
index 0000000..8cf47ae
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_18.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_19.9.png b/res/drawable-hdpi/appointment_indicator_leftside_19.9.png
new file mode 100644
index 0000000..6831c01
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_19.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_2.9.png b/res/drawable-hdpi/appointment_indicator_leftside_2.9.png
new file mode 100644
index 0000000..b4cee11
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_2.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_20.9.png b/res/drawable-hdpi/appointment_indicator_leftside_20.9.png
new file mode 100644
index 0000000..d07d826
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_20.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_21.9.png b/res/drawable-hdpi/appointment_indicator_leftside_21.9.png
new file mode 100644
index 0000000..f410269
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_21.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_3.9.png b/res/drawable-hdpi/appointment_indicator_leftside_3.9.png
new file mode 100644
index 0000000..69bd6a9
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_3.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_4.9.png b/res/drawable-hdpi/appointment_indicator_leftside_4.9.png
new file mode 100644
index 0000000..d09ea90
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_4.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_5.9.png b/res/drawable-hdpi/appointment_indicator_leftside_5.9.png
new file mode 100644
index 0000000..d27fc91
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_5.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_6.9.png b/res/drawable-hdpi/appointment_indicator_leftside_6.9.png
new file mode 100644
index 0000000..c014633
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_6.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_7.9.png b/res/drawable-hdpi/appointment_indicator_leftside_7.9.png
new file mode 100644
index 0000000..febb514
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_7.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_8.9.png b/res/drawable-hdpi/appointment_indicator_leftside_8.9.png
new file mode 100644
index 0000000..1415e44
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_8.9.png
Binary files differ
diff --git a/res/drawable-hdpi/appointment_indicator_leftside_9.9.png b/res/drawable-hdpi/appointment_indicator_leftside_9.9.png
new file mode 100644
index 0000000..d018fcf
--- /dev/null
+++ b/res/drawable-hdpi/appointment_indicator_leftside_9.9.png
Binary files differ
diff --git a/res/drawable-hdpi/button_indicator_next.png b/res/drawable-hdpi/button_indicator_next.png
new file mode 100644
index 0000000..c7c553b
--- /dev/null
+++ b/res/drawable-hdpi/button_indicator_next.png
Binary files differ
diff --git a/res/drawable-hdpi/expander_ic_folder_maximized.9.png b/res/drawable-hdpi/expander_ic_folder_maximized.9.png
new file mode 100644
index 0000000..4d71060
--- /dev/null
+++ b/res/drawable-hdpi/expander_ic_folder_maximized.9.png
Binary files differ
diff --git a/res/drawable-hdpi/expander_ic_folder_minimized.9.png b/res/drawable-hdpi/expander_ic_folder_minimized.9.png
new file mode 100644
index 0000000..0c04943
--- /dev/null
+++ b/res/drawable-hdpi/expander_ic_folder_minimized.9.png
Binary files differ
diff --git a/res/drawable-hdpi/icon.png b/res/drawable-hdpi/icon.png
new file mode 100644
index 0000000..b0b3f71
--- /dev/null
+++ b/res/drawable-hdpi/icon.png
Binary files differ
diff --git a/res/drawable-hdpi/presence_inactive.png b/res/drawable-hdpi/presence_inactive.png
new file mode 100644
index 0000000..d96d351
--- /dev/null
+++ b/res/drawable-hdpi/presence_inactive.png
Binary files differ
diff --git a/res/drawable-hdpi/section_dark.9.png b/res/drawable-hdpi/section_dark.9.png
new file mode 100644
index 0000000..3e63fa6
--- /dev/null
+++ b/res/drawable-hdpi/section_dark.9.png
Binary files differ
diff --git a/res/drawable-hdpi/section_light.9.png b/res/drawable-hdpi/section_light.9.png
new file mode 100644
index 0000000..6fc53ca
--- /dev/null
+++ b/res/drawable-hdpi/section_light.9.png
Binary files differ
diff --git a/res/drawable-hdpi/text_box_light.9.png b/res/drawable-hdpi/text_box_light.9.png
new file mode 100644
index 0000000..de9c0ad
--- /dev/null
+++ b/res/drawable-hdpi/text_box_light.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_10.9.png b/res/drawable-mdpi/appointment_indicator_leftside_10.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_10.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_10.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_11.9.png b/res/drawable-mdpi/appointment_indicator_leftside_11.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_11.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_11.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_12.9.png b/res/drawable-mdpi/appointment_indicator_leftside_12.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_12.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_12.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_13.9.png b/res/drawable-mdpi/appointment_indicator_leftside_13.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_13.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_13.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_14.9.png b/res/drawable-mdpi/appointment_indicator_leftside_14.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_14.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_14.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_15.9.png b/res/drawable-mdpi/appointment_indicator_leftside_15.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_15.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_15.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_16.9.png b/res/drawable-mdpi/appointment_indicator_leftside_16.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_16.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_16.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_17.9.png b/res/drawable-mdpi/appointment_indicator_leftside_17.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_17.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_17.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_18.9.png b/res/drawable-mdpi/appointment_indicator_leftside_18.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_18.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_18.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_19.9.png b/res/drawable-mdpi/appointment_indicator_leftside_19.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_19.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_19.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_2.9.png b/res/drawable-mdpi/appointment_indicator_leftside_2.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_2.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_2.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_20.9.png b/res/drawable-mdpi/appointment_indicator_leftside_20.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_20.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_20.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_21.9.png b/res/drawable-mdpi/appointment_indicator_leftside_21.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_21.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_21.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_3.9.png b/res/drawable-mdpi/appointment_indicator_leftside_3.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_3.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_3.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_4.9.png b/res/drawable-mdpi/appointment_indicator_leftside_4.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_4.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_4.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_5.9.png b/res/drawable-mdpi/appointment_indicator_leftside_5.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_5.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_5.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_6.9.png b/res/drawable-mdpi/appointment_indicator_leftside_6.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_6.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_6.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_7.9.png b/res/drawable-mdpi/appointment_indicator_leftside_7.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_7.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_7.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_8.9.png b/res/drawable-mdpi/appointment_indicator_leftside_8.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_8.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_8.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_9.9.png b/res/drawable-mdpi/appointment_indicator_leftside_9.9.png
similarity index 100%
rename from res/drawable/appointment_indicator_leftside_9.9.png
rename to res/drawable-mdpi/appointment_indicator_leftside_9.9.png
Binary files differ
diff --git a/res/drawable/button_indicator_next.png b/res/drawable-mdpi/button_indicator_next.png
similarity index 100%
rename from res/drawable/button_indicator_next.png
rename to res/drawable-mdpi/button_indicator_next.png
Binary files differ
diff --git a/res/drawable/expander_ic_folder_maximized.9.png b/res/drawable-mdpi/expander_ic_folder_maximized.9.png
similarity index 100%
rename from res/drawable/expander_ic_folder_maximized.9.png
rename to res/drawable-mdpi/expander_ic_folder_maximized.9.png
Binary files differ
diff --git a/res/drawable/expander_ic_folder_minimized.9.png b/res/drawable-mdpi/expander_ic_folder_minimized.9.png
similarity index 100%
rename from res/drawable/expander_ic_folder_minimized.9.png
rename to res/drawable-mdpi/expander_ic_folder_minimized.9.png
Binary files differ
diff --git a/res/drawable/icon.png b/res/drawable-mdpi/icon.png
similarity index 100%
rename from res/drawable/icon.png
rename to res/drawable-mdpi/icon.png
Binary files differ
diff --git a/res/drawable/presence_inactive.png b/res/drawable-mdpi/presence_inactive.png
similarity index 100%
rename from res/drawable/presence_inactive.png
rename to res/drawable-mdpi/presence_inactive.png
Binary files differ
diff --git a/res/drawable-mdpi/section_dark.9.png b/res/drawable-mdpi/section_dark.9.png
new file mode 100644
index 0000000..7242b61
--- /dev/null
+++ b/res/drawable-mdpi/section_dark.9.png
Binary files differ
diff --git a/res/drawable-mdpi/section_light.9.png b/res/drawable-mdpi/section_light.9.png
new file mode 100644
index 0000000..9852851
--- /dev/null
+++ b/res/drawable-mdpi/section_light.9.png
Binary files differ
diff --git a/res/drawable/text_box_light.9.png b/res/drawable-mdpi/text_box_light.9.png
similarity index 100%
rename from res/drawable/text_box_light.9.png
rename to res/drawable-mdpi/text_box_light.9.png
Binary files differ
diff --git a/res/drawable/appointment_indicator_leftside_1.9.png b/res/drawable/appointment_indicator_leftside_1.9.png
deleted file mode 100644
index 5e40235..0000000
--- a/res/drawable/appointment_indicator_leftside_1.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/attached_image_placeholder.png b/res/drawable/attached_image_placeholder.png
deleted file mode 100644
index 90acbf7..0000000
--- a/res/drawable/attached_image_placeholder.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/divider_horizontal_email.9.png b/res/drawable/divider_horizontal_email.9.png
deleted file mode 100644
index 1f56364..0000000
--- a/res/drawable/divider_horizontal_email.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/email_quoted_bar.9.png b/res/drawable/email_quoted_bar.9.png
deleted file mode 100644
index 3d197d5..0000000
--- a/res/drawable/email_quoted_bar.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/ic_email_attachment.png b/res/drawable/ic_email_attachment.png
deleted file mode 100644
index 6fe0d3e..0000000
--- a/res/drawable/ic_email_attachment.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/ic_email_attachment_small.png b/res/drawable/ic_email_attachment_small.png
deleted file mode 100644
index eaed42c..0000000
--- a/res/drawable/ic_email_attachment_small.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/ic_email_thread_open_top_default.9.png b/res/drawable/ic_email_thread_open_top_default.9.png
deleted file mode 100644
index 9aebde9..0000000
--- a/res/drawable/ic_email_thread_open_top_default.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/ic_left_prev_arrow_default.png b/res/drawable/ic_left_prev_arrow_default.png
deleted file mode 100755
index acc60b7..0000000
--- a/res/drawable/ic_left_prev_arrow_default.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/ic_menu_forward_mail.png b/res/drawable/ic_menu_forward_mail.png
deleted file mode 100644
index 97b63af..0000000
--- a/res/drawable/ic_menu_forward_mail.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/ic_menu_reply.png b/res/drawable/ic_menu_reply.png
deleted file mode 100644
index bd18b58..0000000
--- a/res/drawable/ic_menu_reply.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/ic_menu_reply_all.png b/res/drawable/ic_menu_reply_all.png
deleted file mode 100644
index 731e997..0000000
--- a/res/drawable/ic_menu_reply_all.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/ic_menu_save_draft.png b/res/drawable/ic_menu_save_draft.png
deleted file mode 100644
index 7231933..0000000
--- a/res/drawable/ic_menu_save_draft.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/ic_mms_attachment_small.png b/res/drawable/ic_mms_attachment_small.png
deleted file mode 100644
index a661ce4..0000000
--- a/res/drawable/ic_mms_attachment_small.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/ic_right_next_arrow_default.png b/res/drawable/ic_right_next_arrow_default.png
deleted file mode 100755
index 62d3bbc..0000000
--- a/res/drawable/ic_right_next_arrow_default.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/stat_notify_email_generic.png b/res/drawable/stat_notify_email_generic.png
deleted file mode 100644
index 686033f..0000000
--- a/res/drawable/stat_notify_email_generic.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable/text_box.9.png b/res/drawable/text_box.9.png
deleted file mode 100644
index e7d4207..0000000
--- a/res/drawable/text_box.9.png
+++ /dev/null
Binary files differ
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 6abaa2e..fc811b9 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -16,14 +16,44 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
- <!-- Permissions label -->
- <string name="read_attachment_label">read Email attachments</string>
- <!-- Permissions description -->
- <string name="read_attachment_desc">Allows this application to read your Email attachments.</string>
+ <!-- Deprecated strings - Move the identifiers to this section, mark as DO NOT TRANSLATE,
+ and remove the actual text. These will be removed in a bulk operation. -->
+ <!-- Do Not Translate. Unused string. -->
+ <string name="special_mailbox_name_inbox"></string>
+ <!-- Do Not Translate. Unused string. -->
+ <string name="special_mailbox_display_name_inbox"></string>
+ <!-- Do Not Translate. Unused string. -->
+ <string name="special_mailbox_display_name_outbox"></string>
+ <!-- Do Not Translate. Unused string. -->
+ <string name="special_mailbox_display_name_drafts"></string>
+ <!-- Do Not Translate. Unused string. -->
+ <string name="special_mailbox_display_name_trash"></string>
+ <!-- Do Not Translate. Unused string. -->
+ <string name="special_mailbox_display_name_sent"></string>
+ <!-- Do Not Translate. Unused string. -->
+ <string name="special_mailbox_display_name_junk"></string>
+ <!-- Do Not Translate. Unused string. -->
+ <string name="account_setup_incoming_delete_policy_7days_label"></string>
+ <!-- Do Not Translate. Unused string. -->
+ <string name="account_setup_incoming_security_ssl_optional_label"></string>
+ <!-- Do Not Translate. Unused string. -->
+ <string name="account_setup_incoming_security_tls_optional_label"></string>
+
+ <!-- Permissions label for reading attachments -->
+ <string name="read_attachment_label">Read Email attachments</string>
+ <!-- Permissions description for reading attachments -->
+ <string name="read_attachment_desc">Allows this application to read your Email
+ attachments.</string>
+ <!-- Permissions label for accessing the main provider -->
+ <string name="permission_access_provider_label">Access Email provider data</string>
+ <!-- Permissions description for accessing the main provider -->
+ <string name="permission_access_provider_desc">Allows this application to access your Email
+ database, including received messages, sent messages, usernames and passwords.</string>
+
<!-- Name of application on Home screen -->
<string name="app_name">Email</string>
<!-- Title of Accounts screen -->
- <string name="accounts_title">Your accounts</string>
+ <string name="accounts_title">Email</string>
<!-- Title of compose screen -->
<string name="compose_title">Compose</string>
<!-- Title of debug screen -->
@@ -54,11 +84,25 @@
<string name="discard_action">Discard</string>
<!-- Menu item/button name -->
<string name="save_draft_action">Save as draft</string>
+ <!-- Menu item/button name -->
+ <string name="read_unread_action">Read/Unread</string>
+ <!-- Menu item/button name -->
+ <string name="read_action">Mark read</string>
+ <!-- Menu item/button name -->
+ <string name="unread_action">Mark unread</string>
+ <!-- Menu item/button name -->
+ <string name="favorite_action">Favorite</string>
+ <!-- Menu item/button name -->
+ <string name="set_star_action">Add star</string>
+ <!-- Menu item/button name -->
+ <string name="remove_star_action">Remove star</string>
<!-- Menu item -->
<string name="refresh_action">Refresh</string>
<!-- Menu item -->
<string name="add_account_action">Add account</string>
<!-- Menu item -->
+ <string name="deselect_all_action">Deselect all</string>
+ <!-- Menu item -->
<string name="compose_action">Compose</string>
<!-- Menu item/button name -->
<string name="search_action">Search</string>
@@ -69,6 +113,8 @@
<!-- Menu item -->
<string name="remove_account_action">Remove account</string>
<!-- Menu item -->
+ <string name="folders_action">Folders</string>
+ <!-- Menu item -->
<string name="accounts_action">Accounts</string>
<!-- Menu item -->
<string name="mark_as_read_action">Mark as read</string>
@@ -97,6 +143,9 @@
<string name="status_loading_more_failed">Retry loading more messages</string>
<!-- Notification title in status bar -->
<string name="notification_new_title">New email</string>
+ <!-- Dialog text when we are unable to load the message for display -->
+ <string name="error_loading_message_body">Unexpected error while loading message text. Message
+ may be too large to view.</string>
<!-- Notification message in notifications window when one account has
one or more new messages; e.g, "279 unread (someone@google.com)". -->
@@ -120,27 +169,33 @@
<item quantity="other">in <xliff:g id="number_accounts" example="10">%d</xliff:g> accounts</item>
</plurals>
-
- <!-- In the folder list view, the inbox will be displayed with this name -->
- <string name="special_mailbox_name_inbox">Inbox</string>
+ <!-- The next set of strings are used server-side and must not be localized. -->
+ <!-- Do Not Translate. This is the name of the "inbox" folder, on the server. -->
+ <string name="mailbox_name_server_inbox">Inbox</string>
<!-- Do Not Translate. This is the name of the "outbox" folder, on the server. -->
- <string name="special_mailbox_name_outbox">Outbox</string>
+ <string name="mailbox_name_server_outbox">Outbox</string>
<!-- Do Not Translate. This is the name of the "drafts" folder, on the server. -->
- <string name="special_mailbox_name_drafts">Drafts</string>
+ <string name="mailbox_name_server_drafts">Drafts</string>
<!-- Do Not Translate. This is the name of the "trash" folder, on the server. -->
- <string name="special_mailbox_name_trash">Trash</string>
+ <string name="mailbox_name_server_trash">Trash</string>
<!-- Do Not Translate. This is the name of the "sent" folder, on the server. -->
- <string name="special_mailbox_name_sent">Sent</string>
- <!-- In the folder list view, the outbox will be displayed with this name -->
- <string name="special_mailbox_display_name_outbox">Outbox</string>
- <!-- In the folder list view, the drafts will be displayed with this name -->
- <string name="special_mailbox_display_name_drafts">Drafts</string>
- <!-- In the folder list view, the trash will be displayed with this name -->
- <string name="special_mailbox_display_name_trash">Trash</string>
- <!-- In the folder list view, the sent will be displayed with this name -->
- <string name="special_mailbox_display_name_sent">Sent</string>
- <!-- Text on screen when you don't have any external email accounts set up -->
- <string name="accounts_welcome">Welcome to Email setup!\n\nUse any email account with Email.\n\nMost popular email accounts can be set up in 2 steps!</string>
+ <string name="mailbox_name_server_sent">Sent</string>
+ <!-- Do Not Translate. This is the name of the "junk" folder, on the server. -->
+ <string name="mailbox_name_server_junk">Junk</string>
+
+ <!-- The next set of strings are used in local display and may be localized. -->
+ <!-- In the UI, the inbox will be displayed with this name -->
+ <string name="mailbox_name_display_inbox">Inbox</string>
+ <!-- In the UI, the outbox will be displayed with this name -->
+ <string name="mailbox_name_display_outbox">Outbox</string>
+ <!-- In the UI, the drafts will be displayed with this name -->
+ <string name="mailbox_name_display_drafts">Drafts</string>
+ <!-- In the UI, the trash will be displayed with this name -->
+ <string name="mailbox_name_display_trash">Trash</string>
+ <!-- In the UI, the sent will be displayed with this name -->
+ <string name="mailbox_name_display_sent">Sent</string>
+ <!-- In the UI, the junk will be displayed with this name -->
+ <string name="mailbox_name_display_junk">Junk</string>
<!-- Version number, shown only on debug screen -->
<string name="debug_version_fmt">Version: <xliff:g id="version">%s</xliff:g></string>
@@ -148,12 +203,34 @@
<string name="debug_enable_debug_logging_label">Enable extra debug logging?</string>
<!-- Checkbox label, shown only on debug screen -->
<string name="debug_enable_sensitive_logging_label">Enable sensitive information debug logging? (May show passwords in logs.)</string>
+ <!-- Checkbox label, shown only on debug screen -->
+ <string name="debug_enable_exchange_logging_label">Enable exchange parser logging? (Extremely verbose)</string>
+ <!-- Checkbox label, shown only on debug screen -->
+ <string name="debug_enable_exchange_file_logging_label">Enable exchange sd card logging?</string>
+
+ <!-- The text in the small separator between smart folders and the accounts -->
+ <string name="account_folder_list_separator_accounts">Accounts</string>
+ <!-- The summary section entry in the AccountFolder list to display all inboxes -->
+ <string name="account_folder_list_summary_inbox">Combined Inbox</string>
+ <!-- The summary section entry in the AccountFolder list to display all unread -->
+ <string name="account_folder_list_summary_unread">Unread</string>
+ <!-- The summary section entry in the AccountFolder list to display all starred -->
+ <string name="account_folder_list_summary_starred">Starred</string>
+ <!-- The summary section entry in the AccountFolder list to display all drafts -->
+ <string name="account_folder_list_summary_drafts">Drafts</string>
+ <!-- The summary section entry in the AccountFolder list to display all outboxes -->
+ <string name="account_folder_list_summary_outbox">Outbox</string>
+
+ <!-- Title of the screen that shows a list of mailboxes for an account -->
+ <string name="mailbox_list_title">Mailbox</string>
<!-- Appears at the bottom of list of messages; user selects to load more messages from that folder. -->
<string name="message_list_load_more_messages_action">Load more messages</string>
<!-- Appears at the bottom of list of messages of outbox;
user selects to send pending messages. -->
<string name="message_list_send_pending_messages_action">Send outgoing messages</string>
+ <!-- Appears at the title bar of list of messages. -->
+ <string name="message_list_title"><xliff:g id="account">%s</xliff:g>: <xliff:g id="mailbox">%s</xliff:g></string>
<!-- Hint text in To field -->
<string name="message_compose_to_hint">To</string>
<!-- Hint text in Cc field -->
@@ -172,16 +249,19 @@
<string name="message_compose_quoted_text_label">Quoted text</string>
<!-- Toast that appears if you try to send with no recipients. -->
<string name="message_compose_error_no_recipients">You must add at least one recipient.</string>
+ <!-- An address field contains invalid email addresses. -->
+ <string name="message_compose_error_invalid_email">Some email addresses are invalid.</string>
<!-- Toast that appears in the context of forwarding a message with attachment(s) -->
<string name="message_compose_attachments_skipped_toast">Some attachments cannot be forwarded because they have not downloaded.</string>
<!-- Toast that appears when an attachment is too big to send. -->
<string name="message_compose_attachment_size">File too large to attach.</string>
+ <!-- Display name for composed message, indicating the destination of the message.
+ e.g. "John and 2 others" -->
+ <string name="message_compose_display_name"><xliff:g id="name" example="John">%1$s</xliff:g> and <xliff:g id="number" example="27">%2$d</xliff:g> others</string>
<!-- Label for To field in read message view -->
<string name="message_view_to_label">To:</string>
<!-- Label for CC field in read message view -->
<string name="message_view_cc_label">Cc:</string>
- <!-- Label for BCC field in read message view -->
- <string name="message_view_bcc_label">Bcc:</string>
<!-- Menu item -->
<string name="message_view_attachment_view_action">Open</string>
<!-- Button name -->
@@ -196,8 +276,6 @@
<string name="message_view_show_pictures_action">Show pictures</string>
<!-- Toast while fetching attachment -->
<string name="message_view_fetching_attachment_toast">Fetching attachment.</string>
- <!-- Appears progress dialog for fetching pictures -->
- <string name="message_view_fetching_pictures_toast">Fetching images\u2026</string>
<!-- Appears progress dialog for fetching attachment -->
<string name="message_view_fetching_attachment_progress">Fetching attachment <xliff:g id="filename">%s</xliff:g></string>
<!-- Toast shown briefly while deleting a message -->
@@ -217,8 +295,15 @@
<!-- Title of screen when setting up new email account -->
<string name="account_setup_basics_title">Set up email</string>
+ <!-- On "Set up email" screen, enthusiastic welcome message. -->
+ <string name="accounts_welcome">You can configure Email for most accounts in just a few steps.
+ </string>
+ <!-- On "Set up email" screen, enthusiastic welcome message (in EAS mode). -->
+ <string name="accounts_welcome_exchange">You can configure an Exchange account in just a few
+ steps.</string>
<!-- On "Set up email" screen, brief instructions -->
- <string name="account_setup_basics_instructions">Type your account email address:</string>
+ <!-- DEPRECATED - REMOVE -->
+ <string name="account_setup_basics_instructions"></string>
<!-- On "Set up email" screen, hint for account email address text field -->
<string name="account_setup_basics_email_hint">Email address</string>
<!-- On "Set up email" screen, hint for account email password text field -->
@@ -230,6 +315,13 @@
<!-- Toast when we can't build a URI from the given email & password -->
<!-- Note, the error message in the toast is purposefully vague, because I *don't* know exactly what's wrong. -->
<string name="account_setup_username_password_toast">Please type a valid email address and password.</string>
+ <!-- Title of dialog shown when a duplicate account is created -->
+ <string name="account_duplicate_dlg_title">Duplicate Account</string>
+ <!-- Message of dialog shown when a duplicate account is created. The display name of
+ the duplicate account is displayed. -->
+ <string name="account_duplicate_dlg_message_fmt">
+ This login is already in use for the account \"<xliff:g id="duplicate">%s</xliff:g>\".
+ </string>
<!-- Do Not Translate. Activity Title for check-settings screen -->
<string name="account_setup_check_settings_title"></string>
@@ -259,7 +351,7 @@
<!-- "Add new email account" screen, button name in response to what type of account this is -->
<string name="account_setup_account_type_imap_action">IMAP account</string>
<!-- "Add new email account" screen, button name in response to what type of account this is -->
- <string name="account_setup_account_type_exchange_action">Exchange/ActiveSync</string>
+ <string name="account_setup_account_type_exchange_action">Exchange account</string>
<!-- "Incoming server settings" screen, label for text field -->
<string name="account_setup_incoming_title">Incoming server settings</string>
<!-- "Incoming server settings" screen, label for text field -->
@@ -277,21 +369,19 @@
<!-- "Incoming server settings" screen, options for "Security type" pop-up menu -->
<string name="account_setup_incoming_security_none_label">None</string>
<!-- "Incoming server settings" screen, options for "Security type" pop-up menu -->
- <string name="account_setup_incoming_security_ssl_optional_label">SSL (if available)</string>
+ <string name="account_setup_incoming_security_ssl_trust_certificates_label">SSL (Accept all certificates)</string>
<!-- "Incoming server settings" screen, options for "Security type" pop-up menu -->
- <string name="account_setup_incoming_security_ssl_label">SSL (always)</string>
+ <string name="account_setup_incoming_security_ssl_label">SSL</string>
<!-- "Incoming server settings" screen, options for "Security type" pop-up menu -->
- <string name="account_setup_incoming_security_tls_optional_label">TLS (if available)</string>
+ <string name="account_setup_incoming_security_tls_trust_certificates_label">TLS (Accept all certificates)</string>
<!-- "Incoming server settings" screen, options for "Security type" pop-up menu -->
- <string name="account_setup_incoming_security_tls_label">TLS (always)</string>
+ <string name="account_setup_incoming_security_tls_label">TLS</string>
<!-- "Incoming server settings" screen, label for pop-up menu -->
<string name="account_setup_incoming_delete_policy_label">Delete email from server</string>
<!-- "Incoming server settings" screen, options in pop-up menu for Delete email from server: -->
<!-- "Incoming server settings" screen, options in pop-up menu for Delete email from server: -->
<string name="account_setup_incoming_delete_policy_never_label">Never</string>
<!-- "Incoming server settings" screen, options in pop-up menu for Delete email from server: -->
- <string name="account_setup_incoming_delete_policy_7days_label">After 7 days</string>
- <!-- "Incoming server settings" screen, options in pop-up menu for Delete email from server: -->
<string name="account_setup_incoming_delete_policy_delete_label">When I delete from Inbox</string>
<!-- "Incoming server settings" screen, label for setting IMAP path prefix: -->
@@ -317,12 +407,16 @@
<string name="account_setup_exchange_title">Exchange server settings</string>
<!-- On "Exchange" setup screen, the name of the server -->
<string name="account_setup_exchange_server_label">Exchange Server</string>
- <!-- On "Exchange" setup screen, the domain name label -->
+ <!-- On "Exchange" setup screen, the domain\\username -->
+ <string name="account_setup_exchange_username_label">Domain\\Username</string>
+ <!-- Deprecated: On "Exchange" setup screen, the domain name label -->
<string name="account_setup_exchange_domain_label">Domain</string>
- <!-- On "Exchange" setup screen, the domain name hint -->
+ <!-- Deprecated: On "Exchange" setup screen, the domain name hint -->
<string name="account_setup_exchange_domain_hint">Enter domain here</string>
<!-- On "Exchange" setup screen, the use-SSL checkbox label -->
<string name="account_setup_exchange_ssl_label">Use secure connection (SSL)</string>
+ <!-- On "Exchange" setup screen, the trust ssl certificates checkbox label -->
+ <string name="account_setup_exchange_trust_certificates_label">Accept all SSL certificates</string>
<!-- In Account setup options screen, Activity title -->
<string name="account_setup_options_title">Account options</string>
@@ -347,6 +441,8 @@
<string name="account_setup_options_default_label">Send email from this account by default.</string>
<!-- In Account setup options & Account Settings screens, check box for new-mail notification -->
<string name="account_setup_options_notify_label">Notify me when email arrives.</string>
+ <!-- In Account setup options screen, optional check box to also sync contacts -->
+ <string name="account_setup_options_sync_contacts_label">Sync contacts from this account.</string>
<!-- Dialog title when "setup" could not finish -->
<string name="account_setup_failed_dlg_title">Setup could not finish</string>
<!-- In Account setup options screen, label for email check frequency selector -->
@@ -399,6 +495,8 @@
<string name="account_settings_default_summary">Send email from this account by default</string>
<!-- On Settings screen, setting option name -->
<string name="account_settings_notify_label">Email notifications</string>
+ <!-- On Settings screen, summary line when called via AccountManager for Exchange accounts -->
+ <string name="account_settings_exchange_summary">Sync frequency, notifications, etc.</string>
<!-- On Settings screen, setting summary text -->
<string name="account_settings_notify_summary">Notify in status bar when email arrives</string>
<!-- On Settings screen, setting option name and title of dialog box that opens -->
@@ -415,6 +513,12 @@
<string name="account_settings_name_label">Your name</string>
<!-- On Settings screen, section heading -->
<string name="account_settings_notifications">Notification settings</string>
+
+ <!-- On settings screen, sync contacts check box label -->
+ <string name="account_settings_sync_contacts_enable">Sync contacts</string>
+ <!-- On settings screen, sync contacts summary text -->
+ <string name="account_settings_sync_contacts_summary">Also sync contacts from this account</string>
+
<!-- On Settings screen, setting check box label -->
<string name="account_settings_vibrate_enable">Vibrate</string>
<!-- On Settings screen, setting summary text -->
@@ -443,4 +547,10 @@
these mail accounts.</string>
<!-- Message that appears when adding a T-Online account -->
<string name="provider_note_t_online">Before setting up this email account, please visit the T-Online Web site and create a password for POP3 email access.</string>
+
+ <!-- Name of Microsoft Exchange account type; used by AccountManager -->
+ <string name="exchange_name">Corporate</string>
+
+ <!-- Message that appears if the AccountManager cannot create the system Account -->
+ <string name="system_account_create_failed">The AccountManager could not create the Account; please try again.</string>
</resources>
diff --git a/res/xml/authenticator.xml b/res/xml/authenticator.xml
new file mode 100644
index 0000000..ae82c17
--- /dev/null
+++ b/res/xml/authenticator.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2009, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<!-- The attributes in this XML file provide configuration information -->
+<!-- for the Account Manager. -->
+
+<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:accountType="com.android.exchange"
+ android:icon="@drawable/ic_exchange_selected"
+ android:smallIcon="@drawable/ic_exchange_minitab_selected"
+ android:label="@string/exchange_name"
+ android:accountPreferences="@xml/account_preferences"
+/>
diff --git a/res/xml/syncadapter_contacts.xml b/res/xml/syncadapter_contacts.xml
new file mode 100644
index 0000000..4691aee
--- /dev/null
+++ b/res/xml/syncadapter_contacts.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2009, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<!-- The attributes in this XML file provide configuration information -->
+<!-- for the SyncAdapter. -->
+
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+ android:contentAuthority="com.android.contacts"
+ android:accountType="com.android.exchange"
+/>
diff --git a/src/com/android/exchange/AbstractSyncService.java b/src/com/android/exchange/AbstractSyncService.java
new file mode 100644
index 0000000..425424d
--- /dev/null
+++ b/src/com/android/exchange/AbstractSyncService.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to 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.android.exchange;
+
+import com.android.email.mail.MessagingException;
+import com.android.email.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.exchange.utility.FileLogger;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.net.NetworkInfo.DetailedState;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * Base class for all protocol services SyncManager (extends Service, implements
+ * Runnable) instantiates subclasses to run a sync (either timed, or push, or
+ * mail placed in outbox, etc.) EasSyncService is currently implemented; my goal
+ * would be to move IMAP to this structure when it comes time to introduce push
+ * functionality.
+ */
+public abstract class AbstractSyncService implements Runnable {
+
+ public String TAG = "AbstractSyncService";
+
+ public static final String SUMMARY_PROTOCOL = "_SUMMARY_";
+ public static final String SYNCED_PROTOCOL = "_SYNCING_";
+ public static final String MOVE_FAVORITES_PROTOCOL = "_MOVE_FAVORITES_";
+ public static final int SECONDS = 1000;
+ public static final int MINUTES = 60*SECONDS;
+ public static final int HOURS = 60*MINUTES;
+ public static final int DAYS = 24*HOURS;
+
+ public static final int CONNECT_TIMEOUT = 30*SECONDS;
+ public static final int NETWORK_WAIT = 15*SECONDS;
+
+ public static final String IMAP_PROTOCOL = "imap";
+ public static final String EAS_PROTOCOL = "eas";
+ public static final int EXIT_DONE = 0;
+ public static final int EXIT_IO_ERROR = 1;
+ public static final int EXIT_LOGIN_FAILURE = 2;
+ public static final int EXIT_EXCEPTION = 3;
+
+ public Mailbox mMailbox;
+ protected long mMailboxId;
+ protected Thread mThread;
+ protected int mExitStatus = EXIT_EXCEPTION;
+ protected String mMailboxName;
+ public Account mAccount;
+ public Context mContext;
+ public int mChangeCount = 0;
+ public int mSyncReason = 0;
+ protected volatile boolean mStop = false;
+ protected Object mSynchronizer = new Object();
+
+ protected volatile long mRequestTime = 0;
+ protected ArrayList<PartRequest> mPartRequests = new ArrayList<PartRequest>();
+ protected PartRequest mPendingPartRequest = null;
+
+ /**
+ * Sent by SyncManager to request that the service stop itself cleanly
+ */
+ public abstract void stop();
+
+ /**
+ * Sent by SyncManager to indicate a user request requiring service has been
+ * added to the service's pending request queue
+ */
+ public abstract void ping();
+
+ /**
+ * Called to validate an account; abstract to allow each protocol to do what
+ * is necessary. For consistency with the Email app's original
+ * functionality, success is indicated by a failure to throw an Exception
+ * (ugh). Parameters are self-explanatory
+ *
+ * @param host
+ * @param userName
+ * @param password
+ * @param port
+ * @param ssl
+ * @param context
+ * @throws MessagingException
+ */
+ public abstract void validateAccount(String host, String userName, String password, int port,
+ boolean ssl, boolean trustCertificates, Context context) throws MessagingException;
+
+ public AbstractSyncService(Context _context, Mailbox _mailbox) {
+ mContext = _context;
+ mMailbox = _mailbox;
+ mMailboxId = _mailbox.mId;
+ mMailboxName = _mailbox.mServerId;
+ mAccount = Account.restoreAccountWithId(_context, _mailbox.mAccountKey);
+ }
+
+ // Will be required when subclasses are instantiated by name
+ public AbstractSyncService(String prefix) {
+ }
+
+ /**
+ * The UI can call this static method to perform account validation. This method wraps each
+ * protocol's validateAccount method. Arguments are self-explanatory, except where noted.
+ *
+ * @param klass the protocol class (EasSyncService.class for example)
+ * @param host
+ * @param userName
+ * @param password
+ * @param port
+ * @param ssl
+ * @param context
+ * @throws MessagingException
+ */
+ static public void validate(Class<? extends AbstractSyncService> klass, String host,
+ String userName, String password, int port, boolean ssl, boolean trustCertificates,
+ Context context)
+ throws MessagingException {
+ AbstractSyncService svc;
+ try {
+ svc = klass.newInstance();
+ svc.validateAccount(host, userName, password, port, ssl, trustCertificates, context);
+ } catch (IllegalAccessException e) {
+ throw new MessagingException("internal error", e);
+ } catch (InstantiationException e) {
+ throw new MessagingException("internal error", e);
+ }
+ }
+
+ public static class ValidationResult {
+ static final int NO_FAILURE = 0;
+ static final int CONNECTION_FAILURE = 1;
+ static final int VALIDATION_FAILURE = 2;
+ static final int EXCEPTION = 3;
+
+ static final ValidationResult succeeded = new ValidationResult(true, NO_FAILURE, null);
+ boolean success;
+ int failure = NO_FAILURE;
+ String reason = null;
+ Exception exception = null;
+
+ ValidationResult(boolean _success, int _failure, String _reason) {
+ success = _success;
+ failure = _failure;
+ reason = _reason;
+ }
+
+ ValidationResult(boolean _success) {
+ success = _success;
+ }
+
+ ValidationResult(Exception e) {
+ success = false;
+ failure = EXCEPTION;
+ exception = e;
+ }
+
+ public boolean isSuccess() {
+ return success;
+ }
+
+ public String getReason() {
+ return reason;
+ }
+ }
+
+ public boolean isStopped() {
+ return mStop;
+ }
+
+ public Object getSynchronizer() {
+ return mSynchronizer;
+ }
+
+ /**
+ * Convenience methods to do user logging (i.e. connection activity). Saves a bunch of
+ * repetitive code.
+ */
+ public void userLog(String string, int code, String string2) {
+ if (Eas.USER_LOG) {
+ userLog(string + code + string2);
+ }
+ }
+
+ public void userLog(String string, int code) {
+ if (Eas.USER_LOG) {
+ userLog(string + code);
+ }
+ }
+
+ public void userLog(String str, Exception e) {
+ if (Eas.USER_LOG) {
+ Log.e(TAG, str, e);
+ } else {
+ Log.e(TAG, str + e);
+ }
+ if (Eas.FILE_LOG) {
+ FileLogger.log(e);
+ }
+ }
+
+ /**
+ * Standard logging for EAS.
+ * If user logging is active, we concatenate any arguments and log them using Log.d
+ * We also check for file logging, and log appropriately
+ * @param strings strings to concatenate and log
+ */
+ public void userLog(String ...strings) {
+ if (Eas.USER_LOG) {
+ String logText;
+ if (strings.length == 1) {
+ logText = strings[0];
+ } else {
+ StringBuilder sb = new StringBuilder(64);
+ for (String string: strings) {
+ sb.append(string);
+ }
+ logText = sb.toString();
+ }
+ Log.d(TAG, logText);
+ if (Eas.FILE_LOG) {
+ FileLogger.log(TAG, logText);
+ }
+ }
+ }
+
+ /**
+ * Error log is used for serious issues that should always be logged
+ * @param str the string to log
+ */
+ public void errorLog(String str) {
+ Log.e(TAG, str);
+ if (Eas.FILE_LOG) {
+ FileLogger.log(TAG, str);
+ }
+ }
+
+ /**
+ * Waits for up to 10 seconds for network connectivity; returns whether or not there is
+ * network connectivity.
+ *
+ * @return whether there is network connectivity
+ */
+ public boolean hasConnectivity() {
+ ConnectivityManager cm =
+ (ConnectivityManager)mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ int tries = 0;
+ while (tries++ < 1) {
+ NetworkInfo info = cm.getActiveNetworkInfo();
+ if (info != null && info.isConnected()) {
+ DetailedState state = info.getDetailedState();
+ if (state == DetailedState.CONNECTED) {
+ return true;
+ }
+ }
+ try {
+ Thread.sleep(10*SECONDS);
+ } catch (InterruptedException e) {
+ }
+ }
+ return false;
+ }
+
+ /**
+ * PartRequest handling (common functionality)
+ * Can be overridden if desired, but IMAP/EAS both use the next three methods as-is
+ */
+
+ public void addPartRequest(PartRequest req) {
+ synchronized (mPartRequests) {
+ mPartRequests.add(req);
+ mRequestTime = System.currentTimeMillis();
+ }
+ }
+
+ public void removePartRequest(PartRequest req) {
+ synchronized (mPartRequests) {
+ mPartRequests.remove(req);
+ }
+ }
+
+ public PartRequest hasPartRequest(long emailId, String part) {
+ synchronized (mPartRequests) {
+ for (PartRequest pr : mPartRequests) {
+ if (pr.emailId == emailId && pr.loc.equals(part))
+ return pr;
+ }
+ }
+ return null;
+ }
+
+ // cancelPartRequest is sent in response to user input to stop an attachment load
+ // that is in progress. This will almost certainly require code overriding the base
+ // functionality, as sockets may need to be closed, etc. and this functionality will be
+ // service dependent. This returns the canceled PartRequest or null
+ public PartRequest cancelPartRequest(long emailId, String part) {
+ synchronized (mPartRequests) {
+ PartRequest p = null;
+ for (PartRequest pr : mPartRequests) {
+ if (pr.emailId == emailId && pr.loc.equals(part)) {
+ p = pr;
+ break;
+ }
+ }
+ if (p != null) {
+ mPartRequests.remove(p);
+ return p;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Convenience method wrapping calls to retrieve columns from a single row, via EmailProvider.
+ * The arguments are exactly the same as to contentResolver.query(). Results are returned in
+ * an array of Strings corresponding to the columns in the projection.
+ */
+ protected String[] getRowColumns(Uri contentUri, String[] projection, String selection,
+ String[] selectionArgs) {
+ String[] values = new String[projection.length];
+ ContentResolver cr = mContext.getContentResolver();
+ Cursor c = cr.query(contentUri, projection, selection, selectionArgs, null);
+ try {
+ if (c.moveToFirst()) {
+ for (int i = 0; i < projection.length; i++) {
+ values[i] = c.getString(i);
+ }
+ } else {
+ return null;
+ }
+ } finally {
+ c.close();
+ }
+ return values;
+ }
+
+ /**
+ * Convenience method for retrieving columns from a particular row in EmailProvider.
+ * Passed in here are a base uri (e.g. Message.CONTENT_URI), the unique id of a row, and
+ * a projection. This method calls the previous one with the appropriate URI.
+ */
+ protected String[] getRowColumns(Uri baseUri, long id, String ... projection) {
+ return getRowColumns(ContentUris.withAppendedId(baseUri, id), projection, null, null);
+ }
+}
diff --git a/src/com/android/exchange/BootReceiver.java b/src/com/android/exchange/BootReceiver.java
new file mode 100644
index 0000000..5312bfb
--- /dev/null
+++ b/src/com/android/exchange/BootReceiver.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2009 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.android.exchange;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+public class BootReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.d("Exchange", "BootReceiver onReceive");
+ context.startService(new Intent(context, SyncManager.class));
+ }
+}
diff --git a/src/com/android/exchange/ContactsSyncAdapterService.java b/src/com/android/exchange/ContactsSyncAdapterService.java
new file mode 100644
index 0000000..69fe792
--- /dev/null
+++ b/src/com/android/exchange/ContactsSyncAdapterService.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2009 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.android.exchange;
+
+import com.android.email.provider.EmailContent;
+import com.android.email.provider.EmailContent.AccountColumns;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.email.provider.EmailContent.MailboxColumns;
+
+import android.accounts.Account;
+import android.accounts.OperationCanceledException;
+import android.app.Service;
+import android.content.AbstractThreadedSyncAdapter;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SyncResult;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
+
+public class ContactsSyncAdapterService extends Service {
+ private static final String TAG = "EAS ContactsSyncAdapterService";
+ private static SyncAdapterImpl sSyncAdapter = null;
+ private static final Object sSyncAdapterLock = new Object();
+
+ private static final String[] ID_PROJECTION = new String[] {EmailContent.RECORD_ID};
+ private static final String ACCOUNT_AND_TYPE_CONTACTS =
+ MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.TYPE + '=' + Mailbox.TYPE_CONTACTS;
+
+ public ContactsSyncAdapterService() {
+ super();
+ }
+
+ private static class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
+ private Context mContext;
+
+ public SyncAdapterImpl(Context context) {
+ super(context, true /* autoInitialize */);
+ mContext = context;
+ }
+
+ @Override
+ public void onPerformSync(Account account, Bundle extras,
+ String authority, ContentProviderClient provider, SyncResult syncResult) {
+ try {
+ ContactsSyncAdapterService.performSync(mContext, account, extras,
+ authority, provider, syncResult);
+ } catch (OperationCanceledException e) {
+ }
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ synchronized (sSyncAdapterLock) {
+ if (sSyncAdapter == null) {
+ sSyncAdapter = new SyncAdapterImpl(getApplicationContext());
+ }
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return sSyncAdapter.getSyncAdapterBinder();
+ }
+
+ /**
+ * Partial integration with system SyncManager; we tell our EAS SyncManager to start a contacts
+ * sync when we get the signal from the system SyncManager.
+ * The missing piece at this point is integration with the push/ping mechanism in EAS; this will
+ * be put in place at a later time.
+ */
+ private static void performSync(Context context, Account account, Bundle extras,
+ String authority, ContentProviderClient provider, SyncResult syncResult)
+ throws OperationCanceledException {
+ ContentResolver cr = context.getContentResolver();
+ Log.i(TAG, "performSync");
+ if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD)) {
+ Uri uri = RawContacts.CONTENT_URI.buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name)
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE)
+ .build();
+ Cursor c = cr.query(uri,
+ new String[] {RawContacts._ID}, RawContacts.DIRTY + "=1", null, null);
+ try {
+ if (!c.moveToFirst()) {
+ Log.i(TAG, "Upload sync; no changes");
+ return;
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ // Find the (EmailProvider) account associated with this email address
+ Cursor accountCursor =
+ cr.query(com.android.email.provider.EmailContent.Account.CONTENT_URI, ID_PROJECTION,
+ AccountColumns.EMAIL_ADDRESS + "=?", new String[] {account.name}, null);
+ try {
+ if (accountCursor.moveToFirst()) {
+ long accountId = accountCursor.getLong(0);
+ // Now, find the contacts mailbox associated with the account
+ Cursor mailboxCursor = cr.query(Mailbox.CONTENT_URI, ID_PROJECTION,
+ ACCOUNT_AND_TYPE_CONTACTS, new String[] {Long.toString(accountId)}, null);
+ try {
+ if (mailboxCursor.moveToFirst()) {
+ Log.i(TAG, "Contact sync requested for " + account.name);
+ // Ask for a sync from our sync manager
+ SyncManager.serviceRequest(mailboxCursor.getLong(0),
+ SyncManager.SYNC_UPSYNC);
+ }
+ } finally {
+ mailboxCursor.close();
+ }
+ }
+ } finally {
+ accountCursor.close();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/exchange/Eas.java b/src/com/android/exchange/Eas.java
new file mode 100644
index 0000000..c005544
--- /dev/null
+++ b/src/com/android/exchange/Eas.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to 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.android.exchange;
+
+import android.util.Log;
+
+/**
+ * Constants used throughout the EAS implementation are stored here.
+ *
+ */
+public class Eas {
+ // For debugging
+ public static boolean WAIT_DEBUG = false; // DO NOT CHECK IN WITH THIS SET TO TRUE
+ public static boolean DEBUG = false; // DO NOT CHECK IN WITH THIS SET TO TRUE
+
+ // The following two are for user logging (the second providing more detail)
+ public static boolean USER_LOG = false; // DO NOT CHECK IN WITH THIS SET TO TRUE
+ public static boolean PARSER_LOG = false; // DO NOT CHECK IN WITH THIS SET TO TRUE
+ public static boolean FILE_LOG = false; // DO NOT CHECK IN WITH THIS SET TO TRUE
+
+ public static final int DEBUG_BIT = 1;
+ public static final int DEBUG_EXCHANGE_BIT = 2;
+ public static final int DEBUG_FILE_BIT = 4;
+
+ public static final String VERSION = "0.3";
+ public static final String ACCOUNT_MANAGER_TYPE = "com.android.exchange";
+ public static final String ACCOUNT_MAILBOX = "__eas";
+
+ // From EAS spec
+ // Mail Cal
+ // 0 No filter Yes Yes
+ // 1 1 day ago Yes No
+ // 2 3 days ago Yes No
+ // 3 1 week ago Yes No
+ // 4 2 weeks ago Yes Yes
+ // 5 1 month ago Yes Yes
+ // 6 3 months ago No Yes
+ // 7 6 months ago No Yes
+
+ public static final String FILTER_ALL = "0";
+ public static final String FILTER_1_DAY = "1";
+ public static final String FILTER_3_DAYS = "2";
+ public static final String FILTER_1_WEEK = "3";
+ public static final String FILTER_2_WEEKS = "4";
+ public static final String FILTER_1_MONTH = "5";
+ public static final String FILTER_3_MONTHS = "6";
+ public static final String FILTER_6_MONTHS = "7";
+ public static final String BODY_PREFERENCE_TEXT = "1";
+ public static final String BODY_PREFERENCE_HTML = "2";
+
+ // For EAS 12, we use HTML, so we want a larger size than in EAS 2.5
+ public static final String EAS12_TRUNCATION_SIZE = "200000";
+ // For EAS 2.5, truncation is a code; the largest is "7", which is 100k
+ public static final String EAS2_5_TRUNCATION_SIZE = "7";
+
+ public static final int FOLDER_STATUS_OK = 1;
+ public static final int FOLDER_STATUS_INVALID_KEY = 9;
+
+ public static final int EXCHANGE_ERROR_NOTIFICATION = 0x10;
+
+ public static void setUserDebug(int state) {
+ // DEBUG takes precedence and is never true in a user build
+ if (!DEBUG) {
+ USER_LOG = (state & DEBUG_BIT) != 0;
+ PARSER_LOG = (state & DEBUG_EXCHANGE_BIT) != 0;
+ FILE_LOG = (state & DEBUG_FILE_BIT) != 0;
+ if (FILE_LOG || PARSER_LOG) {
+ USER_LOG = true;
+ }
+ Log.d("Eas Debug", "Logging: " + (USER_LOG ? "User " : "") +
+ (PARSER_LOG ? "Parser " : "") + (FILE_LOG ? "File" : ""));
+ }
+ }
+}
diff --git a/src/com/android/exchange/EasException.java b/src/com/android/exchange/EasException.java
new file mode 100644
index 0000000..e7613d7
--- /dev/null
+++ b/src/com/android/exchange/EasException.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to 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.android.exchange;
+
+public class EasException extends Exception {
+ private static final long serialVersionUID = 5894556952470989968L;
+}
diff --git a/src/com/android/exchange/EasOutboxService.java b/src/com/android/exchange/EasOutboxService.java
new file mode 100644
index 0000000..4e1e61c
--- /dev/null
+++ b/src/com/android/exchange/EasOutboxService.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to 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.android.exchange;
+
+import com.android.email.mail.MessagingException;
+import com.android.email.mail.transport.Rfc822Output;
+import com.android.email.provider.EmailContent.Body;
+import com.android.email.provider.EmailContent.BodyColumns;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.email.provider.EmailContent.MailboxColumns;
+import com.android.email.provider.EmailContent.Message;
+import com.android.email.provider.EmailContent.MessageColumns;
+import com.android.email.provider.EmailContent.SyncColumns;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.entity.InputStreamEntity;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.os.RemoteException;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+public class EasOutboxService extends EasSyncService {
+
+ public static final int SEND_FAILED = 1;
+ public static final String MAILBOX_KEY_AND_NOT_SEND_FAILED =
+ MessageColumns.MAILBOX_KEY + "=? and (" + SyncColumns.SERVER_ID + " is null or " +
+ SyncColumns.SERVER_ID + "!=" + SEND_FAILED + ')';
+ public static final String[] BODY_SOURCE_PROJECTION =
+ new String[] {BodyColumns.SOURCE_MESSAGE_KEY};
+ public static final String WHERE_MESSAGE_KEY = Body.MESSAGE_KEY + "=?";
+
+ // This needs to be long enough to send the longest reasonable message, without being so long
+ // as to effectively "hang" sending of mail. The standard 30 second timeout isn't long enough
+ // for pictures and the like. For now, we'll use 15 minutes, in the knowledge that any socket
+ // failure would probably generate an Exception before timing out anyway
+ public static final int SEND_MAIL_TIMEOUT = 15*MINUTES;
+
+ public EasOutboxService(Context _context, Mailbox _mailbox) {
+ super(_context, _mailbox);
+ }
+
+ private void sendCallback(long msgId, String subject, int status) {
+ try {
+ SyncManager.callback().sendMessageStatus(msgId, subject, status, 0);
+ } catch (RemoteException e) {
+ // It's all good
+ }
+ }
+
+ /**
+ * Send a single message via EAS
+ * Note that we mark messages SEND_FAILED when there is a permanent failure, rather than an
+ * IOException, which is handled by SyncManager with retries, backoffs, etc.
+ *
+ * @param cacheDir the cache directory for this context
+ * @param msgId the _id of the message to send
+ * @throws IOException
+ */
+ int sendMessage(File cacheDir, long msgId) throws IOException, MessagingException {
+ int result;
+ sendCallback(msgId, null, EmailServiceStatus.IN_PROGRESS);
+ File tmpFile = File.createTempFile("eas_", "tmp", cacheDir);
+ // Write the output to a temporary file
+ try {
+ String[] cols = getRowColumns(Message.CONTENT_URI, msgId, MessageColumns.FLAGS,
+ MessageColumns.SUBJECT);
+ int flags = Integer.parseInt(cols[0]);
+ String subject = cols[1];
+
+ boolean reply = (flags & Message.FLAG_TYPE_REPLY) != 0;
+ boolean forward = (flags & Message.FLAG_TYPE_FORWARD) != 0;
+ // The reference message and mailbox are called item and collection in EAS
+ String itemId = null;
+ String collectionId = null;
+ if (reply || forward) {
+ // First, we need to get the id of the reply/forward message
+ cols = getRowColumns(Body.CONTENT_URI, BODY_SOURCE_PROJECTION,
+ WHERE_MESSAGE_KEY, new String[] {Long.toString(msgId)});
+ if (cols != null) {
+ long refId = Long.parseLong(cols[0]);
+ // Then, we need the serverId and mailboxKey of the message
+ cols = getRowColumns(Message.CONTENT_URI, refId, SyncColumns.SERVER_ID,
+ MessageColumns.MAILBOX_KEY);
+ if (cols != null) {
+ itemId = cols[0];
+ long boxId = Long.parseLong(cols[1]);
+ // Then, we need the serverId of the mailbox
+ cols = getRowColumns(Mailbox.CONTENT_URI, boxId, MailboxColumns.SERVER_ID);
+ if (cols != null) {
+ collectionId = cols[0];
+ }
+ }
+ }
+ }
+
+ boolean smartSend = itemId != null && collectionId != null;
+
+ // Write the message in rfc822 format to the temporary file
+ FileOutputStream fileStream = new FileOutputStream(tmpFile);
+ Rfc822Output.writeTo(mContext, msgId, fileStream, !smartSend, true);
+ fileStream.close();
+
+ // Now, get an input stream to our temporary file and create an entity with it
+ FileInputStream inputStream = new FileInputStream(tmpFile);
+ InputStreamEntity inputEntity =
+ new InputStreamEntity(inputStream, tmpFile.length());
+
+ // Create the appropriate command and POST it to the server
+ String cmd = "SendMail&SaveInSent=T";
+ if (smartSend) {
+ cmd = reply ? "SmartReply" : "SmartForward";
+ cmd += "&ItemId=" + itemId + "&CollectionId=" + collectionId + "&SaveInSent=T";
+ }
+ userLog("Send cmd: " + cmd);
+ HttpResponse resp = sendHttpClientPost(cmd, inputEntity, SEND_MAIL_TIMEOUT);
+
+ inputStream.close();
+ int code = resp.getStatusLine().getStatusCode();
+ if (code == HttpStatus.SC_OK) {
+ userLog("Deleting message...");
+ mContentResolver.delete(ContentUris.withAppendedId(Message.CONTENT_URI, msgId),
+ null, null);
+ result = EmailServiceStatus.SUCCESS;
+ sendCallback(-1, subject, EmailServiceStatus.SUCCESS);
+ } else {
+ userLog("Message sending failed, code: " + code);
+ ContentValues cv = new ContentValues();
+ cv.put(SyncColumns.SERVER_ID, SEND_FAILED);
+ Message.update(mContext, Message.CONTENT_URI, msgId, cv);
+ result = EmailServiceStatus.REMOTE_EXCEPTION;
+ if (isAuthError(code)) {
+ result = EmailServiceStatus.LOGIN_FAILED;
+ }
+ sendCallback(msgId, null, result);
+
+ }
+ } catch (IOException e) {
+ // We catch this just to send the callback
+ sendCallback(msgId, null, EmailServiceStatus.CONNECTION_ERROR);
+ throw e;
+ } finally {
+ // Clean up the temporary file
+ if (tmpFile.exists()) {
+ tmpFile.delete();
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public void run() {
+ setupService();
+ File cacheDir = mContext.getCacheDir();
+ try {
+ mDeviceId = SyncManager.getDeviceId();
+ Cursor c = mContext.getContentResolver().query(Message.CONTENT_URI,
+ Message.ID_COLUMN_PROJECTION, MAILBOX_KEY_AND_NOT_SEND_FAILED,
+ new String[] {Long.toString(mMailbox.mId)}, null);
+ try {
+ while (c.moveToNext()) {
+ long msgId = c.getLong(0);
+ if (msgId != 0) {
+ int result = sendMessage(cacheDir, msgId);
+ // If there's an error, it should stop the service; we will distinguish
+ // at least between login failures and everything else
+ if (result == EmailServiceStatus.LOGIN_FAILED) {
+ mExitStatus = EXIT_LOGIN_FAILURE;
+ return;
+ } else if (result == EmailServiceStatus.REMOTE_EXCEPTION) {
+ mExitStatus = EXIT_EXCEPTION;
+ return;
+ }
+ }
+ }
+ } finally {
+ c.close();
+ }
+ mExitStatus = EXIT_DONE;
+ } catch (IOException e) {
+ mExitStatus = EXIT_IO_ERROR;
+ } catch (Exception e) {
+ userLog("Exception caught in EasOutboxService", e);
+ mExitStatus = EXIT_EXCEPTION;
+ } finally {
+ userLog(mMailbox.mDisplayName, ": sync finished");
+ userLog("Outbox exited with status ", mExitStatus);
+ SyncManager.done(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
new file mode 100644
index 0000000..3906024
--- /dev/null
+++ b/src/com/android/exchange/EasSyncService.java
@@ -0,0 +1,1173 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to 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.android.exchange;
+
+import com.android.email.codec.binary.Base64;
+import com.android.email.mail.AuthenticationFailedException;
+import com.android.email.mail.MessagingException;
+import com.android.email.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.AccountColumns;
+import com.android.email.provider.EmailContent.Attachment;
+import com.android.email.provider.EmailContent.AttachmentColumns;
+import com.android.email.provider.EmailContent.HostAuth;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.email.provider.EmailContent.MailboxColumns;
+import com.android.email.provider.EmailContent.Message;
+import com.android.exchange.adapter.AbstractSyncAdapter;
+import com.android.exchange.adapter.AccountSyncAdapter;
+import com.android.exchange.adapter.ContactsSyncAdapter;
+import com.android.exchange.adapter.EmailSyncAdapter;
+import com.android.exchange.adapter.FolderSyncParser;
+import com.android.exchange.adapter.PingParser;
+import com.android.exchange.adapter.Serializer;
+import com.android.exchange.adapter.Tags;
+import com.android.exchange.adapter.Parser.EasParserException;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpOptions;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.os.RemoteException;
+import android.os.SystemClock;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.security.cert.CertificateException;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class EasSyncService extends AbstractSyncService {
+ private static final String EMAIL_WINDOW_SIZE = "5";
+ public static final String PIM_WINDOW_SIZE = "5";
+ private static final String WHERE_ACCOUNT_KEY_AND_SERVER_ID =
+ MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SERVER_ID + "=?";
+ private static final String WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING =
+ MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL +
+ '=' + Mailbox.CHECK_INTERVAL_PING;
+ private static final String AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX = " AND " +
+ MailboxColumns.SYNC_INTERVAL + " IN (" + Mailbox.CHECK_INTERVAL_PING +
+ ',' + Mailbox.CHECK_INTERVAL_PUSH + ") AND " + MailboxColumns.TYPE + "!=\"" +
+ Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + '\"';
+ private static final String WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX =
+ MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.SYNC_INTERVAL +
+ '=' + Mailbox.CHECK_INTERVAL_PUSH_HOLD;
+ static private final int CHUNK_SIZE = 16*1024;
+
+ static private final String PING_COMMAND = "Ping";
+ static private final int COMMAND_TIMEOUT = 20*SECONDS;
+
+ /**
+ * We start with an 8 minute timeout, and increase/decrease by 3 minutes at a time. There's
+ * no point having a timeout shorter than 5 minutes, I think; at that point, we can just let
+ * the ping exception out. The maximum I use is 17 minutes, which is really an empirical
+ * choice; too long and we risk silent connection loss and loss of push for that period. Too
+ * short and we lose efficiency/battery life.
+ *
+ * If we ever have to drop the ping timeout, we'll never increase it again. There's no point
+ * going into hysteresis; the NAT timeout isn't going to change without a change in connection,
+ * which will cause the sync service to be restarted at the starting heartbeat and going through
+ * the process again.
+ */
+ static private final int PING_MINUTES = 60; // in seconds
+ static private final int PING_FUDGE_LOW = 10;
+ static private final int PING_STARTING_HEARTBEAT = (8*PING_MINUTES)-PING_FUDGE_LOW;
+ static private final int PING_MIN_HEARTBEAT = (5*PING_MINUTES)-PING_FUDGE_LOW;
+ static private final int PING_MAX_HEARTBEAT = (17*PING_MINUTES)-PING_FUDGE_LOW;
+ static private final int PING_HEARTBEAT_INCREMENT = 3*PING_MINUTES;
+ static private final int PING_FORCE_HEARTBEAT = 2*PING_MINUTES;
+
+ static private final int PROTOCOL_PING_STATUS_COMPLETED = 1;
+
+ // Fallbacks (in minutes) for ping loop failures
+ static private final int MAX_PING_FAILURES = 1;
+ static private final int PING_FALLBACK_INBOX = 5;
+ static private final int PING_FALLBACK_PIM = 25;
+
+ // Reasonable default
+ String mProtocolVersion = "2.5";
+ public Double mProtocolVersionDouble;
+ protected String mDeviceId = null;
+ private String mDeviceType = "Android";
+ private String mAuthString = null;
+ private String mCmdString = null;
+ public String mHostAddress;
+ public String mUserName;
+ public String mPassword;
+ private boolean mSsl = true;
+ private boolean mTrustSsl = false;
+ public ContentResolver mContentResolver;
+ private String[] mBindArguments = new String[2];
+ private ArrayList<String> mPingChangeList;
+ private HttpPost mPendingPost = null;
+ // The ping time (in seconds)
+ private int mPingHeartbeat = PING_STARTING_HEARTBEAT;
+ // The longest successful ping heartbeat
+ private int mPingHighWaterMark = 0;
+ // Whether we've ever lowered the heartbeat
+ private boolean mPingHeartbeatDropped = false;
+ // Whether a POST was aborted due to watchdog timeout
+ private boolean mAborted = false;
+
+ public EasSyncService(Context _context, Mailbox _mailbox) {
+ super(_context, _mailbox);
+ mContentResolver = _context.getContentResolver();
+ HostAuth ha = HostAuth.restoreHostAuthWithId(_context, mAccount.mHostAuthKeyRecv);
+ mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0;
+ mTrustSsl = (ha.mFlags & HostAuth.FLAG_TRUST_ALL_CERTIFICATES) != 0;
+ }
+
+ private EasSyncService(String prefix) {
+ super(prefix);
+ }
+
+ public EasSyncService() {
+ this("EAS Validation");
+ }
+
+ @Override
+ public void ping() {
+ userLog("Alarm ping received!");
+ synchronized(getSynchronizer()) {
+ if (mPendingPost != null) {
+ userLog("Aborting pending POST!");
+ mAborted = true;
+ mPendingPost.abort();
+ }
+ }
+ }
+
+ @Override
+ public void stop() {
+ mStop = true;
+ synchronized(getSynchronizer()) {
+ if (mPendingPost != null) {
+ mPendingPost.abort();
+ }
+ }
+ }
+
+ /**
+ * Determine whether an HTTP code represents an authentication error
+ * @param code the HTTP code returned by the server
+ * @return whether or not the code represents an authentication error
+ */
+ protected boolean isAuthError(int code) {
+ return ((code == HttpStatus.SC_UNAUTHORIZED) || (code == HttpStatus.SC_FORBIDDEN));
+ }
+
+ @Override
+ public void validateAccount(String hostAddress, String userName, String password, int port,
+ boolean ssl, boolean trustCertificates, Context context) throws MessagingException {
+ try {
+ userLog("Testing EAS: ", hostAddress, ", ", userName, ", ssl = ", ssl ? "1" : "0");
+ EasSyncService svc = new EasSyncService("%TestAccount%");
+ svc.mContext = context;
+ svc.mHostAddress = hostAddress;
+ svc.mUserName = userName;
+ svc.mPassword = password;
+ svc.mSsl = ssl;
+ svc.mTrustSsl = trustCertificates;
+ svc.mDeviceId = SyncManager.getDeviceId();
+ HttpResponse resp = svc.sendHttpClientOptions();
+ int code = resp.getStatusLine().getStatusCode();
+ userLog("Validation (OPTIONS) response: " + code);
+ if (code == HttpStatus.SC_OK) {
+ // No exception means successful validation
+ Header commands = resp.getFirstHeader("MS-ASProtocolCommands");
+ Header versions = resp.getFirstHeader("ms-asprotocolversions");
+ if (commands == null || versions == null) {
+ userLog("OPTIONS response without commands or versions; reporting I/O error");
+ throw new MessagingException(MessagingException.IOERROR);
+ }
+ userLog("Validation successful");
+ return;
+ }
+ if (isAuthError(code)) {
+ userLog("Authentication failed");
+ throw new AuthenticationFailedException("Validation failed");
+ } else {
+ // TODO Need to catch other kinds of errors (e.g. policy) For now, report the code.
+ userLog("Validation failed, reporting I/O error: ", code);
+ throw new MessagingException(MessagingException.IOERROR);
+ }
+ } catch (IOException e) {
+ Throwable cause = e.getCause();
+ if (cause != null && cause instanceof CertificateException) {
+ userLog("CertificateException caught: ", e.getMessage());
+ throw new MessagingException(MessagingException.GENERAL_SECURITY);
+ }
+ userLog("IOException caught: ", e.getMessage());
+ throw new MessagingException(MessagingException.IOERROR);
+ }
+
+ }
+
+ private void doStatusCallback(long messageId, long attachmentId, int status) {
+ try {
+ SyncManager.callback().loadAttachmentStatus(messageId, attachmentId, status, 0);
+ } catch (RemoteException e) {
+ // No danger if the client is no longer around
+ }
+ }
+
+ private void doProgressCallback(long messageId, long attachmentId, int progress) {
+ try {
+ SyncManager.callback().loadAttachmentStatus(messageId, attachmentId,
+ EmailServiceStatus.IN_PROGRESS, progress);
+ } catch (RemoteException e) {
+ // No danger if the client is no longer around
+ }
+ }
+
+ public File createUniqueFileInternal(String dir, String filename) {
+ File directory;
+ if (dir == null) {
+ directory = mContext.getFilesDir();
+ } else {
+ directory = new File(dir);
+ }
+ if (!directory.exists()) {
+ directory.mkdirs();
+ }
+ File file = new File(directory, filename);
+ if (!file.exists()) {
+ return file;
+ }
+ // Get the extension of the file, if any.
+ int index = filename.lastIndexOf('.');
+ String name = filename;
+ String extension = "";
+ if (index != -1) {
+ name = filename.substring(0, index);
+ extension = filename.substring(index);
+ }
+ for (int i = 2; i < Integer.MAX_VALUE; i++) {
+ file = new File(directory, name + '-' + i + extension);
+ if (!file.exists()) {
+ return file;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Loads an attachment, based on the PartRequest passed in. The PartRequest is basically our
+ * wrapper for Attachment
+ * @param req the part (attachment) to be retrieved
+ * @throws IOException
+ */
+ protected void getAttachment(PartRequest req) throws IOException {
+ Attachment att = req.att;
+ Message msg = Message.restoreMessageWithId(mContext, att.mMessageKey);
+ doProgressCallback(msg.mId, att.mId, 0);
+
+ String cmd = "GetAttachment&AttachmentName=" + att.mLocation;
+ HttpResponse res = sendHttpClientPost(cmd, null, COMMAND_TIMEOUT);
+
+ int status = res.getStatusLine().getStatusCode();
+ if (status == HttpStatus.SC_OK) {
+ HttpEntity e = res.getEntity();
+ int len = (int)e.getContentLength();
+ InputStream is = res.getEntity().getContent();
+ File f = (req.destination != null)
+ ? new File(req.destination)
+ : createUniqueFileInternal(req.destination, att.mFileName);
+ if (f != null) {
+ // Ensure that the target directory exists
+ File destDir = f.getParentFile();
+ if (!destDir.exists()) {
+ destDir.mkdirs();
+ }
+ FileOutputStream os = new FileOutputStream(f);
+ // len > 0 means that Content-Length was set in the headers
+ // len < 0 means "chunked" transfer-encoding
+ if (len != 0) {
+ try {
+ mPendingPartRequest = req;
+ byte[] bytes = new byte[CHUNK_SIZE];
+ int length = len;
+ // Loop terminates 1) when EOF is reached or 2) if an IOException occurs
+ // One of these is guaranteed to occur
+ int totalRead = 0;
+ userLog("Attachment content-length: ", len);
+ while (true) {
+ int read = is.read(bytes, 0, CHUNK_SIZE);
+
+ // read < 0 means that EOF was reached
+ if (read < 0) {
+ userLog("Attachment load reached EOF, totalRead: ", totalRead);
+ break;
+ }
+
+ // Keep track of how much we've read for progress callback
+ totalRead += read;
+
+ // Write these bytes out
+ os.write(bytes, 0, read);
+
+ // We can't report percentages if this is chunked; by definition, the
+ // length of incoming data is unknown
+ if (length > 0) {
+ // Belt and suspenders check to prevent runaway reading
+ if (totalRead > length) {
+ errorLog("totalRead is greater than attachment length?");
+ break;
+ }
+ int pct = (totalRead * 100 / length);
+ doProgressCallback(msg.mId, att.mId, pct);
+ }
+ }
+ } finally {
+ mPendingPartRequest = null;
+ }
+ }
+ os.flush();
+ os.close();
+
+ // EmailProvider will throw an exception if we try to update an unsaved attachment
+ if (att.isSaved()) {
+ String contentUriString = (req.contentUriString != null)
+ ? req.contentUriString
+ : "file://" + f.getAbsolutePath();
+ ContentValues cv = new ContentValues();
+ cv.put(AttachmentColumns.CONTENT_URI, contentUriString);
+ att.update(mContext, cv);
+ doStatusCallback(msg.mId, att.mId, EmailServiceStatus.SUCCESS);
+ }
+ }
+ } else {
+ doStatusCallback(msg.mId, att.mId, EmailServiceStatus.MESSAGE_NOT_FOUND);
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private String makeUriString(String cmd, String extra) throws IOException {
+ // Cache the authentication string and the command string
+ String safeUserName = URLEncoder.encode(mUserName);
+ if (mAuthString == null) {
+ String cs = mUserName + ':' + mPassword;
+ mAuthString = "Basic " + new String(Base64.encodeBase64(cs.getBytes()));
+ mCmdString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId + "&DeviceType="
+ + mDeviceType;
+ }
+ String us = (mSsl ? (mTrustSsl ? "httpts" : "https") : "http") + "://" + mHostAddress +
+ "/Microsoft-Server-ActiveSync";
+ if (cmd != null) {
+ us += "?Cmd=" + cmd + mCmdString;
+ }
+ if (extra != null) {
+ us += extra;
+ }
+ return us;
+ }
+
+ private void setHeaders(HttpRequestBase method) {
+ method.setHeader("Authorization", mAuthString);
+ method.setHeader("MS-ASProtocolVersion", mProtocolVersion);
+ method.setHeader("Connection", "keep-alive");
+ method.setHeader("User-Agent", mDeviceType + '/' + Eas.VERSION);
+ }
+
+ private ClientConnectionManager getClientConnectionManager() {
+ return SyncManager.getClientConnectionManager();
+ }
+
+ private HttpClient getHttpClient(int timeout) {
+ HttpParams params = new BasicHttpParams();
+ HttpConnectionParams.setConnectionTimeout(params, 15*SECONDS);
+ HttpConnectionParams.setSoTimeout(params, timeout);
+ HttpConnectionParams.setSocketBufferSize(params, 8192);
+ HttpClient client = new DefaultHttpClient(getClientConnectionManager(), params);
+ return client;
+ }
+
+ protected HttpResponse sendHttpClientPost(String cmd, byte[] bytes) throws IOException {
+ return sendHttpClientPost(cmd, new ByteArrayEntity(bytes), COMMAND_TIMEOUT);
+ }
+
+ protected HttpResponse sendHttpClientPost(String cmd, HttpEntity entity) throws IOException {
+ return sendHttpClientPost(cmd, entity, COMMAND_TIMEOUT);
+ }
+
+ protected HttpResponse sendPing(byte[] bytes, int heartbeat) throws IOException {
+ Thread.currentThread().setName(mAccount.mDisplayName + ": Ping");
+ if (Eas.USER_LOG) {
+ userLog("Send ping, timeout: " + heartbeat + "s, high: " + mPingHighWaterMark + 's');
+ }
+ return sendHttpClientPost(PING_COMMAND, new ByteArrayEntity(bytes), (heartbeat+5)*SECONDS);
+ }
+
+ protected HttpResponse sendHttpClientPost(String cmd, HttpEntity entity, int timeout)
+ throws IOException {
+ HttpClient client = getHttpClient(timeout);
+ boolean sleepAllowed = cmd.equals(PING_COMMAND);
+
+ // Split the mail sending commands
+ String extra = null;
+ boolean msg = false;
+ if (cmd.startsWith("SmartForward&") || cmd.startsWith("SmartReply&")) {
+ int cmdLength = cmd.indexOf('&');
+ extra = cmd.substring(cmdLength);
+ cmd = cmd.substring(0, cmdLength);
+ msg = true;
+ } else if (cmd.startsWith("SendMail&")) {
+ msg = true;
+ }
+
+ String us = makeUriString(cmd, extra);
+ HttpPost method = new HttpPost(URI.create(us));
+ // Send the proper Content-Type header
+ // If entity is null (e.g. for attachments), don't set this header
+ if (msg) {
+ method.setHeader("Content-Type", "message/rfc822");
+ } else if (entity != null) {
+ method.setHeader("Content-Type", "application/vnd.ms-sync.wbxml");
+ }
+ setHeaders(method);
+ method.setEntity(entity);
+ synchronized(getSynchronizer()) {
+ mPendingPost = method;
+ if (sleepAllowed) {
+ SyncManager.runAsleep(mMailboxId, timeout+(10*SECONDS));
+ }
+ }
+ try {
+ return client.execute(method);
+ } finally {
+ synchronized(getSynchronizer()) {
+ if (sleepAllowed) {
+ SyncManager.runAwake(mMailboxId);
+ }
+ mPendingPost = null;
+ }
+ }
+ }
+
+ protected HttpResponse sendHttpClientOptions() throws IOException {
+ HttpClient client = getHttpClient(COMMAND_TIMEOUT);
+ String us = makeUriString("OPTIONS", null);
+ HttpOptions method = new HttpOptions(URI.create(us));
+ setHeaders(method);
+ return client.execute(method);
+ }
+
+ String getTargetCollectionClassFromCursor(Cursor c) {
+ int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
+ if (type == Mailbox.TYPE_CONTACTS) {
+ return "Contacts";
+ } else if (type == Mailbox.TYPE_CALENDAR) {
+ return "Calendar";
+ } else {
+ return "Email";
+ }
+ }
+
+ /**
+ * Performs FolderSync
+ *
+ * @throws IOException
+ * @throws EasParserException
+ */
+ public void runAccountMailbox() throws IOException, EasParserException {
+ // Initialize exit status to success
+ mExitStatus = EmailServiceStatus.SUCCESS;
+ try {
+ try {
+ SyncManager.callback()
+ .syncMailboxListStatus(mAccount.mId, EmailServiceStatus.IN_PROGRESS, 0);
+ } catch (RemoteException e1) {
+ // Don't care if this fails
+ }
+
+ if (mAccount.mSyncKey == null) {
+ mAccount.mSyncKey = "0";
+ userLog("Account syncKey INIT to 0");
+ ContentValues cv = new ContentValues();
+ cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
+ mAccount.update(mContext, cv);
+ }
+
+ boolean firstSync = mAccount.mSyncKey.equals("0");
+ if (firstSync) {
+ userLog("Initial FolderSync");
+ }
+
+ // When we first start up, change all mailboxes to push.
+ ContentValues cv = new ContentValues();
+ cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH);
+ if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
+ WHERE_ACCOUNT_AND_SYNC_INTERVAL_PING,
+ new String[] {Long.toString(mAccount.mId)}) > 0) {
+ SyncManager.kick("change ping boxes to push");
+ }
+
+ // Determine our protocol version, if we haven't already
+ if (mAccount.mProtocolVersion == null) {
+ userLog("Determine EAS protocol version");
+ HttpResponse resp = sendHttpClientOptions();
+ int code = resp.getStatusLine().getStatusCode();
+ userLog("OPTIONS response: ", code);
+ if (code == HttpStatus.SC_OK) {
+ Header header = resp.getFirstHeader("MS-ASProtocolCommands");
+ userLog(header.getValue());
+ header = resp.getFirstHeader("ms-asprotocolversions");
+ String versions = header.getValue();
+ if (versions != null) {
+ if (versions.contains("12.0")) {
+ mProtocolVersion = "12.0";
+ }
+ mProtocolVersionDouble = Double.parseDouble(mProtocolVersion);
+ mAccount.mProtocolVersion = mProtocolVersion;
+ userLog(versions);
+ userLog("Using version ", mProtocolVersion);
+ } else {
+ errorLog("No protocol versions in OPTIONS response");
+ throw new IOException();
+ }
+ } else {
+ errorLog("OPTIONS command failed; throwing IOException");
+ throw new IOException();
+ }
+ }
+
+ // Change all pushable boxes to push when we start the account mailbox
+ if (mAccount.mSyncInterval == Account.CHECK_INTERVAL_PUSH) {
+ cv = new ContentValues();
+ cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH);
+ if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
+ SyncManager.WHERE_IN_ACCOUNT_AND_PUSHABLE,
+ new String[] {Long.toString(mAccount.mId)}) > 0) {
+ userLog("Push account; set pushable boxes to push...");
+ }
+ }
+
+ while (!mStop) {
+ userLog("Sending Account syncKey: ", mAccount.mSyncKey);
+ Serializer s = new Serializer();
+ s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY)
+ .text(mAccount.mSyncKey).end().end().done();
+ HttpResponse resp = sendHttpClientPost("FolderSync", s.toByteArray());
+ if (mStop) break;
+ int code = resp.getStatusLine().getStatusCode();
+ if (code == HttpStatus.SC_OK) {
+ HttpEntity entity = resp.getEntity();
+ int len = (int)entity.getContentLength();
+ if (len != 0) {
+ InputStream is = entity.getContent();
+ // Returns true if we need to sync again
+ if (new FolderSyncParser(is, new AccountSyncAdapter(mMailbox, this))
+ .parse()) {
+ continue;
+ }
+ }
+ } else if (isAuthError(code)) {
+ mExitStatus = EXIT_LOGIN_FAILURE;
+ } else {
+ userLog("FolderSync response error: ", code);
+ }
+
+ // Change all push/hold boxes to push
+ cv = new ContentValues();
+ cv.put(Mailbox.SYNC_INTERVAL, Account.CHECK_INTERVAL_PUSH);
+ if (mContentResolver.update(Mailbox.CONTENT_URI, cv,
+ WHERE_PUSH_HOLD_NOT_ACCOUNT_MAILBOX,
+ new String[] {Long.toString(mAccount.mId)}) > 0) {
+ userLog("Set push/hold boxes to push...");
+ }
+
+ try {
+ SyncManager.callback()
+ .syncMailboxListStatus(mAccount.mId, mExitStatus, 0);
+ } catch (RemoteException e1) {
+ // Don't care if this fails
+ }
+
+ // Wait for push notifications.
+ String threadName = Thread.currentThread().getName();
+ try {
+ runPingLoop();
+ } catch (StaleFolderListException e) {
+ // We break out if we get told about a stale folder list
+ userLog("Ping interrupted; folder list requires sync...");
+ } finally {
+ Thread.currentThread().setName(threadName);
+ }
+ }
+ } catch (IOException e) {
+ // We catch this here to send the folder sync status callback
+ // A folder sync failed callback will get sent from run()
+ try {
+ if (!mStop) {
+ SyncManager.callback()
+ .syncMailboxListStatus(mAccount.mId,
+ EmailServiceStatus.CONNECTION_ERROR, 0);
+ }
+ } catch (RemoteException e1) {
+ // Don't care if this fails
+ }
+ throw e;
+ }
+ }
+
+ void pushFallback(long mailboxId) {
+ Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId);
+ ContentValues cv = new ContentValues();
+ int mins = PING_FALLBACK_PIM;
+ if (mailbox.mType == Mailbox.TYPE_INBOX) {
+ mins = PING_FALLBACK_INBOX;
+ }
+ cv.put(Mailbox.SYNC_INTERVAL, mins);
+ mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId),
+ cv, null, null);
+ errorLog("*** PING ERROR LOOP: Set " + mailbox.mDisplayName + " to " + mins + " min sync");
+ SyncManager.kick("push fallback");
+ }
+
+ void runPingLoop() throws IOException, StaleFolderListException {
+ int pingHeartbeat = mPingHeartbeat;
+ userLog("runPingLoop");
+ // Do push for all sync services here
+ long endTime = System.currentTimeMillis() + (30*MINUTES);
+ HashMap<String, Integer> pingErrorMap = new HashMap<String, Integer>();
+ ArrayList<String> readyMailboxes = new ArrayList<String>();
+ ArrayList<String> notReadyMailboxes = new ArrayList<String>();
+ int pingWaitCount = 0;
+
+ while ((System.currentTimeMillis() < endTime) && !mStop) {
+ // Count of pushable mailboxes
+ int pushCount = 0;
+ // Count of mailboxes that can be pushed right now
+ int canPushCount = 0;
+ // Count of uninitialized boxes
+ int uninitCount = 0;
+
+ Serializer s = new Serializer();
+ Cursor c = mContentResolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
+ MailboxColumns.ACCOUNT_KEY + '=' + mAccount.mId +
+ AND_FREQUENCY_PING_PUSH_AND_NOT_ACCOUNT_MAILBOX, null, null);
+ notReadyMailboxes.clear();
+ readyMailboxes.clear();
+ try {
+ // Loop through our pushed boxes seeing what is available to push
+ while (c.moveToNext()) {
+ pushCount++;
+ // Two requirements for push:
+ // 1) SyncManager tells us the mailbox is syncable (not running, not stopped)
+ // 2) The syncKey isn't "0" (i.e. it's synced at least once)
+ long mailboxId = c.getLong(Mailbox.CONTENT_ID_COLUMN);
+ int pingStatus = SyncManager.pingStatus(mailboxId);
+ String mailboxName = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
+ if (pingStatus == SyncManager.PING_STATUS_OK) {
+
+ String syncKey = c.getString(Mailbox.CONTENT_SYNC_KEY_COLUMN);
+ if ((syncKey == null) || syncKey.equals("0")) {
+ // We can't push until the initial sync is done
+ pushCount--;
+ uninitCount++;
+ continue;
+ }
+
+ if (canPushCount++ == 0) {
+ // Initialize the Ping command
+ s.start(Tags.PING_PING)
+ .data(Tags.PING_HEARTBEAT_INTERVAL,
+ Integer.toString(pingHeartbeat))
+ .start(Tags.PING_FOLDERS);
+ }
+
+ String folderClass = getTargetCollectionClassFromCursor(c);
+ s.start(Tags.PING_FOLDER)
+ .data(Tags.PING_ID, c.getString(Mailbox.CONTENT_SERVER_ID_COLUMN))
+ .data(Tags.PING_CLASS, folderClass)
+ .end();
+ readyMailboxes.add(mailboxName);
+ } else if ((pingStatus == SyncManager.PING_STATUS_RUNNING) ||
+ (pingStatus == SyncManager.PING_STATUS_WAITING)) {
+ notReadyMailboxes.add(mailboxName);
+ } else if (pingStatus == SyncManager.PING_STATUS_UNABLE) {
+ pushCount--;
+ userLog(mailboxName, " in error state; ignore");
+ continue;
+ }
+ }
+ } finally {
+ c.close();
+ }
+
+ if (Eas.USER_LOG) {
+ if (!notReadyMailboxes.isEmpty()) {
+ userLog("Ping not ready for: " + notReadyMailboxes);
+ }
+ if (!readyMailboxes.isEmpty()) {
+ userLog("Ping ready for: " + readyMailboxes);
+ }
+ }
+
+ // If we've waited 10 seconds or more, just ping with whatever boxes are ready
+ // But use a shorter than normal heartbeat
+ boolean forcePing = !notReadyMailboxes.isEmpty() && (pingWaitCount > 5);
+
+ if ((canPushCount > 0) && ((canPushCount == pushCount) || forcePing)) {
+ // If all pingable boxes are ready for push, send Ping to the server
+ s.end().end().done();
+ pingWaitCount = 0;
+
+ // If we've been stopped, this is a good time to return
+ if (mStop) return;
+
+ long pingTime = SystemClock.elapsedRealtime();
+ try {
+ // Send the ping, wrapped by appropriate timeout/alarm
+ if (forcePing) {
+ userLog("Forcing ping after waiting for all boxes to be ready");
+ }
+ HttpResponse res =
+ sendPing(s.toByteArray(), forcePing ? PING_FORCE_HEARTBEAT : pingHeartbeat);
+
+ int code = res.getStatusLine().getStatusCode();
+ userLog("Ping response: ", code);
+
+ // Return immediately if we've been asked to stop during the ping
+ if (mStop) {
+ userLog("Stopping pingLoop");
+ return;
+ }
+
+ if (code == HttpStatus.SC_OK) {
+ HttpEntity e = res.getEntity();
+ int len = (int)e.getContentLength();
+ InputStream is = res.getEntity().getContent();
+ if (len != 0) {
+ int pingResult = parsePingResult(is, mContentResolver, pingErrorMap);
+ // If our ping completed (status = 1), and we weren't forced and we're
+ // not at the maximum, try increasing timeout by two minutes
+ if (pingResult == PROTOCOL_PING_STATUS_COMPLETED && !forcePing) {
+ if (pingHeartbeat > mPingHighWaterMark) {
+ mPingHighWaterMark = pingHeartbeat;
+ userLog("Setting high water mark at: ", mPingHighWaterMark);
+ }
+ if ((pingHeartbeat < PING_MAX_HEARTBEAT) &&
+ !mPingHeartbeatDropped) {
+ pingHeartbeat += PING_HEARTBEAT_INCREMENT;
+ if (pingHeartbeat > PING_MAX_HEARTBEAT) {
+ pingHeartbeat = PING_MAX_HEARTBEAT;
+ }
+ userLog("Increasing ping heartbeat to ", pingHeartbeat, "s");
+ }
+ }
+ } else {
+ userLog("Ping returned empty result; throwing IOException");
+ throw new IOException();
+ }
+ } else if (isAuthError(code)) {
+ mExitStatus = EXIT_LOGIN_FAILURE;
+ userLog("Authorization error during Ping: ", code);
+ throw new IOException();
+ }
+ } catch (IOException e) {
+ String message = e.getMessage();
+ // If we get the exception that is indicative of a NAT timeout and if we
+ // haven't yet "fixed" the timeout, back off by two minutes and "fix" it
+ boolean hasMessage = message != null;
+ userLog("IOException runPingLoop: " + (hasMessage ? message : "[no message]"));
+ if (mAborted || (hasMessage && message.contains("reset by peer"))) {
+ long pingLength = SystemClock.elapsedRealtime() - pingTime;
+ if ((pingHeartbeat > PING_MIN_HEARTBEAT) &&
+ (pingHeartbeat > mPingHighWaterMark)) {
+ pingHeartbeat -= PING_HEARTBEAT_INCREMENT;
+ mPingHeartbeatDropped = true;
+ if (pingHeartbeat < PING_MIN_HEARTBEAT) {
+ pingHeartbeat = PING_MIN_HEARTBEAT;
+ }
+ userLog("Decreased ping heartbeat to ", pingHeartbeat, "s");
+ } else if (mAborted || (pingLength < 2000)) {
+ userLog("Abort or NAT type return < 2 seconds; throwing IOException");
+ throw e;
+ } else {
+ userLog("NAT type IOException > 2 seconds?");
+ }
+ } else {
+ throw e;
+ }
+ }
+ } else if (forcePing) {
+ // In this case, there aren't any boxes that are pingable, but there are boxes
+ // waiting (for IOExceptions)
+ userLog("pingLoop waiting 60s for any pingable boxes");
+ sleep(60*SECONDS, true);
+ } else if (pushCount > 0) {
+ // If we want to Ping, but can't just yet, wait a little bit
+ // TODO Change sleep to wait and use notify from SyncManager when a sync ends
+ sleep(2*SECONDS, false);
+ pingWaitCount++;
+ //userLog("pingLoop waited 2s for: ", (pushCount - canPushCount), " box(es)");
+ } else if (uninitCount > 0) {
+ // In this case, we're doing an initial sync of at least one mailbox. Since this
+ // is typically a one-time case, I'm ok with trying again every 10 seconds until
+ // we're in one of the other possible states.
+ userLog("pingLoop waiting for initial sync of ", uninitCount, " box(es)");
+ sleep(10*SECONDS, true);
+ } else {
+ // We've got nothing to do, so we'll check again in 30 minutes at which time
+ // we'll update the folder list. Let the device sleep in the meantime...
+ userLog("pingLoop sleeping for 30m");
+ sleep(30*MINUTES, true);
+ }
+ }
+ }
+
+ void sleep(long ms, boolean runAsleep) {
+ if (runAsleep) {
+ SyncManager.runAsleep(mMailboxId, ms+(5*SECONDS));
+ }
+ try {
+ Thread.sleep(ms);
+ } catch (InterruptedException e) {
+ // Doesn't matter whether we stop early; it's the thought that counts
+ } finally {
+ if (runAsleep) {
+ SyncManager.runAwake(mMailboxId);
+ }
+ }
+ }
+
+ private int parsePingResult(InputStream is, ContentResolver cr,
+ HashMap<String, Integer> errorMap)
+ throws IOException, StaleFolderListException {
+ PingParser pp = new PingParser(is, this);
+ if (pp.parse()) {
+ // True indicates some mailboxes need syncing...
+ // syncList has the serverId's of the mailboxes...
+ mBindArguments[0] = Long.toString(mAccount.mId);
+ mPingChangeList = pp.getSyncList();
+ for (String serverId: mPingChangeList) {
+ mBindArguments[1] = serverId;
+ Cursor c = cr.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
+ WHERE_ACCOUNT_KEY_AND_SERVER_ID, mBindArguments, null);
+ try {
+ if (c.moveToFirst()) {
+
+ /**
+ * Check the boxes reporting changes to see if there really were any...
+ * We do this because bugs in various Exchange servers can put us into a
+ * looping behavior by continually reporting changes in a mailbox, even when
+ * there aren't any.
+ *
+ * This behavior is seemingly random, and therefore we must code defensively
+ * by backing off of push behavior when it is detected.
+ *
+ * One known cause, on certain Exchange 2003 servers, is acknowledged by
+ * Microsoft, and the server hotfix for this case can be found at
+ * http://support.microsoft.com/kb/923282
+ */
+
+ // Check the status of the last sync
+ String status = c.getString(Mailbox.CONTENT_SYNC_STATUS_COLUMN);
+ int type = SyncManager.getStatusType(status);
+ // This check should always be true...
+ if (type == SyncManager.SYNC_PING) {
+ int changeCount = SyncManager.getStatusChangeCount(status);
+ if (changeCount > 0) {
+ errorMap.remove(serverId);
+ } else if (changeCount == 0) {
+ // This means that a ping reported changes in error; we keep a count
+ // of consecutive errors of this kind
+ String name = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
+ Integer failures = errorMap.get(serverId);
+ if (failures == null) {
+ userLog("Last ping reported changes in error for: ", name);
+ errorMap.put(serverId, 1);
+ } else if (failures > MAX_PING_FAILURES) {
+ // We'll back off of push for this box
+ pushFallback(c.getLong(Mailbox.CONTENT_ID_COLUMN));
+ continue;
+ } else {
+ userLog("Last ping reported changes in error for: ", name);
+ errorMap.put(serverId, failures + 1);
+ }
+ }
+ }
+
+ // If there were no problems with previous sync, we'll start another one
+ SyncManager.startManualSync(c.getLong(Mailbox.CONTENT_ID_COLUMN),
+ SyncManager.SYNC_PING, null);
+ }
+ } finally {
+ c.close();
+ }
+ }
+ }
+ return pp.getSyncStatus();
+ }
+
+ private String getFilterType() {
+ String filter = Eas.FILTER_1_WEEK;
+ switch (mAccount.mSyncLookback) {
+ case com.android.email.Account.SYNC_WINDOW_1_DAY: {
+ filter = Eas.FILTER_1_DAY;
+ break;
+ }
+ case com.android.email.Account.SYNC_WINDOW_3_DAYS: {
+ filter = Eas.FILTER_3_DAYS;
+ break;
+ }
+ case com.android.email.Account.SYNC_WINDOW_1_WEEK: {
+ filter = Eas.FILTER_1_WEEK;
+ break;
+ }
+ case com.android.email.Account.SYNC_WINDOW_2_WEEKS: {
+ filter = Eas.FILTER_2_WEEKS;
+ break;
+ }
+ case com.android.email.Account.SYNC_WINDOW_1_MONTH: {
+ filter = Eas.FILTER_1_MONTH;
+ break;
+ }
+ case com.android.email.Account.SYNC_WINDOW_ALL: {
+ filter = Eas.FILTER_ALL;
+ break;
+ }
+ }
+ return filter;
+ }
+
+ /**
+ * Common code to sync E+PIM data
+ *
+ * @param target, an EasMailbox, EasContacts, or EasCalendar object
+ */
+ public void sync(AbstractSyncAdapter target) throws IOException {
+ Mailbox mailbox = target.mMailbox;
+
+ boolean moreAvailable = true;
+ while (!mStop && moreAvailable) {
+ // If we have no connectivity, just exit cleanly. SyncManager will start us up again
+ // when connectivity has returned
+ if (!hasConnectivity()) {
+ userLog("No connectivity in sync; finishing sync");
+ mExitStatus = EXIT_DONE;
+ return;
+ }
+
+ while (true) {
+ PartRequest req = null;
+ synchronized (mPartRequests) {
+ if (mPartRequests.isEmpty()) {
+ break;
+ } else {
+ req = mPartRequests.get(0);
+ }
+ }
+ getAttachment(req);
+ synchronized(mPartRequests) {
+ mPartRequests.remove(req);
+ }
+ }
+
+ Serializer s = new Serializer();
+ String className = target.getCollectionName();
+ String syncKey = target.getSyncKey();
+ userLog("sync, sending ", className, " syncKey: ", syncKey);
+ s.start(Tags.SYNC_SYNC)
+ .start(Tags.SYNC_COLLECTIONS)
+ .start(Tags.SYNC_COLLECTION)
+ .data(Tags.SYNC_CLASS, className)
+ .data(Tags.SYNC_SYNC_KEY, syncKey)
+ .data(Tags.SYNC_COLLECTION_ID, mailbox.mServerId)
+ .tag(Tags.SYNC_DELETES_AS_MOVES);
+
+ // EAS doesn't like GetChanges if the syncKey is "0"; not documented
+ if (!syncKey.equals("0")) {
+ s.tag(Tags.SYNC_GET_CHANGES);
+ }
+ s.data(Tags.SYNC_WINDOW_SIZE,
+ className.equals("Email") ? EMAIL_WINDOW_SIZE : PIM_WINDOW_SIZE);
+
+ // Handle options
+ s.start(Tags.SYNC_OPTIONS);
+ // Set the lookback appropriately (EAS calls this a "filter") for all but Contacts
+ if (!className.equals("Contacts")) {
+ s.data(Tags.SYNC_FILTER_TYPE, getFilterType());
+ }
+ // Set the truncation amount for all classes
+ if (mProtocolVersionDouble >= 12.0) {
+ s.start(Tags.BASE_BODY_PREFERENCE)
+ // HTML for email; plain text for everything else
+ .data(Tags.BASE_TYPE, (className.equals("Email") ? Eas.BODY_PREFERENCE_HTML
+ : Eas.BODY_PREFERENCE_TEXT))
+ .data(Tags.BASE_TRUNCATION_SIZE, Eas.EAS12_TRUNCATION_SIZE)
+ .end();
+ } else {
+ s.data(Tags.SYNC_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE);
+ }
+ s.end();
+
+ // Send our changes up to the server
+ target.sendLocalChanges(s);
+
+ s.end().end().end().done();
+ HttpResponse resp = sendHttpClientPost("Sync", s.toByteArray());
+ int code = resp.getStatusLine().getStatusCode();
+ if (code == HttpStatus.SC_OK) {
+ InputStream is = resp.getEntity().getContent();
+ if (is != null) {
+ moreAvailable = target.parse(is);
+ target.cleanup();
+ } else {
+ userLog("Empty input stream in sync command response");
+ }
+ } else {
+ userLog("Sync response error: ", code);
+ if (isAuthError(code)) {
+ mExitStatus = EXIT_LOGIN_FAILURE;
+ } else {
+ mExitStatus = EXIT_IO_ERROR;
+ }
+ return;
+ }
+ }
+ mExitStatus = EXIT_DONE;
+ }
+
+ protected void setupService() {
+ // Make sure account and mailbox are always the latest from the database
+ mAccount = Account.restoreAccountWithId(mContext, mAccount.mId);
+ mMailbox = Mailbox.restoreMailboxWithId(mContext, mMailbox.mId);
+
+ mThread = Thread.currentThread();
+ android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
+ TAG = mThread.getName();
+
+ HostAuth ha = HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv);
+ mHostAddress = ha.mAddress;
+ mUserName = ha.mLogin;
+ mPassword = ha.mPassword;
+ }
+
+ /* (non-Javadoc)
+ * @see java.lang.Runnable#run()
+ */
+ public void run() {
+ setupService();
+
+ try {
+ SyncManager.callback().syncMailboxStatus(mMailboxId, EmailServiceStatus.IN_PROGRESS, 0);
+ } catch (RemoteException e1) {
+ // Don't care if this fails
+ }
+
+ // Whether or not we're the account mailbox
+ try {
+ mDeviceId = SyncManager.getDeviceId();
+ if ((mMailbox == null) || (mAccount == null)) {
+ return;
+ } else if (mMailbox.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
+ runAccountMailbox();
+ } else {
+ AbstractSyncAdapter target;
+ mProtocolVersion = mAccount.mProtocolVersion;
+ mProtocolVersionDouble = Double.parseDouble(mProtocolVersion);
+ if (mMailbox.mType == Mailbox.TYPE_CONTACTS) {
+ target = new ContactsSyncAdapter(mMailbox, this);
+ } else {
+ target = new EmailSyncAdapter(mMailbox, this);
+ }
+ // We loop here because someone might have put a request in while we were syncing
+ // and we've missed that opportunity...
+ do {
+ if (mRequestTime != 0) {
+ userLog("Looping for user request...");
+ mRequestTime = 0;
+ }
+ sync(target);
+ } while (mRequestTime != 0);
+ }
+ } catch (IOException e) {
+ String message = e.getMessage();
+ userLog("Caught IOException: ", ((message == null) ? "No message" : message));
+ mExitStatus = EXIT_IO_ERROR;
+ } catch (Exception e) {
+ userLog("Uncaught exception in EasSyncService", e);
+ } finally {
+ if (!mStop) {
+ userLog("Sync finished");
+ SyncManager.done(this);
+ // If this is the account mailbox, wake up SyncManager
+ // Because this box has a "push" interval, it will be restarted immediately
+ // which will cause the folder list to be reloaded...
+ int status;
+ switch (mExitStatus) {
+ case EXIT_IO_ERROR:
+ status = EmailServiceStatus.CONNECTION_ERROR;
+ break;
+ case EXIT_DONE:
+ status = EmailServiceStatus.SUCCESS;
+ break;
+ case EXIT_LOGIN_FAILURE:
+ status = EmailServiceStatus.LOGIN_FAILED;
+ break;
+ default:
+ status = EmailServiceStatus.REMOTE_EXCEPTION;
+ errorLog("Sync ended due to an exception.");
+ break;
+ }
+
+ try {
+ SyncManager.callback().syncMailboxStatus(mMailboxId, status, 0);
+ } catch (RemoteException e1) {
+ // Don't care if this fails
+ }
+
+ if (mExitStatus == EXIT_DONE) {
+ // Save the sync time and status
+ ContentValues cv = new ContentValues();
+ cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
+ String s = "S" + mSyncReason + ':' + status + ':' + mChangeCount;
+ cv.put(Mailbox.SYNC_STATUS, s);
+ mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI,
+ mMailboxId), cv, null, null);
+ }
+ } else {
+ userLog("Stopped sync finished.");
+ }
+
+ // Make sure SyncManager knows about this
+ SyncManager.kick("sync finished");
+ }
+ }
+}
diff --git a/src/com/android/exchange/EmailContent.aidl b/src/com/android/exchange/EmailContent.aidl
new file mode 100644
index 0000000..c6b4a7d
--- /dev/null
+++ b/src/com/android/exchange/EmailContent.aidl
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to 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.android.exchange;
+
+parcelable EmailContent.Attachment;
+
diff --git a/src/com/android/exchange/EmailServiceStatus.java b/src/com/android/exchange/EmailServiceStatus.java
new file mode 100644
index 0000000..697548c
--- /dev/null
+++ b/src/com/android/exchange/EmailServiceStatus.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to 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.android.exchange;
+
+/**
+ * Definitions of service status codes returned to IEmailServiceCallback's status method
+ */
+public interface EmailServiceStatus {
+ public static final int SUCCESS = 0;
+ public static final int IN_PROGRESS = 1;
+
+ public static final int MESSAGE_NOT_FOUND = 0x10;
+ public static final int ATTACHMENT_NOT_FOUND = 0x11;
+ public static final int FOLDER_NOT_DELETED = 0x12;
+ public static final int FOLDER_NOT_RENAMED = 0x13;
+ public static final int FOLDER_NOT_CREATED = 0x14;
+ public static final int REMOTE_EXCEPTION = 0x15;
+ public static final int LOGIN_FAILED = 0x16;
+
+ // Maybe we should automatically retry these?
+ public static final int CONNECTION_ERROR = 0x20;
+}
diff --git a/src/com/android/exchange/EmailSyncAlarmReceiver.java b/src/com/android/exchange/EmailSyncAlarmReceiver.java
new file mode 100644
index 0000000..4a44860
--- /dev/null
+++ b/src/com/android/exchange/EmailSyncAlarmReceiver.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to 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.android.exchange;
+
+import com.android.email.provider.EmailContent.Message;
+import com.android.email.provider.EmailContent.MessageColumns;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * EmailSyncAlarmReceiver (USAR) is used by the SyncManager to start up-syncs of user-modified data
+ * back to the Exchange server.
+ *
+ * Here's how this works for Email, for example:
+ *
+ * 1) User modifies or deletes an email from the UI.
+ * 2) SyncManager, which has a ContentObserver watching the Message class, is alerted to a change
+ * 3) SyncManager sets an alarm (to be received by USAR) for a few seconds in the
+ * future (currently 15), the delay preventing excess syncing (think of it as a debounce mechanism).
+ * 4) ESAR Receiver's onReceive method is called
+ * 5) ESAR goes through all change and deletion records and compiles a list of mailboxes which have
+ * changes to be uploaded.
+ * 6) ESAR calls SyncManager to start syncs of those mailboxes
+ *
+ */
+public class EmailSyncAlarmReceiver extends BroadcastReceiver {
+ final String[] MAILBOX_DATA_PROJECTION = {MessageColumns.MAILBOX_KEY};
+ private static String TAG = "EmailSyncAlarm";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.v(TAG, "onReceive");
+ ArrayList<Long> mailboxesToNotify = new ArrayList<Long>();
+ ContentResolver cr = context.getContentResolver();
+ int messageCount = 0;
+
+ // Get a selector for EAS accounts (we don't want to sync on changes to POP/IMAP messages)
+ String selector = SyncManager.getEasAccountSelector();
+
+ // Find all of the deletions
+ Cursor c = cr.query(Message.DELETED_CONTENT_URI, MAILBOX_DATA_PROJECTION, selector,
+ null, null);
+ try {
+ // Keep track of which mailboxes to notify; we'll only notify each one once
+ while (c.moveToNext()) {
+ messageCount++;
+ long mailboxId = c.getLong(0);
+ if (!mailboxesToNotify.contains(mailboxId)) {
+ mailboxesToNotify.add(mailboxId);
+ }
+ }
+ } finally {
+ c.close();
+ }
+
+ // Now, find changed messages
+ c = cr.query(Message.UPDATED_CONTENT_URI, MAILBOX_DATA_PROJECTION, selector,
+ null, null);
+ try {
+ // Keep track of which mailboxes to notify; we'll only notify each one once
+ while (c.moveToNext()) {
+ messageCount++;
+ long mailboxId = c.getLong(0);
+ if (!mailboxesToNotify.contains(mailboxId)) {
+ mailboxesToNotify.add(mailboxId);
+ }
+ }
+ } finally {
+ c.close();
+ }
+
+ // Request service from the mailbox
+ for (Long mailboxId: mailboxesToNotify) {
+ SyncManager.serviceRequest(mailboxId, SyncManager.SYNC_UPSYNC);
+ }
+ Log.v(TAG, "Changed/Deleted messages: " + messageCount + ", mailboxes: " +
+ mailboxesToNotify.size());
+ }
+}
diff --git a/src/com/android/exchange/IEmailService.aidl b/src/com/android/exchange/IEmailService.aidl
new file mode 100644
index 0000000..ec0fd5e
--- /dev/null
+++ b/src/com/android/exchange/IEmailService.aidl
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to 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.android.exchange;
+
+import com.android.exchange.IEmailServiceCallback;
+import com.android.exchange.EmailContent;
+
+interface IEmailService {
+ int validate(in String protocol, in String host, in String userName, in String password,
+ int port, boolean ssl, boolean trustCertificates) ;
+
+ void startSync(long mailboxId);
+ void stopSync(long mailboxId);
+
+ void loadMore(long messageId);
+ void loadAttachment(long attachmentId, String destinationFile, String contentUriString);
+
+ void updateFolderList(long accountId);
+
+ boolean createFolder(long accountId, String name);
+ boolean deleteFolder(long accountId, String name);
+ boolean renameFolder(long accountId, String oldName, String newName);
+
+ void setCallback(IEmailServiceCallback cb);
+
+ void setLogging(int on);
+
+ void hostChanged(long accountId);
+}
\ No newline at end of file
diff --git a/src/com/android/exchange/IEmailServiceCallback.aidl b/src/com/android/exchange/IEmailServiceCallback.aidl
new file mode 100644
index 0000000..a789c4a
--- /dev/null
+++ b/src/com/android/exchange/IEmailServiceCallback.aidl
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to 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.android.exchange;
+
+oneway interface IEmailServiceCallback {
+ /*
+ * Ordinary results:
+ * statuscode = 1, progress = 0: "starting"
+ * statuscode = 0, progress = n/a: "finished"
+ *
+ * If there is an error, it must be reported as follows:
+ * statuscode = err, progress = n/a: "stopping due to error"
+ *
+ * *Optionally* a callback can also include intermediate values from 1..99 e.g.
+ * statuscode = 1, progress = 0: "starting"
+ * statuscode = 1, progress = 30: "working"
+ * statuscode = 1, progress = 60: "working"
+ * statuscode = 0, progress = n/a: "finished"
+ */
+
+ /**
+ * Callback to indicate that an account is being synced (updating folder list)
+ * accountId = the account being synced
+ * statusCode = 0 for OK, 1 for progress, other codes for error
+ * progress = 0 for "start", 1..100 for optional progress reports
+ */
+ void syncMailboxListStatus(long accountId, int statusCode, int progress);
+
+ /**
+ * Callback to indicate that a mailbox is being synced
+ * mailboxId = the mailbox being synced
+ * statusCode = 0 for OK, 1 for progress, other codes for error
+ * progress = 0 for "start", 1..100 for optional progress reports
+ */
+ void syncMailboxStatus(long mailboxId, int statusCode, int progress);
+
+ /**
+ * Callback to indicate that a particular attachment is being synced
+ * messageId = the message that owns the attachment
+ * attachmentId = the attachment being synced
+ * statusCode = 0 for OK, 1 for progress, other codes for error
+ * progress = 0 for "start", 1..100 for optional progress reports
+ */
+ void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, int progress);
+
+ /**
+ * Callback to indicate that a particular message is being sent
+ * messageId = the message being sent
+ * statusCode = 0 for OK, 1 for progress, other codes for error
+ * progress = 0 for "start", 1..100 for optional progress reports
+ */
+ void sendMessageStatus(long messageId, String subject, int statusCode, int progress);
+}
diff --git a/src/com/android/exchange/MailboxAlarmReceiver.java b/src/com/android/exchange/MailboxAlarmReceiver.java
new file mode 100644
index 0000000..cc4e2f4
--- /dev/null
+++ b/src/com/android/exchange/MailboxAlarmReceiver.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to 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.android.exchange;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * MailboxAlarmReceiver is used to "wake up" the SyncManager at the appropriate time(s). It may
+ * also be used for individual sync adapters, but this isn't implemented at the present time.
+ *
+ */
+public class MailboxAlarmReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ long mid = intent.getLongExtra("mailbox", -1);
+ if (SyncManager.INSTANCE != null) {
+ SyncManager.INSTANCE.log("Alarm received for: " + SyncManager.alarmOwner(mid));
+ }
+ SyncManager.ping(context, mid);
+ }
+}
+
diff --git a/src/com/android/exchange/MockParserStream.java b/src/com/android/exchange/MockParserStream.java
new file mode 100644
index 0000000..a518667
--- /dev/null
+++ b/src/com/android/exchange/MockParserStream.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to 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.android.exchange;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * MockParserStream is an InputStream that feeds pre-generated data into various EasParser
+ * subclasses.
+ *
+ * After parsing is done, the result can be obtained with getResult
+ *
+ */
+public class MockParserStream extends InputStream {
+ int[] array;
+ int pos = 0;
+ Object value;
+
+ MockParserStream (int[] _array) {
+ array = _array;
+ }
+
+ @Override
+ public int read() throws IOException {
+ try {
+ return array[pos++];
+ } catch (IndexOutOfBoundsException e) {
+ throw new IOException("End of stream");
+ }
+ }
+
+ public void setResult(Object _value) {
+ value = _value;
+ }
+
+ public Object getResult() {
+ return value;
+ }
+}
diff --git a/src/com/android/exchange/PartRequest.java b/src/com/android/exchange/PartRequest.java
new file mode 100644
index 0000000..72b79f4
--- /dev/null
+++ b/src/com/android/exchange/PartRequest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to 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.android.exchange;
+
+import com.android.email.provider.EmailContent.Attachment;
+
+/**
+ * PartRequest is the EAS wrapper for attachment loading requests. In addition to information about
+ * the attachment to be loaded, it also contains the callback to be used for status/progress
+ * updates to the UI.
+ */
+public class PartRequest {
+ public long timeStamp;
+ public long emailId;
+ public Attachment att;
+ public String destination;
+ public String contentUriString;
+ public String loc;
+
+ public PartRequest(Attachment _att) {
+ timeStamp = System.currentTimeMillis();
+ emailId = _att.mMessageKey;
+ att = _att;
+ loc = att.mLocation;
+ }
+
+ public PartRequest(Attachment _att, String _destination, String _contentUriString) {
+ this(_att);
+ destination = _destination;
+ contentUriString = _contentUriString;
+ }
+}
diff --git a/src/com/android/exchange/StaleFolderListException.java b/src/com/android/exchange/StaleFolderListException.java
new file mode 100644
index 0000000..70ac032
--- /dev/null
+++ b/src/com/android/exchange/StaleFolderListException.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to 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.android.exchange;
+
+public class StaleFolderListException extends EasException {
+ private static final long serialVersionUID = 1L;
+}
diff --git a/src/com/android/exchange/SyncManager.java b/src/com/android/exchange/SyncManager.java
new file mode 100644
index 0000000..ece930c
--- /dev/null
+++ b/src/com/android/exchange/SyncManager.java
@@ -0,0 +1,1864 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to 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.android.exchange;
+
+import com.android.email.mail.MessagingException;
+import com.android.email.mail.store.TrustManagerFactory;
+import com.android.email.provider.EmailContent;
+import com.android.email.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.Attachment;
+import com.android.email.provider.EmailContent.HostAuth;
+import com.android.email.provider.EmailContent.HostAuthColumns;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.email.provider.EmailContent.MailboxColumns;
+import com.android.email.provider.EmailContent.Message;
+import com.android.email.provider.EmailContent.SyncColumns;
+import com.android.exchange.utility.FileLogger;
+
+import org.apache.harmony.xnet.provider.jsse.SSLContextImpl;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.conn.params.ConnManagerPNames;
+import org.apache.http.conn.params.ConnPerRoute;
+import org.apache.http.conn.routing.HttpRoute;
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.conn.ssl.SSLSocketFactory;
+import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpParams;
+
+import android.accounts.AccountManager;
+import android.accounts.OnAccountsUpdateListener;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SyncStatusObserver;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.net.NetworkInfo.State;
+import android.os.Bundle;
+import android.os.Debug;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.os.PowerManager.WakeLock;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * The SyncManager handles all aspects of starting, maintaining, and stopping the various sync
+ * adapters used by Exchange. However, it is capable of handing any kind of email sync, and it
+ * would be appropriate to use for IMAP push, when that functionality is added to the Email
+ * application.
+ *
+ * The Email application communicates with EAS sync adapters via SyncManager's binder interface,
+ * which exposes UI-related functionality to the application (see the definitions below)
+ *
+ * SyncManager uses ContentObservers to detect changes to accounts, mailboxes, and messages in
+ * order to maintain proper 2-way syncing of data. (More documentation to follow)
+ *
+ */
+public class SyncManager extends Service implements Runnable {
+
+ private static final String TAG = "EAS SyncManager";
+
+ // The SyncManager's mailbox "id"
+ private static final int SYNC_MANAGER_ID = -1;
+
+ private static final int SECONDS = 1000;
+ private static final int MINUTES = 60*SECONDS;
+ private static final int ONE_DAY_MINUTES = 1440;
+
+ private static final int SYNC_MANAGER_HEARTBEAT_TIME = 15*MINUTES;
+ private static final int CONNECTIVITY_WAIT_TIME = 10*MINUTES;
+
+ // Sync hold constants for services with transient errors
+ private static final int HOLD_DELAY_MAXIMUM = 4*MINUTES;
+
+ // Reason codes when SyncManager.kick is called (mainly for debugging)
+ // UI has changed data, requiring an upsync of changes
+ public static final int SYNC_UPSYNC = 0;
+ // A scheduled sync (when not using push)
+ public static final int SYNC_SCHEDULED = 1;
+ // Mailbox was marked push
+ public static final int SYNC_PUSH = 2;
+ // A ping (EAS push signal) was received
+ public static final int SYNC_PING = 3;
+ // startSync was requested of SyncManager
+ public static final int SYNC_SERVICE_START_SYNC = 4;
+ // A part request (attachment load, for now) was sent to SyncManager
+ public static final int SYNC_SERVICE_PART_REQUEST = 5;
+ // Misc.
+ public static final int SYNC_KICK = 6;
+
+ private static final String WHERE_PUSH_OR_PING_NOT_ACCOUNT_MAILBOX =
+ MailboxColumns.ACCOUNT_KEY + "=? and " + MailboxColumns.TYPE + "!=" +
+ Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + " and " + MailboxColumns.SYNC_INTERVAL +
+ " IN (" + Mailbox.CHECK_INTERVAL_PING + ',' + Mailbox.CHECK_INTERVAL_PUSH + ')';
+ protected static final String WHERE_IN_ACCOUNT_AND_PUSHABLE =
+ MailboxColumns.ACCOUNT_KEY + "=? and type in (" + Mailbox.TYPE_INBOX + ','
+ + Mailbox.TYPE_EAS_ACCOUNT_MAILBOX + ',' + Mailbox.TYPE_CONTACTS + ')';
+ private static final String WHERE_MAILBOX_KEY = EmailContent.RECORD_ID + "=?";
+ private static final String WHERE_PROTOCOL_EAS = HostAuthColumns.PROTOCOL + "=\"" +
+ AbstractSyncService.EAS_PROTOCOL + "\"";
+ private static final String WHERE_NOT_INTERVAL_NEVER_AND_ACCOUNT_KEY_IN =
+ "(" + MailboxColumns.TYPE + '=' + Mailbox.TYPE_OUTBOX
+ + " or " + MailboxColumns.SYNC_INTERVAL + "!=" + Mailbox.CHECK_INTERVAL_NEVER + ')'
+ + " and " + MailboxColumns.ACCOUNT_KEY + " in (";
+ private static final String ACCOUNT_KEY_IN = MailboxColumns.ACCOUNT_KEY + " in (";
+
+ // Offsets into the syncStatus data for EAS that indicate type, exit status, and change count
+ // The format is S<type_char>:<exit_char>:<change_count>
+ public static final int STATUS_TYPE_CHAR = 1;
+ public static final int STATUS_EXIT_CHAR = 3;
+ public static final int STATUS_CHANGE_COUNT_OFFSET = 5;
+
+ // Ready for ping
+ public static final int PING_STATUS_OK = 0;
+ // Service already running (can't ping)
+ public static final int PING_STATUS_RUNNING = 1;
+ // Service waiting after I/O error (can't ping)
+ public static final int PING_STATUS_WAITING = 2;
+ // Service had a fatal error; can't run
+ public static final int PING_STATUS_UNABLE = 3;
+
+ // We synchronize on this for all actions affecting the service and error maps
+ private static Object sSyncToken = new Object();
+ // All threads can use this lock to wait for connectivity
+ public static Object sConnectivityLock = new Object();
+ public static boolean sConnectivityHold = false;
+
+ // Keeps track of running services (by mailbox id)
+ private HashMap<Long, AbstractSyncService> mServiceMap =
+ new HashMap<Long, AbstractSyncService>();
+ // Keeps track of services whose last sync ended with an error (by mailbox id)
+ private HashMap<Long, SyncError> mSyncErrorMap = new HashMap<Long, SyncError>();
+ // Keeps track of which services require a wake lock (by mailbox id)
+ private HashMap<Long, Boolean> mWakeLocks = new HashMap<Long, Boolean>();
+ // Keeps track of PendingIntents for mailbox alarms (by mailbox id)
+ private HashMap<Long, PendingIntent> mPendingIntents = new HashMap<Long, PendingIntent>();
+ // The actual WakeLock obtained by SyncManager
+ private WakeLock mWakeLock = null;
+ private static final AccountList EMPTY_ACCOUNT_LIST = new AccountList();
+
+ // Observers that we use to look for changed mail-related data
+ private Handler mHandler = new Handler();
+ private AccountObserver mAccountObserver;
+ private MailboxObserver mMailboxObserver;
+ private SyncedMessageObserver mSyncedMessageObserver;
+ private MessageObserver mMessageObserver;
+ private EasSyncStatusObserver mSyncStatusObserver;
+ private EasAccountsUpdatedListener mAccountsUpdatedListener;
+
+ private ContentResolver mResolver;
+
+ // The singleton SyncManager object, with its thread and stop flag
+ protected static SyncManager INSTANCE;
+ private static Thread sServiceThread = null;
+ // Cached unique device id
+ private static String sDeviceId = null;
+ // ConnectionManager that all EAS threads can use
+ private static ClientConnectionManager sClientConnectionManager = null;
+
+ private boolean mStop = false;
+
+ // The reason for SyncManager's next wakeup call
+ private String mNextWaitReason;
+ // Whether we have an unsatisfied "kick" pending
+ private boolean mKicked = false;
+
+ // Receiver of connectivity broadcasts
+ private ConnectivityReceiver mConnectivityReceiver = null;
+
+ // The callback sent in from the UI using setCallback
+ private IEmailServiceCallback mCallback;
+ private RemoteCallbackList<IEmailServiceCallback> mCallbackList =
+ new RemoteCallbackList<IEmailServiceCallback>();
+
+ /**
+ * Proxy that can be used by various sync adapters to tie into SyncManager's callback system.
+ * Used this way: SyncManager.callback().callbackMethod(args...);
+ * The proxy wraps checking for existence of a SyncManager instance and an active callback.
+ * Failures of these callbacks can be safely ignored.
+ */
+ static private final IEmailServiceCallback.Stub sCallbackProxy =
+ new IEmailServiceCallback.Stub() {
+
+ public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode,
+ int progress) throws RemoteException {
+ IEmailServiceCallback cb = INSTANCE == null ? null: INSTANCE.mCallback;
+ if (cb != null) {
+ cb.loadAttachmentStatus(messageId, attachmentId, statusCode, progress);
+ }
+ }
+
+ public void sendMessageStatus(long messageId, String subject, int statusCode, int progress)
+ throws RemoteException {
+ IEmailServiceCallback cb = INSTANCE == null ? null: INSTANCE.mCallback;
+ if (cb != null) {
+ cb.sendMessageStatus(messageId, subject, statusCode, progress);
+ }
+ }
+
+ public void syncMailboxListStatus(long accountId, int statusCode, int progress)
+ throws RemoteException {
+ IEmailServiceCallback cb = INSTANCE == null ? null: INSTANCE.mCallback;
+ if (cb != null) {
+ cb.syncMailboxListStatus(accountId, statusCode, progress);
+ }
+ }
+
+ public void syncMailboxStatus(long mailboxId, int statusCode, int progress)
+ throws RemoteException {
+ IEmailServiceCallback cb = INSTANCE == null ? null: INSTANCE.mCallback;
+ if (cb != null) {
+ cb.syncMailboxStatus(mailboxId, statusCode, progress);
+ } else if (INSTANCE != null) {
+ INSTANCE.log("orphan syncMailboxStatus, id=" + mailboxId + " status=" + statusCode);
+ }
+ }
+ };
+
+ /**
+ * Create our EmailService implementation here.
+ */
+ private final IEmailService.Stub mBinder = new IEmailService.Stub() {
+
+ public int validate(String protocol, String host, String userName, String password,
+ int port, boolean ssl, boolean trustCertificates) throws RemoteException {
+ try {
+ AbstractSyncService.validate(EasSyncService.class, host, userName, password, port,
+ ssl, trustCertificates, SyncManager.this);
+ return MessagingException.NO_ERROR;
+ } catch (MessagingException e) {
+ return e.getExceptionType();
+ }
+ }
+
+ public void startSync(long mailboxId) throws RemoteException {
+ checkSyncManagerServiceRunning();
+ Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId);
+ if (m.mType == Mailbox.TYPE_OUTBOX) {
+ // We're using SERVER_ID to indicate an error condition (it has no other use for
+ // sent mail) Upon request to sync the Outbox, we clear this so that all messages
+ // are candidates for sending.
+ ContentValues cv = new ContentValues();
+ cv.put(SyncColumns.SERVER_ID, 0);
+ INSTANCE.getContentResolver().update(Message.CONTENT_URI,
+ cv, WHERE_MAILBOX_KEY, new String[] {Long.toString(mailboxId)});
+
+ kick("start outbox");
+ // Outbox can't be synced in EAS
+ return;
+ } else if (m.mType == Mailbox.TYPE_DRAFTS) {
+ // Drafts can't be synced in EAS
+ return;
+ }
+ startManualSync(mailboxId, SyncManager.SYNC_SERVICE_START_SYNC, null);
+ }
+
+ public void stopSync(long mailboxId) throws RemoteException {
+ stopManualSync(mailboxId);
+ }
+
+ public void loadAttachment(long attachmentId, String destinationFile,
+ String contentUriString) throws RemoteException {
+ Attachment att = Attachment.restoreAttachmentWithId(SyncManager.this, attachmentId);
+ partRequest(new PartRequest(att, destinationFile, contentUriString));
+ }
+
+ public void updateFolderList(long accountId) throws RemoteException {
+ reloadFolderList(SyncManager.this, accountId, false);
+ }
+
+ public void hostChanged(long accountId) throws RemoteException {
+ if (INSTANCE != null) {
+ synchronized (INSTANCE.mSyncErrorMap) {
+ // Go through the various error mailboxes
+ for (long mailboxId: INSTANCE.mSyncErrorMap.keySet()) {
+ SyncError error = INSTANCE.mSyncErrorMap.get(mailboxId);
+ // If it's a login failure, look a little harder
+ Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId);
+ // If it's for the account whose host has changed, clear the error
+ if (m.mAccountKey == accountId) {
+ error.fatal = false;
+ error.holdEndTime = 0;
+ }
+ }
+ }
+ // Stop any running syncs
+ INSTANCE.stopAccountSyncs(accountId, true);
+ // Kick SyncManager
+ kick("host changed");
+ }
+ }
+
+ public void setLogging(int on) throws RemoteException {
+ Eas.setUserDebug(on);
+ }
+
+ public void loadMore(long messageId) throws RemoteException {
+ // TODO Auto-generated method stub
+ }
+
+ // The following three methods are not implemented in this version
+ public boolean createFolder(long accountId, String name) throws RemoteException {
+ return false;
+ }
+
+ public boolean deleteFolder(long accountId, String name) throws RemoteException {
+ return false;
+ }
+
+ public boolean renameFolder(long accountId, String oldName, String newName)
+ throws RemoteException {
+ return false;
+ }
+
+ public void setCallback(IEmailServiceCallback cb) throws RemoteException {
+ if (mCallback != null) {
+ mCallbackList.unregister(mCallback);
+ }
+ mCallback = cb;
+ mCallbackList.register(cb);
+ }
+ };
+
+ static class AccountList extends ArrayList<Account> {
+ private static final long serialVersionUID = 1L;
+
+ public boolean contains(long id) {
+ for (Account account: this) {
+ if (account.mId == id) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public Account getById(long id) {
+ for (Account account: this) {
+ if (account.mId == id) {
+ return account;
+ }
+ }
+ return null;
+ }
+ }
+
+ class AccountObserver extends ContentObserver {
+ // mAccounts keeps track of Accounts that we care about (EAS for now)
+ AccountList mAccounts = new AccountList();
+ String mSyncableEasMailboxSelector = null;
+ String mEasAccountSelector = null;
+
+ public AccountObserver(Handler handler) {
+ super(handler);
+ // At startup, we want to see what EAS accounts exist and cache them
+ Cursor c = getContentResolver().query(Account.CONTENT_URI, Account.CONTENT_PROJECTION,
+ null, null, null);
+ try {
+ collectEasAccounts(c, mAccounts);
+ } finally {
+ c.close();
+ }
+
+ // Create the account mailbox for any account that doesn't have one
+ Context context = getContext();
+ for (Account account: mAccounts) {
+ int cnt = Mailbox.count(context, Mailbox.CONTENT_URI, "accountKey=" + account.mId,
+ null);
+ if (cnt == 0) {
+ addAccountMailbox(account.mId);
+ }
+ }
+ }
+
+ /**
+ * Returns a String suitable for appending to a where clause that selects for all syncable
+ * mailboxes in all eas accounts
+ * @return a complex selection string that is not to be cached
+ */
+ public String getSyncableEasMailboxWhere() {
+ if (mSyncableEasMailboxSelector == null) {
+ StringBuilder sb = new StringBuilder(WHERE_NOT_INTERVAL_NEVER_AND_ACCOUNT_KEY_IN);
+ boolean first = true;
+ for (Account account: mAccounts) {
+ if (!first) {
+ sb.append(',');
+ } else {
+ first = false;
+ }
+ sb.append(account.mId);
+ }
+ sb.append(')');
+ mSyncableEasMailboxSelector = sb.toString();
+ }
+ return mSyncableEasMailboxSelector;
+ }
+
+ /**
+ * Returns a String suitable for appending to a where clause that selects for all eas
+ * accounts.
+ * @return a String in the form "accountKey in (a, b, c...)" that is not to be cached
+ */
+ public String getAccountKeyWhere() {
+ if (mEasAccountSelector == null) {
+ StringBuilder sb = new StringBuilder(ACCOUNT_KEY_IN);
+ boolean first = true;
+ for (Account account: mAccounts) {
+ if (!first) {
+ sb.append(',');
+ } else {
+ first = false;
+ }
+ sb.append(account.mId);
+ }
+ sb.append(')');
+ mEasAccountSelector = sb.toString();
+ }
+ return mEasAccountSelector;
+ }
+
+ private boolean syncParametersChanged(Account account) {
+ long accountId = account.mId;
+ // Reload account from database to get its current state
+ account = Account.restoreAccountWithId(getContext(), accountId);
+ for (Account oldAccount: mAccounts) {
+ if (oldAccount.mId == accountId) {
+ return oldAccount.mSyncInterval != account.mSyncInterval ||
+ oldAccount.mSyncLookback != account.mSyncLookback;
+ }
+ }
+ // Really, we can't get here, but we don't want the compiler to complain
+ return false;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ maybeStartSyncManagerThread();
+
+ // A change to the list requires us to scan for deletions (to stop running syncs)
+ // At startup, we want to see what accounts exist and cache them
+ AccountList currentAccounts = new AccountList();
+ Cursor c = getContentResolver().query(Account.CONTENT_URI, Account.CONTENT_PROJECTION,
+ null, null, null);
+ try {
+ collectEasAccounts(c, currentAccounts);
+ for (Account account : mAccounts) {
+ if (!currentAccounts.contains(account.mId)) {
+ // This is a deletion; shut down any account-related syncs
+ stopAccountSyncs(account.mId, true);
+ // Delete this from AccountManager...
+ android.accounts.Account acct =
+ new android.accounts.Account(account.mEmailAddress,
+ Eas.ACCOUNT_MANAGER_TYPE);
+ AccountManager.get(SyncManager.this).removeAccount(acct, null, null);
+ mSyncableEasMailboxSelector = null;
+ mEasAccountSelector = null;
+ } else {
+ // See whether any of our accounts has changed sync interval or window
+ if (syncParametersChanged(account)) {
+ // Set pushable boxes' sync interval to the sync interval of the Account
+ Account updatedAccount =
+ Account.restoreAccountWithId(getContext(), account.mId);
+ ContentValues cv = new ContentValues();
+ cv.put(MailboxColumns.SYNC_INTERVAL, updatedAccount.mSyncInterval);
+ getContentResolver().update(Mailbox.CONTENT_URI, cv,
+ WHERE_IN_ACCOUNT_AND_PUSHABLE,
+ new String[] {Long.toString(account.mId)});
+ // Stop all current syncs; the appropriate ones will restart
+ INSTANCE.log("Account " + account.mDisplayName +
+ " changed; stop running syncs...");
+ stopAccountSyncs(account.mId, true);
+ }
+ }
+ }
+
+ // Look for new accounts
+ for (Account account: currentAccounts) {
+ if (!mAccounts.contains(account.mId)) {
+ // This is an addition; create our magic hidden mailbox...
+ addAccountMailbox(account.mId);
+ // Don't forget to cache the HostAuth
+ HostAuth ha =
+ HostAuth.restoreHostAuthWithId(getContext(), account.mHostAuthKeyRecv);
+ account.mHostAuthRecv = ha;
+ mAccounts.add(account);
+ mSyncableEasMailboxSelector = null;
+ mEasAccountSelector = null;
+ }
+ }
+
+ // Finally, make sure mAccounts is up to date
+ mAccounts = currentAccounts;
+ } finally {
+ c.close();
+ }
+
+ // See if there's anything to do...
+ kick("account changed");
+ }
+
+ private void collectEasAccounts(Cursor c, ArrayList<Account> accounts) {
+ Context context = getContext();
+ if (context == null) return;
+ while (c.moveToNext()) {
+ long hostAuthId = c.getLong(Account.CONTENT_HOST_AUTH_KEY_RECV_COLUMN);
+ if (hostAuthId > 0) {
+ HostAuth ha = HostAuth.restoreHostAuthWithId(context, hostAuthId);
+ if (ha != null && ha.mProtocol.equals("eas")) {
+ Account account = new Account().restore(c);
+ // Cache the HostAuth
+ account.mHostAuthRecv = ha;
+ accounts.add(account);
+ }
+ }
+ }
+ }
+
+ private void addAccountMailbox(long acctId) {
+ Account acct = Account.restoreAccountWithId(getContext(), acctId);
+ Mailbox main = new Mailbox();
+ main.mDisplayName = Eas.ACCOUNT_MAILBOX;
+ main.mServerId = Eas.ACCOUNT_MAILBOX + System.nanoTime();
+ main.mAccountKey = acct.mId;
+ main.mType = Mailbox.TYPE_EAS_ACCOUNT_MAILBOX;
+ main.mSyncInterval = Mailbox.CHECK_INTERVAL_PUSH;
+ main.mFlagVisible = false;
+ main.save(getContext());
+ INSTANCE.log("Initializing account: " + acct.mDisplayName);
+ }
+
+ }
+
+ class MailboxObserver extends ContentObserver {
+ public MailboxObserver(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ // See if there's anything to do...
+ if (!selfChange) {
+ kick("mailbox changed");
+ }
+ }
+ }
+
+ class SyncedMessageObserver extends ContentObserver {
+ Intent syncAlarmIntent = new Intent(INSTANCE, EmailSyncAlarmReceiver.class);
+ PendingIntent syncAlarmPendingIntent =
+ PendingIntent.getBroadcast(INSTANCE, 0, syncAlarmIntent, 0);
+ AlarmManager alarmManager = (AlarmManager)INSTANCE.getSystemService(Context.ALARM_SERVICE);
+
+ public SyncedMessageObserver(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ INSTANCE.log("SyncedMessage changed: (re)setting alarm for 10s");
+ alarmManager.set(AlarmManager.RTC_WAKEUP,
+ System.currentTimeMillis() + 10*SECONDS, syncAlarmPendingIntent);
+ }
+ }
+
+ class MessageObserver extends ContentObserver {
+
+ public MessageObserver(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ // A rather blunt instrument here. But we don't have information about the URI that
+ // triggered this, though it must have been an insert
+ if (!selfChange) {
+ kick(null);
+ }
+ }
+ }
+
+ static public IEmailServiceCallback callback() {
+ return sCallbackProxy;
+ }
+
+ static public AccountList getAccountList() {
+ if (INSTANCE != null) {
+ return INSTANCE.mAccountObserver.mAccounts;
+ } else {
+ return EMPTY_ACCOUNT_LIST;
+ }
+ }
+
+ static public String getEasAccountSelector() {
+ if (INSTANCE != null) {
+ return INSTANCE.mAccountObserver.getAccountKeyWhere();
+ } else {
+ return null;
+ }
+ }
+
+ private Account getAccountById(long accountId) {
+ return mAccountObserver.mAccounts.getById(accountId);
+ }
+
+ public class SyncStatus {
+ static public final int NOT_RUNNING = 0;
+ static public final int DIED = 1;
+ static public final int SYNC = 2;
+ static public final int IDLE = 3;
+ }
+
+ class SyncError {
+ int reason;
+ boolean fatal = false;
+ long holdDelay = 15*SECONDS;
+ long holdEndTime = System.currentTimeMillis() + holdDelay;
+
+ SyncError(int _reason, boolean _fatal) {
+ reason = _reason;
+ fatal = _fatal;
+ }
+
+ /**
+ * We double the holdDelay from 15 seconds through 4 mins
+ */
+ void escalate() {
+ if (holdDelay < HOLD_DELAY_MAXIMUM) {
+ holdDelay *= 2;
+ }
+ holdEndTime = System.currentTimeMillis() + holdDelay;
+ }
+ }
+
+ public class EasSyncStatusObserver implements SyncStatusObserver {
+ public void onStatusChanged(int which) {
+ // We ignore the argument (we can only get called in one case - when settings change)
+ // TODO Go through each account and see if sync is enabled and/or automatic for
+ // the Contacts authority, and set syncInterval accordingly. Then kick ourselves.
+ if (INSTANCE != null) {
+ checkPIMSyncSettings();
+ }
+ }
+ }
+
+ public class EasAccountsUpdatedListener implements OnAccountsUpdateListener {
+ public void onAccountsUpdated(android.accounts.Account[] accounts) {
+ checkWithAccountManager();
+ }
+ }
+
+ static public void smLog(String str) {
+ if (INSTANCE != null) {
+ INSTANCE.log(str);
+ }
+ }
+
+ protected void log(String str) {
+ if (Eas.USER_LOG) {
+ Log.d(TAG, str);
+ if (Eas.FILE_LOG) {
+ FileLogger.log(TAG, str);
+ }
+ }
+ }
+
+ protected void alwaysLog(String str) {
+ if (!Eas.USER_LOG) {
+ Log.d(TAG, str);
+ } else {
+ log(str);
+ }
+ }
+
+ /**
+ * EAS requires a unique device id, so that sync is possible from a variety of different
+ * devices (e.g. the syncKey is specific to a device) If we're on an emulator or some other
+ * device that doesn't provide one, we can create it as droid<n> where <n> is system time.
+ * This would work on a real device as well, but it would be better to use the "real" id if
+ * it's available
+ */
+ static public String getDeviceId() throws IOException {
+ return getDeviceId(null);
+ }
+
+ static public synchronized String getDeviceId(Context context) throws IOException {
+ if (sDeviceId != null) {
+ return sDeviceId;
+ } else if (INSTANCE == null && context == null) {
+ throw new IOException("No context for getDeviceId");
+ } else if (context == null) {
+ context = INSTANCE;
+ }
+
+ // Otherwise, we'll read the id file or create one if it's not found
+ try {
+ File f = context.getFileStreamPath("deviceName");
+ BufferedReader rdr = null;
+ String id;
+ if (f.exists() && f.canRead()) {
+ rdr = new BufferedReader(new FileReader(f), 128);
+ id = rdr.readLine();
+ rdr.close();
+ return id;
+ } else if (f.createNewFile()) {
+ BufferedWriter w = new BufferedWriter(new FileWriter(f), 128);
+ id = "droid" + System.currentTimeMillis();
+ w.write(id);
+ w.close();
+ sDeviceId = id;
+ return id;
+ }
+ } catch (IOException e) {
+ }
+ throw new IOException("Can't get device name");
+ }
+
+ @Override
+ public IBinder onBind(Intent arg0) {
+ return mBinder;
+ }
+
+ /**
+ * Note that there are two ways the EAS SyncManager service can be created:
+ *
+ * 1) as a background service instantiated via startService (which happens on boot, when the
+ * first EAS account is created, etc), in which case the service thread is spun up, mailboxes
+ * sync, etc. and
+ * 2) to execute an RPC call from the UI, in which case the background service will already be
+ * running most of the time (unless we're creating a first EAS account)
+ *
+ * If the running background service detects that there are no EAS accounts (on boot, if none
+ * were created, or afterward if the last remaining EAS account is deleted), it will call
+ * stopSelf() to terminate operation.
+ *
+ * The goal is to ensure that the background service is running at all times when there is at
+ * least one EAS account in existence
+ *
+ * Because there are edge cases in which our process can crash (typically, this has been seen
+ * in UI crashes, ANR's, etc.), it's possible for the UI to start up again without the
+ * background service having been started. We explicitly try to start the service in Welcome
+ * (to handle the case of the app having been reloaded). We also start the service on any
+ * startSync call (if it isn't already running)
+ */
+ @Override
+ public void onCreate() {
+ alwaysLog("!!! EAS SyncManager, onCreate");
+ if (INSTANCE == null) {
+ INSTANCE = this;
+ mResolver = getContentResolver();
+ mAccountObserver = new AccountObserver(mHandler);
+ mResolver.registerContentObserver(Account.CONTENT_URI, true, mAccountObserver);
+ mMailboxObserver = new MailboxObserver(mHandler);
+ mSyncedMessageObserver = new SyncedMessageObserver(mHandler);
+ mMessageObserver = new MessageObserver(mHandler);
+ mSyncStatusObserver = new EasSyncStatusObserver();
+ } else {
+ alwaysLog("!!! EAS SyncManager onCreated, but INSTANCE not null??");
+ }
+ if (sDeviceId == null) {
+ try {
+ getDeviceId(this);
+ } catch (IOException e) {
+ // We can't run in this situation
+ throw new RuntimeException();
+ }
+ }
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ alwaysLog("!!! EAS SyncManager, onStartCommand");
+ maybeStartSyncManagerThread();
+ if (sServiceThread == null) {
+ alwaysLog("!!! EAS SyncManager, stopping self");
+ stopSelf();
+ }
+ return Service.START_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ alwaysLog("!!! EAS SyncManager, onDestroy");
+ if (INSTANCE != null) {
+ INSTANCE = null;
+ mResolver.unregisterContentObserver(mAccountObserver);
+ mResolver = null;
+ mAccountObserver = null;
+ mMailboxObserver = null;
+ mSyncedMessageObserver = null;
+ mMessageObserver = null;
+ mSyncStatusObserver = null;
+ }
+ }
+
+ void maybeStartSyncManagerThread() {
+ // Start our thread...
+ // See if there are any EAS accounts; otherwise, just go away
+ if (EmailContent.count(this, HostAuth.CONTENT_URI, WHERE_PROTOCOL_EAS, null) > 0) {
+ if (sServiceThread == null || !sServiceThread.isAlive()) {
+ log(sServiceThread == null ? "Starting thread..." : "Restarting thread...");
+ sServiceThread = new Thread(this, "SyncManager");
+ sServiceThread.start();
+ }
+ }
+ }
+
+ static void checkSyncManagerServiceRunning() {
+ // Get the service thread running if it isn't
+ // This is a stopgap for cases in which SyncManager died (due to a crash somewhere in
+ // com.android.email) and hasn't been restarted
+ // See the comment for onCreate for details
+ if (INSTANCE == null) return;
+ if (sServiceThread == null) {
+ INSTANCE.alwaysLog("!!! checkSyncManagerServiceRunning; starting service...");
+ INSTANCE.startService(new Intent(INSTANCE, SyncManager.class));
+ }
+ }
+
+ static public ConnPerRoute sConnPerRoute = new ConnPerRoute() {
+ public int getMaxForRoute(HttpRoute route) {
+ return 8;
+ }
+ };
+
+ static public synchronized ClientConnectionManager getClientConnectionManager() {
+ if (sClientConnectionManager == null) {
+ // Create a registry for our three schemes; http and https will use built-in factories
+ SchemeRegistry registry = new SchemeRegistry();
+ registry.register(new Scheme("http",
+ PlainSocketFactory.getSocketFactory(), 80));
+ registry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));
+
+ // Create a new SSLSocketFactory for our "trusted ssl"
+ // Get the unsecure trust manager from the factory
+ X509TrustManager trustManager = TrustManagerFactory.get(null, false);
+ TrustManager[] trustManagers = new TrustManager[] {trustManager};
+ SSLContext sslcontext;
+ try {
+ sslcontext = SSLContext.getInstance("TLS");
+ sslcontext.init(null, trustManagers, null);
+ SSLContextImpl sslContext = new SSLContextImpl();
+ try {
+ sslContext.engineInit(null, trustManagers, null, null, null);
+ } catch (KeyManagementException e) {
+ throw new AssertionError(e);
+ }
+ // Ok, now make our factory
+ SSLSocketFactory sf = new SSLSocketFactory(sslContext.engineGetSocketFactory());
+ sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
+ // Register the httpts scheme with our factory
+ registry.register(new Scheme("httpts", sf, 443));
+ // And create a ccm with our registry
+ HttpParams params = new BasicHttpParams();
+ params.setIntParameter(ConnManagerPNames.MAX_TOTAL_CONNECTIONS, 25);
+ params.setParameter(ConnManagerPNames.MAX_CONNECTIONS_PER_ROUTE, sConnPerRoute);
+ sClientConnectionManager = new ThreadSafeClientConnManager(params, registry);
+ } catch (NoSuchAlgorithmException e2) {
+ } catch (KeyManagementException e1) {
+ }
+ }
+ // Null is a valid return result if we get an exception
+ return sClientConnectionManager;
+ }
+
+ public static void stopAccountSyncs(long acctId) {
+ if (INSTANCE != null) {
+ INSTANCE.stopAccountSyncs(acctId, true);
+ }
+ }
+
+ private void stopAccountSyncs(long acctId, boolean includeAccountMailbox) {
+ synchronized (sSyncToken) {
+ List<Long> deletedBoxes = new ArrayList<Long>();
+ for (Long mid : INSTANCE.mServiceMap.keySet()) {
+ Mailbox box = Mailbox.restoreMailboxWithId(INSTANCE, mid);
+ if (box != null) {
+ if (box.mAccountKey == acctId) {
+ if (!includeAccountMailbox &&
+ box.mType == Mailbox.TYPE_EAS_ACCOUNT_MAILBOX) {
+ AbstractSyncService svc = INSTANCE.mServiceMap.get(mid);
+ if (svc != null) {
+ svc.stop();
+ }
+ continue;
+ }
+ AbstractSyncService svc = INSTANCE.mServiceMap.get(mid);
+ if (svc != null) {
+ svc.stop();
+ Thread t = svc.mThread;
+ if (t != null) {
+ t.interrupt();
+ }
+ }
+ deletedBoxes.add(mid);
+ }
+ }
+ }
+ for (Long mid : deletedBoxes) {
+ releaseMailbox(mid);
+ }
+ }
+ }
+
+ static public void reloadFolderList(Context context, long accountId, boolean force) {
+ if (INSTANCE == null) return;
+ Cursor c = context.getContentResolver().query(Mailbox.CONTENT_URI,
+ Mailbox.CONTENT_PROJECTION, MailboxColumns.ACCOUNT_KEY + "=? AND " +
+ MailboxColumns.TYPE + "=?",
+ new String[] {Long.toString(accountId),
+ Long.toString(Mailbox.TYPE_EAS_ACCOUNT_MAILBOX)}, null);
+ try {
+ if (c.moveToFirst()) {
+ synchronized(sSyncToken) {
+ Mailbox m = new Mailbox().restore(c);
+ Account acct = Account.restoreAccountWithId(context, accountId);
+ if (acct == null) {
+ return;
+ }
+ String syncKey = acct.mSyncKey;
+ // No need to reload the list if we don't have one
+ if (!force && (syncKey == null || syncKey.equals("0"))) {
+ return;
+ }
+
+ // Change all ping/push boxes to push/hold
+ ContentValues cv = new ContentValues();
+ cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH_HOLD);
+ context.getContentResolver().update(Mailbox.CONTENT_URI, cv,
+ WHERE_PUSH_OR_PING_NOT_ACCOUNT_MAILBOX,
+ new String[] {Long.toString(accountId)});
+ INSTANCE.log("Set push/ping boxes to push/hold");
+
+ long id = m.mId;
+ AbstractSyncService svc = INSTANCE.mServiceMap.get(id);
+ // Tell the service we're done
+ if (svc != null) {
+ synchronized (svc.getSynchronizer()) {
+ svc.stop();
+ }
+ // Interrupt the thread so that it can stop
+ Thread thread = svc.mThread;
+ thread.setName(thread.getName() + " (Stopped)");
+ thread.interrupt();
+ // Abandon the service
+ INSTANCE.releaseMailbox(id);
+ // And have it start naturally
+ kick("reload folder list");
+ }
+ }
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ /**
+ * Informs SyncManager that an account has a new folder list; as a result, any existing folder
+ * might have become invalid. Therefore, we act as if the account has been deleted, and then
+ * we reinitialize it.
+ *
+ * @param acctId
+ */
+ static public void folderListReloaded(long acctId) {
+ if (INSTANCE != null) {
+ INSTANCE.stopAccountSyncs(acctId, false);
+ kick("reload folder list");
+ }
+ }
+
+// private void logLocks(String str) {
+// StringBuilder sb = new StringBuilder(str);
+// boolean first = true;
+// for (long id: mWakeLocks.keySet()) {
+// if (!first) {
+// sb.append(", ");
+// } else {
+// first = false;
+// }
+// sb.append(id);
+// }
+// log(sb.toString());
+// }
+
+ private void acquireWakeLock(long id) {
+ synchronized (mWakeLocks) {
+ Boolean lock = mWakeLocks.get(id);
+ if (lock == null) {
+ if (id > 0) {
+ //log("+WakeLock requested for " + alarmOwner(id));
+ }
+ if (mWakeLock == null) {
+ PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
+ mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MAIL_SERVICE");
+ mWakeLock.acquire();
+ log("+WAKE LOCK ACQUIRED");
+ }
+ mWakeLocks.put(id, true);
+ //logLocks("Post-acquire of WakeLock for " + alarmOwner(id) + ": ");
+ }
+ }
+ }
+
+ private void releaseWakeLock(long id) {
+ synchronized (mWakeLocks) {
+ Boolean lock = mWakeLocks.get(id);
+ if (lock != null) {
+ if (id > 0) {
+ //log("+WakeLock not needed for " + alarmOwner(id));
+ }
+ mWakeLocks.remove(id);
+ if (mWakeLocks.isEmpty()) {
+ if (mWakeLock != null) {
+ mWakeLock.release();
+ }
+ mWakeLock = null;
+ log("+WAKE LOCK RELEASED");
+ } else {
+ //logLocks("Post-release of WakeLock for " + alarmOwner(id) + ": ");
+ }
+ }
+ }
+ }
+
+ static public String alarmOwner(long id) {
+ if (id == SYNC_MANAGER_ID) {
+ return "SyncManager";
+ } else {
+ String name = Long.toString(id);
+ if (Eas.USER_LOG && INSTANCE != null) {
+ Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, id);
+ if (m != null) {
+ name = m.mDisplayName + '(' + m.mAccountKey + ')';
+ }
+ }
+ return "Mailbox " + name;
+ }
+ }
+
+ private void clearAlarm(long id) {
+ synchronized (mPendingIntents) {
+ PendingIntent pi = mPendingIntents.get(id);
+ if (pi != null) {
+ AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
+ alarmManager.cancel(pi);
+ log("+Alarm cleared for " + alarmOwner(id));
+ mPendingIntents.remove(id);
+ }
+ }
+ }
+
+ private void setAlarm(long id, long millis) {
+ synchronized (mPendingIntents) {
+ PendingIntent pi = mPendingIntents.get(id);
+ if (pi == null) {
+ Intent i = new Intent(this, MailboxAlarmReceiver.class);
+ i.putExtra("mailbox", id);
+ i.setData(Uri.parse("Box" + id));
+ pi = PendingIntent.getBroadcast(this, 0, i, 0);
+ mPendingIntents.put(id, pi);
+
+ AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
+ alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + millis, pi);
+ log("+Alarm set for " + alarmOwner(id) + ", " + millis/1000 + "s");
+ }
+ }
+ }
+
+ private void clearAlarms() {
+ AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
+ synchronized (mPendingIntents) {
+ for (PendingIntent pi : mPendingIntents.values()) {
+ alarmManager.cancel(pi);
+ }
+ mPendingIntents.clear();
+ }
+ }
+
+ static public void runAwake(long id) {
+ if (INSTANCE == null) return;
+ INSTANCE.acquireWakeLock(id);
+ INSTANCE.clearAlarm(id);
+ }
+
+ static public void runAsleep(long id, long millis) {
+ if (INSTANCE == null) return;
+ INSTANCE.setAlarm(id, millis);
+ INSTANCE.releaseWakeLock(id);
+ }
+
+ static public void ping(Context context, long id) {
+ checkSyncManagerServiceRunning();
+ if (id < 0) {
+ kick("ping SyncManager");
+ } else if (INSTANCE == null) {
+ context.startService(new Intent(context, SyncManager.class));
+ } else {
+ AbstractSyncService service = INSTANCE.mServiceMap.get(id);
+ if (service != null) {
+ Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, id);
+ if (m != null) {
+ // We ignore drafts completely (doesn't sync). Changes in Outbox are handled
+ // in the checkMailboxes loop, so we can ignore these pings.
+ if (m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX) {
+ return;
+ }
+ service.mAccount = Account.restoreAccountWithId(INSTANCE, m.mAccountKey);
+ service.mMailbox = m;
+ service.ping();
+ }
+ }
+ }
+ }
+
+ /**
+ * See if we need to change the syncInterval for any of our PIM mailboxes based on changes
+ * to settings in the AccountManager (sync settings).
+ * This code is called 1) when SyncManager starts, and 2) when SyncManager is running and there
+ * are changes made (this is detected via a SyncStatusObserver)
+ */
+ private void checkPIMSyncSettings() {
+ ContentValues cv = new ContentValues();
+ // For now, just Contacts
+ // First, walk through our list of accounts
+ List<Account> easAccounts = getAccountList();
+ for (Account easAccount: easAccounts) {
+ // Find the contacts mailbox
+ long contactsId =
+ Mailbox.findMailboxOfType(this, easAccount.mId, Mailbox.TYPE_CONTACTS);
+ // Presumably there is one, but if not, it's ok. Just move on...
+ if (contactsId != Mailbox.NO_MAILBOX) {
+ // Create an AccountManager style Account
+ android.accounts.Account acct =
+ new android.accounts.Account(easAccount.mEmailAddress,
+ Eas.ACCOUNT_MANAGER_TYPE);
+ // Get the Contacts mailbox; this happens rarely so it's ok to get it all
+ Mailbox contacts = Mailbox.restoreMailboxWithId(this, contactsId);
+ int syncInterval = contacts.mSyncInterval;
+ // If we're syncable, look further...
+ if (ContentResolver.getIsSyncable(acct, ContactsContract.AUTHORITY) > 0) {
+ // If we're supposed to sync automatically (push), set to push if it's not
+ if (ContentResolver.getSyncAutomatically(acct, ContactsContract.AUTHORITY)) {
+ if (syncInterval == Mailbox.CHECK_INTERVAL_NEVER || syncInterval > 0) {
+ log("Sync setting: Contacts for " + acct.name + " changed to push");
+ cv.put(MailboxColumns.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PUSH);
+ }
+ // If we're NOT supposed to push, and we're not set up that way, change it
+ } else if (syncInterval != Mailbox.CHECK_INTERVAL_NEVER) {
+ log("Sync setting: Contacts for " + acct.name + " changed to manual");
+ cv.put(MailboxColumns.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_NEVER);
+ }
+ // If not, set it to never check
+ } else if (syncInterval != Mailbox.CHECK_INTERVAL_NEVER) {
+ log("Sync setting: Contacts for " + acct.name + " changed to manual");
+ cv.put(MailboxColumns.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_NEVER);
+ }
+
+ // If we've made a change, update the Mailbox, and kick
+ if (cv.containsKey(MailboxColumns.SYNC_INTERVAL)) {
+ mResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, contactsId),
+ cv,null, null);
+ kick("sync settings change");
+ }
+ }
+ }
+ }
+
+ private void checkWithAccountManager() {
+ android.accounts.Account[] accts =
+ AccountManager.get(this).getAccountsByType(Eas.ACCOUNT_MANAGER_TYPE);
+ List<Account> easAccounts = getAccountList();
+ for (Account easAccount: easAccounts) {
+ String accountName = easAccount.mEmailAddress;
+ boolean found = false;
+ for (android.accounts.Account acct: accts) {
+ if (acct.name.equalsIgnoreCase(accountName)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ // This account has been deleted in the AccountManager!
+ log("Account deleted in AccountManager; deleting from provider: " + accountName);
+ // TODO This will orphan downloaded attachments; need to handle this
+ mResolver.delete(ContentUris.withAppendedId(Account.CONTENT_URI, easAccount.mId),
+ null, null);
+ }
+ }
+ }
+
+ private void releaseConnectivityLock(String reason) {
+ // Clear our sync error map when we get connected
+ mSyncErrorMap.clear();
+ synchronized (sConnectivityLock) {
+ sConnectivityLock.notifyAll();
+ }
+ kick(reason);
+ }
+
+ public class ConnectivityReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Bundle b = intent.getExtras();
+ if (b != null) {
+ NetworkInfo a = (NetworkInfo)b.get(ConnectivityManager.EXTRA_NETWORK_INFO);
+ String info = "Connectivity alert for " + a.getTypeName();
+ State state = a.getState();
+ if (state == State.CONNECTED) {
+ info += " CONNECTED";
+ log(info);
+ releaseConnectivityLock("connected");
+ } else if (state == State.DISCONNECTED) {
+ info += " DISCONNECTED";
+ a = (NetworkInfo)b.get(ConnectivityManager.EXTRA_OTHER_NETWORK_INFO);
+ if (a != null && a.getState() == State.CONNECTED) {
+ info += " (OTHER CONNECTED)";
+ releaseConnectivityLock("disconnect/other");
+ ConnectivityManager cm =
+ (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
+ if (cm != null) {
+ NetworkInfo i = cm.getActiveNetworkInfo();
+ if (i == null || i.getState() != State.CONNECTED) {
+ log("CM says we're connected, but no active info?");
+ }
+ }
+ } else {
+ log(info);
+ kick("disconnected");
+ }
+ }
+ }
+ }
+ }
+
+ private void startService(AbstractSyncService service, Mailbox m) {
+ synchronized (sSyncToken) {
+ String mailboxName = m.mDisplayName;
+ String accountName = service.mAccount.mDisplayName;
+ Thread thread = new Thread(service, mailboxName + "(" + accountName + ")");
+ log("Starting thread for " + mailboxName + " in account " + accountName);
+ thread.start();
+ mServiceMap.put(m.mId, service);
+ runAwake(m.mId);
+ }
+ }
+
+ private void startService(Mailbox m, int reason, PartRequest req) {
+ // Don't sync if there's no connectivity
+ if (sConnectivityHold) return;
+ synchronized (sSyncToken) {
+ Account acct = Account.restoreAccountWithId(this, m.mAccountKey);
+ if (acct != null) {
+ AbstractSyncService service;
+ service = new EasSyncService(this, m);
+ service.mSyncReason = reason;
+ if (req != null) {
+ service.addPartRequest(req);
+ }
+ startService(service, m);
+ }
+ }
+ }
+
+ private void stopServices() {
+ synchronized (sSyncToken) {
+ ArrayList<Long> toStop = new ArrayList<Long>();
+
+ // Keep track of which services to stop
+ for (Long mailboxId : mServiceMap.keySet()) {
+ toStop.add(mailboxId);
+ }
+
+ // Shut down all of those running services
+ for (Long mailboxId : toStop) {
+ AbstractSyncService svc = mServiceMap.get(mailboxId);
+ if (svc != null) {
+ log("Stopping " + svc.mAccount.mDisplayName + '/' + svc.mMailbox.mDisplayName);
+ svc.stop();
+ if (svc.mThread != null) {
+ svc.mThread.interrupt();
+ }
+ }
+ releaseWakeLock(mailboxId);
+ }
+ }
+ }
+
+ private void waitForConnectivity() {
+ int cnt = 0;
+ while (!mStop) {
+ ConnectivityManager cm =
+ (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo info = cm.getActiveNetworkInfo();
+ if (info != null) {
+ //log("NetworkInfo: " + info.getTypeName() + ", " + info.getState().name());
+ return;
+ } else {
+
+ // If we're waiting for the long haul, shut down running service threads
+ if (++cnt > 1) {
+ stopServices();
+ }
+
+ // Wait until a network is connected, but let the device sleep
+ // We'll set an alarm just in case we don't get notified (bugs happen)
+ synchronized (sConnectivityLock) {
+ runAsleep(SYNC_MANAGER_ID, CONNECTIVITY_WAIT_TIME+5*SECONDS);
+ try {
+ log("Connectivity lock...");
+ sConnectivityHold = true;
+ sConnectivityLock.wait(CONNECTIVITY_WAIT_TIME);
+ log("Connectivity lock released...");
+ } catch (InterruptedException e) {
+ } finally {
+ sConnectivityHold = false;
+ }
+ runAwake(SYNC_MANAGER_ID);
+ }
+ }
+ }
+ }
+
+ public void run() {
+ mStop = false;
+
+ // If we're really debugging, turn on all logging
+ if (Eas.DEBUG) {
+ Eas.USER_LOG = true;
+ Eas.PARSER_LOG = true;
+ Eas.FILE_LOG = true;
+ }
+
+ // If we need to wait for the debugger, do so
+ if (Eas.WAIT_DEBUG) {
+ Debug.waitForDebugger();
+ }
+
+ // Set up our observers; we need them to know when to start/stop various syncs based
+ // on the insert/delete/update of mailboxes and accounts
+ // We also observe synced messages to trigger upsyncs at the appropriate time
+ mResolver.registerContentObserver(Mailbox.CONTENT_URI, false, mMailboxObserver);
+ mResolver.registerContentObserver(Message.SYNCED_CONTENT_URI, true, mSyncedMessageObserver);
+ mResolver.registerContentObserver(Message.CONTENT_URI, true, mMessageObserver);
+ ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS,
+ mSyncStatusObserver);
+ mAccountsUpdatedListener = new EasAccountsUpdatedListener();
+ AccountManager.get(getApplication())
+ .addOnAccountsUpdatedListener(mAccountsUpdatedListener, mHandler, true);
+
+ mConnectivityReceiver = new ConnectivityReceiver();
+ registerReceiver(mConnectivityReceiver,
+ new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
+
+ // See if any settings have changed while we weren't running...
+ checkPIMSyncSettings();
+
+ try {
+ while (!mStop) {
+ runAwake(SYNC_MANAGER_ID);
+ waitForConnectivity();
+ mNextWaitReason = "Heartbeat";
+ long nextWait = checkMailboxes();
+ try {
+ synchronized (this) {
+ if (!mKicked) {
+ if (nextWait < 0) {
+ log("Negative wait? Setting to 1s");
+ nextWait = 1*SECONDS;
+ }
+ if (nextWait > 10*SECONDS) {
+ log("Next awake in " + nextWait / 1000 + "s: " + mNextWaitReason);
+ runAsleep(SYNC_MANAGER_ID, nextWait + (3*SECONDS));
+ }
+ wait(nextWait);
+ }
+ }
+ } catch (InterruptedException e) {
+ // Needs to be caught, but causes no problem
+ } finally {
+ synchronized (this) {
+ if (mKicked) {
+ //log("Wait deferred due to kick");
+ mKicked = false;
+ }
+ }
+ }
+ }
+ stopServices();
+ log("Shutdown requested");
+ } finally {
+ // Lots of cleanup here
+ // Stop our running syncs
+ stopServices();
+
+ // Stop receivers and content observers
+ if (mConnectivityReceiver != null) {
+ unregisterReceiver(mConnectivityReceiver);
+ }
+
+ if (INSTANCE != null) {
+ ContentResolver resolver = getContentResolver();
+ resolver.unregisterContentObserver(mAccountObserver);
+ resolver.unregisterContentObserver(mMailboxObserver);
+ resolver.unregisterContentObserver(mSyncedMessageObserver);
+ resolver.unregisterContentObserver(mMessageObserver);
+ }
+ // Don't leak the Intent associated with this listener
+ if (mAccountsUpdatedListener != null) {
+ AccountManager.get(this).removeOnAccountsUpdatedListener(mAccountsUpdatedListener);
+ mAccountsUpdatedListener = null;
+ }
+
+ // Clear pending alarms and associated Intents
+ clearAlarms();
+
+ // Release our wake lock, if we have one
+ synchronized (mWakeLocks) {
+ if (mWakeLock != null) {
+ mWakeLock.release();
+ mWakeLock = null;
+ }
+ }
+
+ log("Goodbye");
+ }
+
+ if (!mStop) {
+ // If this wasn't intentional, try to restart the service
+ throw new RuntimeException("EAS SyncManager crash; please restart me...");
+ }
+ }
+
+ private void releaseMailbox(long mailboxId) {
+ mServiceMap.remove(mailboxId);
+ releaseWakeLock(mailboxId);
+ }
+
+ private long checkMailboxes () {
+ // First, see if any running mailboxes have been deleted
+ ArrayList<Long> deletedMailboxes = new ArrayList<Long>();
+ synchronized (sSyncToken) {
+ for (long mailboxId: mServiceMap.keySet()) {
+ Mailbox m = Mailbox.restoreMailboxWithId(this, mailboxId);
+ if (m == null) {
+ deletedMailboxes.add(mailboxId);
+ }
+ }
+ }
+ // If so, stop them or remove them from the map
+ for (Long mailboxId: deletedMailboxes) {
+ AbstractSyncService svc = mServiceMap.get(mailboxId);
+ if (svc != null) {
+ boolean alive = svc.mThread.isAlive();
+ log("Deleted mailbox: " + svc.mMailboxName);
+ if (alive) {
+ stopManualSync(mailboxId);
+ } else {
+ log("Removing from serviceMap");
+ releaseMailbox(mailboxId);
+ }
+ }
+ }
+
+ long nextWait = SYNC_MANAGER_HEARTBEAT_TIME;
+ long now = System.currentTimeMillis();
+
+ // Start up threads that need it; use a query which finds eas mailboxes where the
+ // the sync interval is not "never". This is the set of mailboxes that we control
+ Cursor c = getContentResolver().query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
+ mAccountObserver.getSyncableEasMailboxWhere(), null, null);
+
+ try {
+ while (c.moveToNext()) {
+ long mid = c.getLong(Mailbox.CONTENT_ID_COLUMN);
+ AbstractSyncService service = mServiceMap.get(mid);
+ if (service == null) {
+ // Check whether we're in a hold (temporary or permanent)
+ SyncError syncError = mSyncErrorMap.get(mid);
+ if (syncError != null) {
+ // Nothing we can do about fatal errors
+ if (syncError.fatal) continue;
+ if (now < syncError.holdEndTime) {
+ // If release time is earlier than next wait time,
+ // move next wait time up to the release time
+ if (syncError.holdEndTime < now + nextWait) {
+ nextWait = syncError.holdEndTime - now;
+ mNextWaitReason = "Release hold";
+ }
+ continue;
+ } else {
+ // Keep the error around, but clear the end time
+ syncError.holdEndTime = 0;
+ }
+ }
+
+ // We handle a few types of mailboxes specially
+ int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
+ if (type == Mailbox.TYPE_CONTACTS) {
+ // See if "sync automatically" is set
+ Account account =
+ getAccountById(c.getInt(Mailbox.CONTENT_ACCOUNT_KEY_COLUMN));
+ if (account != null) {
+ android.accounts.Account a =
+ new android.accounts.Account(account.mEmailAddress,
+ Eas.ACCOUNT_MANAGER_TYPE);
+ if (!ContentResolver.getSyncAutomatically(a,
+ ContactsContract.AUTHORITY)) {
+ continue;
+ }
+ }
+ } else if (type == Mailbox.TYPE_TRASH) {
+ continue;
+ }
+
+ // Otherwise, we use the sync interval
+ long interval = c.getInt(Mailbox.CONTENT_SYNC_INTERVAL_COLUMN);
+ if (interval == Mailbox.CHECK_INTERVAL_PUSH) {
+ Mailbox m = EmailContent.getContent(c, Mailbox.class);
+ startService(m, SYNC_PUSH, null);
+ } else if (type == Mailbox.TYPE_OUTBOX) {
+ int cnt = EmailContent.count(this, Message.CONTENT_URI,
+ EasOutboxService.MAILBOX_KEY_AND_NOT_SEND_FAILED,
+ new String[] {Long.toString(mid)});
+ if (cnt > 0) {
+ Mailbox m = EmailContent.getContent(c, Mailbox.class);
+ startService(new EasOutboxService(this, m), m);
+ }
+ } else if (interval > 0 && interval <= ONE_DAY_MINUTES) {
+ long lastSync = c.getLong(Mailbox.CONTENT_SYNC_TIME_COLUMN);
+ long sinceLastSync = now - lastSync;
+ if (sinceLastSync < 0) {
+ log("WHOA! lastSync in the future for mailbox: " + mid);
+ sinceLastSync = interval*MINUTES;
+ }
+ long toNextSync = interval*MINUTES - sinceLastSync;
+ String name = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
+ if (toNextSync <= 0) {
+ Mailbox m = EmailContent.getContent(c, Mailbox.class);
+ startService(m, SYNC_SCHEDULED, null);
+ } else if (toNextSync < nextWait) {
+ nextWait = toNextSync;
+ if (Eas.USER_LOG) {
+ log("Next sync for " + name + " in " + nextWait/1000 + "s");
+ }
+ mNextWaitReason = "Scheduled sync, " + name;
+ } else if (Eas.USER_LOG) {
+ log("Next sync for " + name + " in " + toNextSync/1000 + "s");
+ }
+ }
+ } else {
+ Thread thread = service.mThread;
+ // Look for threads that have died but aren't in an error state
+ if (thread != null && !thread.isAlive() && !mSyncErrorMap.containsKey(mid)) {
+ releaseMailbox(mid);
+ // Restart this if necessary
+ if (nextWait > 3*SECONDS) {
+ nextWait = 3*SECONDS;
+ mNextWaitReason = "Clean up dead thread(s)";
+ }
+ } else {
+ long requestTime = service.mRequestTime;
+ if (requestTime > 0) {
+ long timeToRequest = requestTime - now;
+ if (service instanceof AbstractSyncService && timeToRequest <= 0) {
+ service.mRequestTime = 0;
+ service.ping();
+ } else if (requestTime > 0 && timeToRequest < nextWait) {
+ if (timeToRequest < 11*MINUTES) {
+ nextWait = timeToRequest < 250 ? 250 : timeToRequest;
+ mNextWaitReason = "Sync data change";
+ } else {
+ log("Illegal timeToRequest: " + timeToRequest);
+ }
+ }
+ }
+ }
+ }
+ }
+ } finally {
+ c.close();
+ }
+ return nextWait;
+ }
+
+ static public void serviceRequest(long mailboxId, int reason) {
+ serviceRequest(mailboxId, 5*SECONDS, reason);
+ }
+
+ static public void serviceRequest(long mailboxId, long ms, int reason) {
+ if (INSTANCE == null) return;
+ Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId);
+ // Never allow manual start of Drafts or Outbox via serviceRequest
+ if (m == null || m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_OUTBOX) {
+ INSTANCE.log("Ignoring serviceRequest for drafts/outbox");
+ return;
+ }
+ try {
+ AbstractSyncService service = INSTANCE.mServiceMap.get(mailboxId);
+ if (service != null) {
+ service.mRequestTime = System.currentTimeMillis() + ms;
+ kick("service request");
+ } else {
+ startManualSync(mailboxId, reason, null);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ static public void serviceRequestImmediate(long mailboxId) {
+ if (INSTANCE == null) return;
+ AbstractSyncService service = INSTANCE.mServiceMap.get(mailboxId);
+ if (service != null) {
+ service.mRequestTime = System.currentTimeMillis();
+ Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId);
+ if (m != null) {
+ service.mAccount = Account.restoreAccountWithId(INSTANCE, m.mAccountKey);
+ service.mMailbox = m;
+ kick("service request immediate");
+ }
+ }
+ }
+
+ static public void partRequest(PartRequest req) {
+ if (INSTANCE == null) return;
+ Message msg = Message.restoreMessageWithId(INSTANCE, req.emailId);
+ if (msg == null) {
+ return;
+ }
+ long mailboxId = msg.mMailboxKey;
+ AbstractSyncService service = INSTANCE.mServiceMap.get(mailboxId);
+
+ if (service == null) {
+ service = startManualSync(mailboxId, SYNC_SERVICE_PART_REQUEST, req);
+ kick("part request");
+ } else {
+ service.addPartRequest(req);
+ }
+ }
+
+ static public PartRequest hasPartRequest(long emailId, String part) {
+ if (INSTANCE == null) return null;
+ Message msg = Message.restoreMessageWithId(INSTANCE, emailId);
+ if (msg == null) {
+ return null;
+ }
+ long mailboxId = msg.mMailboxKey;
+ AbstractSyncService service = INSTANCE.mServiceMap.get(mailboxId);
+ if (service != null) {
+ return service.hasPartRequest(emailId, part);
+ }
+ return null;
+ }
+
+ static public void cancelPartRequest(long emailId, String part) {
+ Message msg = Message.restoreMessageWithId(INSTANCE, emailId);
+ if (msg == null) {
+ return;
+ }
+ long mailboxId = msg.mMailboxKey;
+ AbstractSyncService service = INSTANCE.mServiceMap.get(mailboxId);
+ if (service != null) {
+ service.cancelPartRequest(emailId, part);
+ }
+ }
+
+ /**
+ * Determine whether a given Mailbox can be synced, i.e. is not already syncing and is not in
+ * an error state
+ *
+ * @param mailboxId
+ * @return whether or not the Mailbox is available for syncing (i.e. is a valid push target)
+ */
+ static public int pingStatus(long mailboxId) {
+ // Already syncing...
+ if (INSTANCE.mServiceMap.get(mailboxId) != null) {
+ return PING_STATUS_RUNNING;
+ }
+ // No errors or a transient error, don't ping...
+ SyncError error = INSTANCE.mSyncErrorMap.get(mailboxId);
+ if (error != null) {
+ if (error.fatal) {
+ return PING_STATUS_UNABLE;
+ } else if (error.holdEndTime > 0) {
+ return PING_STATUS_WAITING;
+ }
+ }
+ return PING_STATUS_OK;
+ }
+
+ static public AbstractSyncService startManualSync(long mailboxId, int reason, PartRequest req) {
+ if (INSTANCE == null || INSTANCE.mServiceMap == null) return null;
+ synchronized (sSyncToken) {
+ if (INSTANCE.mServiceMap.get(mailboxId) == null) {
+ INSTANCE.mSyncErrorMap.remove(mailboxId);
+ Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId);
+ if (m != null) {
+ INSTANCE.log("Starting sync for " + m.mDisplayName);
+ INSTANCE.startService(m, reason, req);
+ }
+ }
+ }
+ return INSTANCE.mServiceMap.get(mailboxId);
+ }
+
+ // DO NOT CALL THIS IN A LOOP ON THE SERVICEMAP
+ static private void stopManualSync(long mailboxId) {
+ if (INSTANCE == null || INSTANCE.mServiceMap == null) return;
+ synchronized (sSyncToken) {
+ AbstractSyncService svc = INSTANCE.mServiceMap.get(mailboxId);
+ if (svc != null) {
+ INSTANCE.log("Stopping sync for " + svc.mMailboxName);
+ svc.stop();
+ svc.mThread.interrupt();
+ INSTANCE.releaseWakeLock(mailboxId);
+ }
+ }
+ }
+
+ /**
+ * Wake up SyncManager to check for mailboxes needing service
+ */
+ static public void kick(String reason) {
+ if (INSTANCE != null) {
+ synchronized (INSTANCE) {
+ INSTANCE.mKicked = true;
+ INSTANCE.notify();
+ }
+ }
+ if (sConnectivityLock != null) {
+ synchronized (sConnectivityLock) {
+ sConnectivityLock.notify();
+ }
+ }
+ }
+
+ static public void accountUpdated(long acctId) {
+ if (INSTANCE == null) return;
+ synchronized (sSyncToken) {
+ for (AbstractSyncService svc : INSTANCE.mServiceMap.values()) {
+ if (svc.mAccount.mId == acctId) {
+ svc.mAccount = Account.restoreAccountWithId(INSTANCE, acctId);
+ }
+ }
+ }
+ }
+
+ /**
+ * Sent by services indicating that their thread is finished; action depends on the exitStatus
+ * of the service.
+ *
+ * @param svc the service that is finished
+ */
+ static public void done(AbstractSyncService svc) {
+ if (INSTANCE == null) return;
+ synchronized(sSyncToken) {
+ long mailboxId = svc.mMailboxId;
+ HashMap<Long, SyncError> errorMap = INSTANCE.mSyncErrorMap;
+ SyncError syncError = errorMap.get(mailboxId);
+ INSTANCE.releaseMailbox(mailboxId);
+ int exitStatus = svc.mExitStatus;
+ switch (exitStatus) {
+ case AbstractSyncService.EXIT_DONE:
+ if (!svc.mPartRequests.isEmpty()) {
+ // TODO Handle this case
+ }
+ errorMap.remove(mailboxId);
+ break;
+ case AbstractSyncService.EXIT_IO_ERROR:
+ Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId);
+ if (syncError != null) {
+ syncError.escalate();
+ INSTANCE.log(m.mDisplayName + " held for " + syncError.holdDelay + "ms");
+ } else {
+ errorMap.put(mailboxId, INSTANCE.new SyncError(exitStatus, false));
+ INSTANCE.log(m.mDisplayName + " added to syncErrorMap, hold for 15s");
+ }
+ break;
+ case AbstractSyncService.EXIT_LOGIN_FAILURE:
+ case AbstractSyncService.EXIT_EXCEPTION:
+ errorMap.put(mailboxId, INSTANCE.new SyncError(exitStatus, true));
+ break;
+ }
+ kick("sync completed");
+ }
+ }
+
+ /**
+ * Given the status string from a Mailbox, return the type code for the last sync
+ * @param status the syncStatus column of a Mailbox
+ * @return
+ */
+ static public int getStatusType(String status) {
+ if (status == null) {
+ return -1;
+ } else {
+ return status.charAt(STATUS_TYPE_CHAR) - '0';
+ }
+ }
+
+ /**
+ * Given the status string from a Mailbox, return the change count for the last sync
+ * The change count is the number of adds + deletes + changes in the last sync
+ * @param status the syncStatus column of a Mailbox
+ * @return
+ */
+ static public int getStatusChangeCount(String status) {
+ try {
+ String s = status.substring(STATUS_CHANGE_COUNT_OFFSET);
+ return Integer.parseInt(s);
+ } catch (RuntimeException e) {
+ return -1;
+ }
+ }
+
+ static public Context getContext() {
+ return INSTANCE;
+ }
+}
diff --git a/src/com/android/exchange/adapter/AbstractSyncAdapter.java b/src/com/android/exchange/adapter/AbstractSyncAdapter.java
new file mode 100644
index 0000000..d6ad165
--- /dev/null
+++ b/src/com/android/exchange/adapter/AbstractSyncAdapter.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to 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.android.exchange.adapter;
+
+import com.android.email.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.exchange.EasSyncService;
+
+import android.content.Context;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Parent class of all sync adapters (EasMailbox, EasCalendar, and EasContacts)
+ *
+ */
+public abstract class AbstractSyncAdapter {
+
+ public static final int SECONDS = 1000;
+ public static final int MINUTES = SECONDS*60;
+ public static final int HOURS = MINUTES*60;
+ public static final int DAYS = HOURS*24;
+ public static final int WEEKS = DAYS*7;
+
+ public Mailbox mMailbox;
+ public EasSyncService mService;
+ public Context mContext;
+ public Account mAccount;
+
+ // Create the data for local changes that need to be sent up to the server
+ public abstract boolean sendLocalChanges(Serializer s)
+ throws IOException;
+ // Parse incoming data from the EAS server, creating, modifying, and deleting objects as
+ // required through the EmailProvider
+ public abstract boolean parse(InputStream is)
+ throws IOException;
+ // The name used to specify the collection type of the target (Email, Calendar, or Contacts)
+ public abstract String getCollectionName();
+ public abstract void cleanup();
+
+ public AbstractSyncAdapter(Mailbox mailbox, EasSyncService service) {
+ mMailbox = mailbox;
+ mService = service;
+ mContext = service.mContext;
+ mAccount = service.mAccount;
+ }
+
+ public void userLog(String ...strings) {
+ mService.userLog(strings);
+ }
+
+ public void incrementChangeCount() {
+ mService.mChangeCount++;
+ }
+
+ /**
+ * Returns the current SyncKey; override if the SyncKey is stored elsewhere (as for Contacts)
+ * @return the current SyncKey for the Mailbox
+ * @throws IOException
+ */
+ public String getSyncKey() throws IOException {
+ if (mMailbox.mSyncKey == null) {
+ userLog("Reset SyncKey to 0");
+ mMailbox.mSyncKey = "0";
+ }
+ return mMailbox.mSyncKey;
+ }
+
+ public void setSyncKey(String syncKey, boolean inCommands) throws IOException {
+ mMailbox.mSyncKey = syncKey;
+ }
+}
+
diff --git a/src/com/android/exchange/adapter/AbstractSyncParser.java b/src/com/android/exchange/adapter/AbstractSyncParser.java
new file mode 100644
index 0000000..97dfb50
--- /dev/null
+++ b/src/com/android/exchange/adapter/AbstractSyncParser.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to 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.android.exchange.adapter;
+
+import com.android.email.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.email.provider.EmailContent.MailboxColumns;
+import com.android.exchange.EasSyncService;
+import com.android.exchange.SyncManager;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Base class for the Email and PIM sync parsers
+ * Handles the basic flow of syncKeys, looping to get more data, handling errors, etc.
+ * Each subclass must implement a handful of methods that relate specifically to the data type
+ *
+ */
+public abstract class AbstractSyncParser extends Parser {
+
+ protected EasSyncService mService;
+ protected Mailbox mMailbox;
+ protected Account mAccount;
+ protected Context mContext;
+ protected ContentResolver mContentResolver;
+ protected AbstractSyncAdapter mAdapter;
+
+ public AbstractSyncParser(InputStream in, AbstractSyncAdapter adapter) throws IOException {
+ super(in);
+ mAdapter = adapter;
+ mService = adapter.mService;
+ mContext = mService.mContext;
+ mContentResolver = mContext.getContentResolver();
+ mMailbox = mService.mMailbox;
+ mAccount = mService.mAccount;
+ }
+
+ /**
+ * Read, parse, and act on incoming commands from the Exchange server
+ * @throws IOException if the connection is broken
+ */
+ public abstract void commandsParser() throws IOException;
+
+ /**
+ * Read, parse, and act on server responses
+ * @throws IOException
+ */
+ public abstract void responsesParser() throws IOException;
+
+ /**
+ * Commit any changes found during parsing
+ * @throws IOException
+ */
+ public abstract void commit() throws IOException;
+
+ /**
+ * Delete all records of this class in this account
+ */
+ public abstract void wipe();
+
+ /**
+ * Loop through the top-level structure coming from the Exchange server
+ * Sync keys and the more available flag are handled here, whereas specific data parsing
+ * is handled by abstract methods implemented for each data class (e.g. Email, Contacts, etc.)
+ */
+ @Override
+ public boolean parse() throws IOException {
+ int status;
+ boolean moreAvailable = false;
+ boolean newSyncKey = false;
+ int interval = mMailbox.mSyncInterval;
+
+ // If we're not at the top of the xml tree, throw an exception
+ if (nextTag(START_DOCUMENT) != Tags.SYNC_SYNC) {
+ throw new EasParserException();
+ }
+
+ boolean mailboxUpdated = false;
+ ContentValues cv = new ContentValues();
+
+ // Loop here through the remaining xml
+ while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
+ if (tag == Tags.SYNC_COLLECTION || tag == Tags.SYNC_COLLECTIONS) {
+ // Ignore these tags, since we've only got one collection syncing in this loop
+ } else if (tag == Tags.SYNC_STATUS) {
+ // Status = 1 is success; everything else is a failure
+ status = getValueInt();
+ if (status != 1) {
+ mService.errorLog("Sync failed: " + status);
+ // Status = 3 means invalid sync key
+ if (status == 3) {
+ // Must delete all of the data and start over with syncKey of "0"
+ mAdapter.setSyncKey("0", false);
+ // Make this a push box through the first sync
+ // TODO Make frequency conditional on user settings!
+ mMailbox.mSyncInterval = Mailbox.CHECK_INTERVAL_PUSH;
+ mService.errorLog("Bad sync key; RESET and delete data");
+ wipe();
+ // Indicate there's more so that we'll start syncing again
+ moreAvailable = true;
+ } else if (status == 8) {
+ // This is Bad; it means the server doesn't recognize the serverId it
+ // sent us. What's needed is a refresh of the folder list.
+ SyncManager.reloadFolderList(mContext, mAccount.mId, true);
+ }
+ // TODO Look at other error codes and consider what's to be done
+ }
+ } else if (tag == Tags.SYNC_COMMANDS) {
+ commandsParser();
+ } else if (tag == Tags.SYNC_RESPONSES) {
+ responsesParser();
+ } else if (tag == Tags.SYNC_MORE_AVAILABLE) {
+ moreAvailable = true;
+ } else if (tag == Tags.SYNC_SYNC_KEY) {
+ if (mAdapter.getSyncKey().equals("0")) {
+ moreAvailable = true;
+ }
+ String newKey = getValue();
+ userLog("Parsed key for ", mMailbox.mDisplayName, ": ", newKey);
+ if (!newKey.equals(mMailbox.mSyncKey)) {
+ mAdapter.setSyncKey(newKey, true);
+ cv.put(MailboxColumns.SYNC_KEY, newKey);
+ mailboxUpdated = true;
+ newSyncKey = true;
+ }
+ // If we were pushing (i.e. auto-start), now we'll become ping-triggered
+ if (mMailbox.mSyncInterval == Mailbox.CHECK_INTERVAL_PUSH) {
+ mMailbox.mSyncInterval = Mailbox.CHECK_INTERVAL_PING;
+ }
+ } else {
+ skipTag();
+ }
+ }
+
+ // If we don't have a new sync key, ignore moreAvailable (or we'll loop)
+ if (moreAvailable && !newSyncKey) {
+ userLog("!! SyncKey hasn't changed, setting moreAvailable = false");
+ moreAvailable = false;
+ }
+
+ // Commit any changes
+ commit();
+
+ boolean abortSyncs = false;
+
+ // If the sync interval has changed, we need to save it
+ if (mMailbox.mSyncInterval != interval) {
+ cv.put(MailboxColumns.SYNC_INTERVAL, mMailbox.mSyncInterval);
+ mailboxUpdated = true;
+ // If there are changes, and we were bounced from push/ping, try again
+ } else if (mService.mChangeCount > 0 &&
+ mAccount.mSyncInterval == Account.CHECK_INTERVAL_PUSH &&
+ mMailbox.mSyncInterval > 0) {
+ userLog("Changes found to ping loop mailbox ", mMailbox.mDisplayName, ": will ping.");
+ cv.put(MailboxColumns.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PING);
+ mailboxUpdated = true;
+ abortSyncs = true;
+ }
+
+ if (mailboxUpdated) {
+ synchronized (mService.getSynchronizer()) {
+ if (!mService.isStopped()) {
+ mMailbox.update(mContext, cv);
+ }
+ }
+ }
+
+ if (abortSyncs) {
+ userLog("Aborting account syncs due to mailbox change to ping...");
+ SyncManager.stopAccountSyncs(mAccount.mId);
+ }
+
+ // Let the caller know that there's more to do
+ userLog("Returning moreAvailable = " + moreAvailable);
+ return moreAvailable;
+ }
+
+ void userLog(String ...strings) {
+ mService.userLog(strings);
+ }
+
+ void userLog(String string, int num, String string2) {
+ mService.userLog(string, num, string2);
+ }
+}
diff --git a/src/com/android/exchange/adapter/AccountSyncAdapter.java b/src/com/android/exchange/adapter/AccountSyncAdapter.java
new file mode 100644
index 0000000..2a1c7be
--- /dev/null
+++ b/src/com/android/exchange/adapter/AccountSyncAdapter.java
@@ -0,0 +1,34 @@
+package com.android.exchange.adapter;
+
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.exchange.EasSyncService;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class AccountSyncAdapter extends AbstractSyncAdapter {
+
+ public AccountSyncAdapter(Mailbox mailbox, EasSyncService service) {
+ super(mailbox, service);
+ }
+
+ @Override
+ public void cleanup() {
+ }
+
+ @Override
+ public String getCollectionName() {
+ return null;
+ }
+
+ @Override
+ public boolean parse(InputStream is) throws IOException {
+ return false;
+ }
+
+ @Override
+ public boolean sendLocalChanges(Serializer s) throws IOException {
+ return false;
+ }
+
+}
diff --git a/src/com/android/exchange/adapter/CalendarSyncAdapter.java b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
new file mode 100644
index 0000000..739c9e2
--- /dev/null
+++ b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to 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.android.exchange.adapter;
+
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.exchange.EasSyncService;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Sync adapter class for EAS calendars
+ *
+ */
+public class CalendarSyncAdapter extends AbstractSyncAdapter {
+
+ public CalendarSyncAdapter(Mailbox mailbox, EasSyncService service) {
+ super(mailbox, service);
+ }
+
+ @Override
+ public String getCollectionName() {
+ return "Calendar";
+ }
+
+ @Override
+ public boolean sendLocalChanges(Serializer s) throws IOException {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ @Override
+ public void cleanup() {
+ // TODO Auto-generated method stub
+ }
+
+ @Override
+ public boolean parse(InputStream is) throws IOException {
+ // TODO Auto-generated method stub
+ return false;
+ }
+}
diff --git a/src/com/android/exchange/adapter/ContactsSyncAdapter.java b/src/com/android/exchange/adapter/ContactsSyncAdapter.java
new file mode 100644
index 0000000..fff6400
--- /dev/null
+++ b/src/com/android/exchange/adapter/ContactsSyncAdapter.java
@@ -0,0 +1,1909 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to 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.android.exchange.adapter;
+
+import com.android.email.codec.binary.Base64;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.exchange.Eas;
+import com.android.exchange.EasSyncService;
+
+import android.content.ContentProviderClient;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Entity;
+import android.content.EntityIterator;
+import android.content.OperationApplicationException;
+import android.content.ContentProviderOperation.Builder;
+import android.content.Entity.NamedContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.provider.SyncStateContract;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.Settings;
+import android.provider.ContactsContract.SyncState;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.text.util.Rfc822Token;
+import android.text.util.Rfc822Tokenizer;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+/**
+ * Sync adapter for EAS Contacts
+ *
+ */
+public class ContactsSyncAdapter extends AbstractSyncAdapter {
+
+ private static final String TAG = "EasContactsSyncAdapter";
+ private static final String SERVER_ID_SELECTION = RawContacts.SOURCE_ID + "=?";
+ private static final String CLIENT_ID_SELECTION = RawContacts.SYNC1 + "=?";
+ private static final String[] ID_PROJECTION = new String[] {RawContacts._ID};
+ private static final String[] GROUP_PROJECTION = new String[] {Groups.SOURCE_ID};
+
+ private static final ArrayList<NamedContentValues> EMPTY_ARRAY_NAMEDCONTENTVALUES
+ = new ArrayList<NamedContentValues>();
+
+ private static final String FOUND_DATA_ROW = "com.android.exchange.FOUND_ROW";
+
+ private static final int[] HOME_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY,
+ Tags.CONTACTS_HOME_ADDRESS_COUNTRY,
+ Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE,
+ Tags.CONTACTS_HOME_ADDRESS_STATE,
+ Tags.CONTACTS_HOME_ADDRESS_STREET};
+
+ private static final int[] WORK_ADDRESS_TAGS = new int[] {Tags.CONTACTS_BUSINESS_ADDRESS_CITY,
+ Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY,
+ Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE,
+ Tags.CONTACTS_BUSINESS_ADDRESS_STATE,
+ Tags.CONTACTS_BUSINESS_ADDRESS_STREET};
+
+ private static final int[] OTHER_ADDRESS_TAGS = new int[] {Tags.CONTACTS_HOME_ADDRESS_CITY,
+ Tags.CONTACTS_OTHER_ADDRESS_COUNTRY,
+ Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE,
+ Tags.CONTACTS_OTHER_ADDRESS_STATE,
+ Tags.CONTACTS_OTHER_ADDRESS_STREET};
+
+ private static final int MAX_IM_ROWS = 3;
+ private static final int MAX_EMAIL_ROWS = 3;
+ private static final int MAX_PHONE_ROWS = 2;
+ private static final String COMMON_DATA_ROW = Im.DATA; // Could have been Email.DATA, etc.
+ private static final String COMMON_TYPE_ROW = Phone.TYPE; // Could have been any typed row
+
+ private static final int[] IM_TAGS = new int[] {Tags.CONTACTS2_IM_ADDRESS,
+ Tags.CONTACTS2_IM_ADDRESS_2, Tags.CONTACTS2_IM_ADDRESS_3};
+
+ private static final int[] EMAIL_TAGS = new int[] {Tags.CONTACTS_EMAIL1_ADDRESS,
+ Tags.CONTACTS_EMAIL2_ADDRESS, Tags.CONTACTS_EMAIL3_ADDRESS};
+
+ private static final int[] WORK_PHONE_TAGS = new int[] {Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER,
+ Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER};
+
+ private static final int[] HOME_PHONE_TAGS = new int[] {Tags.CONTACTS_HOME_TELEPHONE_NUMBER,
+ Tags.CONTACTS_HOME2_TELEPHONE_NUMBER};
+
+ ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
+ ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
+
+ private boolean mGroupsUsed = false;
+
+ private android.accounts.Account mAccountManagerAccount;
+
+ public ContactsSyncAdapter(Mailbox mailbox, EasSyncService service) {
+ super(mailbox, service);
+ }
+
+ static Uri addCallerIsSyncAdapterParameter(Uri uri) {
+ return uri.buildUpon()
+ .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
+ .build();
+ }
+
+ @Override
+ public boolean parse(InputStream is) throws IOException {
+ EasContactsSyncParser p = new EasContactsSyncParser(is, this);
+ return p.parse();
+ }
+
+ interface UntypedRow {
+ public void addValues(RowBuilder builder);
+ public boolean isSameAs(int type, String value);
+ }
+
+ /**
+ * We get our SyncKey from ContactsProvider. If there's not one, we set it to "0" (the reset
+ * state) and save that away.
+ */
+ @Override
+ public String getSyncKey() throws IOException {
+ ContentProviderClient client =
+ mService.mContentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
+ try {
+ byte[] data = SyncStateContract.Helpers.get(client,
+ ContactsContract.SyncState.CONTENT_URI, getAccountManagerAccount());
+ if (data == null || data.length == 0) {
+ // Initialize the SyncKey
+ setSyncKey("0", false);
+ // Make sure ungrouped contacts for Exchange are defaultly visible
+ ContentValues cv = new ContentValues();
+ cv.put(Groups.ACCOUNT_NAME, mAccount.mEmailAddress);
+ cv.put(Groups.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE);
+ cv.put(Settings.UNGROUPED_VISIBLE, true);
+ client.insert(addCallerIsSyncAdapterParameter(Settings.CONTENT_URI), cv);
+ return "0";
+ } else {
+ return new String(data);
+ }
+ } catch (RemoteException e) {
+ throw new IOException("Can't get SyncKey from ContactsProvider");
+ }
+ }
+
+ /**
+ * We only need to set this when we're forced to make the SyncKey "0" (a reset). In all other
+ * cases, the SyncKey is set within ContactOperations
+ */
+ @Override
+ public void setSyncKey(String syncKey, boolean inCommands) throws IOException {
+ if ("0".equals(syncKey) || !inCommands) {
+ ContentProviderClient client =
+ mService.mContentResolver
+ .acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
+ try {
+ SyncStateContract.Helpers.set(client, ContactsContract.SyncState.CONTENT_URI,
+ getAccountManagerAccount(), syncKey.getBytes());
+ userLog("SyncKey set to ", syncKey, " in ContactsProvider");
+ } catch (RemoteException e) {
+ throw new IOException("Can't set SyncKey in ContactsProvider");
+ }
+ }
+ mMailbox.mSyncKey = syncKey;
+ }
+
+ public android.accounts.Account getAccountManagerAccount() {
+ if (mAccountManagerAccount == null) {
+ mAccountManagerAccount =
+ new android.accounts.Account(mAccount.mEmailAddress, Eas.ACCOUNT_MANAGER_TYPE);
+ }
+ return mAccountManagerAccount;
+ }
+
+ public static final class EasChildren {
+ private EasChildren() {}
+
+ /** MIME type used when storing this in data table. */
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_children";
+ public static final int MAX_CHILDREN = 8;
+ public static final String[] ROWS =
+ new String[] {"data2", "data3", "data4", "data5", "data6", "data7", "data8", "data9"};
+ }
+
+ public static final class EasPersonal {
+ String anniversary;
+ String fileAs;
+
+ /** MIME type used when storing this in data table. */
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_personal";
+ public static final String ANNIVERSARY = "data2";
+ public static final String FILE_AS = "data4";
+
+ boolean hasData() {
+ return anniversary != null || fileAs != null;
+ }
+ }
+
+ public static final class EasBusiness {
+ String customerId;
+ String governmentId;
+ String accountName;
+
+ /** MIME type used when storing this in data table. */
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/eas_business";
+ public static final String CUSTOMER_ID = "data6";
+ public static final String GOVERNMENT_ID = "data7";
+ public static final String ACCOUNT_NAME = "data8";
+
+ boolean hasData() {
+ return customerId != null || governmentId != null || accountName != null;
+ }
+ }
+
+ public static final class Address {
+ String city;
+ String country;
+ String code;
+ String street;
+ String state;
+
+ boolean hasData() {
+ return city != null || country != null || code != null || state != null
+ || street != null;
+ }
+ }
+
+ class EmailRow implements UntypedRow {
+ String email;
+ String displayName;
+
+ public EmailRow(String _email) {
+ Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(_email);
+ // Can't happen, but belt & suspenders
+ if (tokens.length == 0) {
+ email = "";
+ displayName = "";
+ } else {
+ Rfc822Token token = tokens[0];
+ email = token.getAddress();
+ displayName = token.getName();
+ }
+ }
+
+ public void addValues(RowBuilder builder) {
+ builder.withValue(Email.DATA, email);
+ builder.withValue(Email.DISPLAY_NAME, displayName);
+ }
+
+ public boolean isSameAs(int type, String value) {
+ return email.equalsIgnoreCase(value);
+ }
+ }
+
+ class ImRow implements UntypedRow {
+ String im;
+
+ public ImRow(String _im) {
+ im = _im;
+ }
+
+ public void addValues(RowBuilder builder) {
+ builder.withValue(Im.DATA, im);
+ }
+
+ public boolean isSameAs(int type, String value) {
+ return im.equalsIgnoreCase(value);
+ }
+ }
+
+ class PhoneRow implements UntypedRow {
+ String phone;
+ int type;
+
+ public PhoneRow(String _phone, int _type) {
+ phone = _phone;
+ type = _type;
+ }
+
+ public void addValues(RowBuilder builder) {
+ builder.withValue(Im.DATA, phone);
+ builder.withValue(Phone.TYPE, type);
+ }
+
+ public boolean isSameAs(int _type, String value) {
+ return type == _type && phone.equalsIgnoreCase(value);
+ }
+ }
+
+ class EasContactsSyncParser extends AbstractSyncParser {
+
+ String[] mBindArgument = new String[1];
+ String mMailboxIdAsString;
+ Uri mAccountUri;
+ ContactOperations ops = new ContactOperations();
+
+ public EasContactsSyncParser(InputStream in, ContactsSyncAdapter adapter)
+ throws IOException {
+ super(in, adapter);
+ mAccountUri = uriWithAccountAndIsSyncAdapter(RawContacts.CONTENT_URI);
+ }
+
+ @Override
+ public void wipe() {
+ mContentResolver.delete(mAccountUri, null, null);
+ }
+
+ public void addData(String serverId, ContactOperations ops, Entity entity)
+ throws IOException {
+ String fileAs = null;
+ String prefix = null;
+ String firstName = null;
+ String lastName = null;
+ String middleName = null;
+ String suffix = null;
+ String companyName = null;
+ String yomiFirstName = null;
+ String yomiLastName = null;
+ String yomiCompanyName = null;
+ String title = null;
+ String department = null;
+ String officeLocation = null;
+ Address home = new Address();
+ Address work = new Address();
+ Address other = new Address();
+ EasBusiness business = new EasBusiness();
+ EasPersonal personal = new EasPersonal();
+ ArrayList<String> children = new ArrayList<String>();
+ ArrayList<UntypedRow> emails = new ArrayList<UntypedRow>();
+ ArrayList<UntypedRow> ims = new ArrayList<UntypedRow>();
+ ArrayList<UntypedRow> homePhones = new ArrayList<UntypedRow>();
+ ArrayList<UntypedRow> workPhones = new ArrayList<UntypedRow>();
+ if (entity == null) {
+ ops.newContact(serverId);
+ }
+
+ while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
+ switch (tag) {
+ case Tags.CONTACTS_FIRST_NAME:
+ firstName = getValue();
+ break;
+ case Tags.CONTACTS_LAST_NAME:
+ lastName = getValue();
+ break;
+ case Tags.CONTACTS_MIDDLE_NAME:
+ middleName = getValue();
+ break;
+ case Tags.CONTACTS_FILE_AS:
+ fileAs = getValue();
+ break;
+ case Tags.CONTACTS_SUFFIX:
+ suffix = getValue();
+ break;
+ case Tags.CONTACTS_COMPANY_NAME:
+ companyName = getValue();
+ break;
+ case Tags.CONTACTS_JOB_TITLE:
+ title = getValue();
+ break;
+ case Tags.CONTACTS_EMAIL1_ADDRESS:
+ case Tags.CONTACTS_EMAIL2_ADDRESS:
+ case Tags.CONTACTS_EMAIL3_ADDRESS:
+ emails.add(new EmailRow(getValue()));
+ break;
+ case Tags.CONTACTS_BUSINESS2_TELEPHONE_NUMBER:
+ case Tags.CONTACTS_BUSINESS_TELEPHONE_NUMBER:
+ workPhones.add(new PhoneRow(getValue(), Phone.TYPE_WORK));
+ break;
+ case Tags.CONTACTS2_MMS:
+ ops.addPhone(entity, Phone.TYPE_MMS, getValue());
+ break;
+ case Tags.CONTACTS_BUSINESS_FAX_NUMBER:
+ ops.addPhone(entity, Phone.TYPE_FAX_WORK, getValue());
+ break;
+ case Tags.CONTACTS2_COMPANY_MAIN_PHONE:
+ ops.addPhone(entity, Phone.TYPE_COMPANY_MAIN, getValue());
+ break;
+ case Tags.CONTACTS_HOME_FAX_NUMBER:
+ ops.addPhone(entity, Phone.TYPE_FAX_HOME, getValue());
+ break;
+ case Tags.CONTACTS_HOME_TELEPHONE_NUMBER:
+ case Tags.CONTACTS_HOME2_TELEPHONE_NUMBER:
+ homePhones.add(new PhoneRow(getValue(), Phone.TYPE_HOME));
+ break;
+ case Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER:
+ ops.addPhone(entity, Phone.TYPE_MOBILE, getValue());
+ break;
+ case Tags.CONTACTS_CAR_TELEPHONE_NUMBER:
+ ops.addPhone(entity, Phone.TYPE_CAR, getValue());
+ break;
+ case Tags.CONTACTS_RADIO_TELEPHONE_NUMBER:
+ ops.addPhone(entity, Phone.TYPE_RADIO, getValue());
+ break;
+ case Tags.CONTACTS_PAGER_NUMBER:
+ ops.addPhone(entity, Phone.TYPE_PAGER, getValue());
+ break;
+ case Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER:
+ ops.addPhone(entity, Phone.TYPE_ASSISTANT, getValue());
+ break;
+ case Tags.CONTACTS2_IM_ADDRESS:
+ case Tags.CONTACTS2_IM_ADDRESS_2:
+ case Tags.CONTACTS2_IM_ADDRESS_3:
+ ims.add(new ImRow(getValue()));
+ break;
+ case Tags.CONTACTS_BUSINESS_ADDRESS_CITY:
+ work.city = getValue();
+ break;
+ case Tags.CONTACTS_BUSINESS_ADDRESS_COUNTRY:
+ work.country = getValue();
+ break;
+ case Tags.CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE:
+ work.code = getValue();
+ break;
+ case Tags.CONTACTS_BUSINESS_ADDRESS_STATE:
+ work.state = getValue();
+ break;
+ case Tags.CONTACTS_BUSINESS_ADDRESS_STREET:
+ work.street = getValue();
+ break;
+ case Tags.CONTACTS_HOME_ADDRESS_CITY:
+ home.city = getValue();
+ break;
+ case Tags.CONTACTS_HOME_ADDRESS_COUNTRY:
+ home.country = getValue();
+ break;
+ case Tags.CONTACTS_HOME_ADDRESS_POSTAL_CODE:
+ home.code = getValue();
+ break;
+ case Tags.CONTACTS_HOME_ADDRESS_STATE:
+ home.state = getValue();
+ break;
+ case Tags.CONTACTS_HOME_ADDRESS_STREET:
+ home.street = getValue();
+ break;
+ case Tags.CONTACTS_OTHER_ADDRESS_CITY:
+ other.city = getValue();
+ break;
+ case Tags.CONTACTS_OTHER_ADDRESS_COUNTRY:
+ other.country = getValue();
+ break;
+ case Tags.CONTACTS_OTHER_ADDRESS_POSTAL_CODE:
+ other.code = getValue();
+ break;
+ case Tags.CONTACTS_OTHER_ADDRESS_STATE:
+ other.state = getValue();
+ break;
+ case Tags.CONTACTS_OTHER_ADDRESS_STREET:
+ other.street = getValue();
+ break;
+
+ case Tags.CONTACTS_CHILDREN:
+ childrenParser(children);
+ break;
+
+ case Tags.CONTACTS_YOMI_COMPANY_NAME:
+ yomiCompanyName = getValue();
+ break;
+ case Tags.CONTACTS_YOMI_FIRST_NAME:
+ yomiFirstName = getValue();
+ break;
+ case Tags.CONTACTS_YOMI_LAST_NAME:
+ yomiLastName = getValue();
+ break;
+
+ case Tags.CONTACTS2_NICKNAME:
+ ops.addNickname(entity, getValue());
+ break;
+
+ case Tags.CONTACTS_ASSISTANT_NAME:
+ ops.addRelation(entity, Relation.TYPE_ASSISTANT, getValue());
+ break;
+ case Tags.CONTACTS2_MANAGER_NAME:
+ ops.addRelation(entity, Relation.TYPE_MANAGER, getValue());
+ break;
+ case Tags.CONTACTS_SPOUSE:
+ ops.addRelation(entity, Relation.TYPE_SPOUSE, getValue());
+ break;
+ case Tags.CONTACTS_DEPARTMENT:
+ department = getValue();
+ break;
+ case Tags.CONTACTS_TITLE:
+ prefix = getValue();
+ break;
+
+ // EAS Business
+ case Tags.CONTACTS_OFFICE_LOCATION:
+ officeLocation = getValue();
+ break;
+ case Tags.CONTACTS2_CUSTOMER_ID:
+ business.customerId = getValue();
+ break;
+ case Tags.CONTACTS2_GOVERNMENT_ID:
+ business.governmentId = getValue();
+ break;
+ case Tags.CONTACTS2_ACCOUNT_NAME:
+ business.accountName = getValue();
+ break;
+
+ // EAS Personal
+ case Tags.CONTACTS_ANNIVERSARY:
+ personal.anniversary = getValue();
+ break;
+ case Tags.CONTACTS_BIRTHDAY:
+ ops.addBirthday(entity, getValue());
+ break;
+ case Tags.CONTACTS_WEBPAGE:
+ ops.addWebpage(entity, getValue());
+ break;
+
+ case Tags.CONTACTS_PICTURE:
+ ops.addPhoto(entity, getValue());
+ break;
+
+ case Tags.BASE_BODY:
+ ops.addNote(entity, bodyParser());
+ break;
+ case Tags.CONTACTS_BODY:
+ ops.addNote(entity, getValue());
+ break;
+
+ case Tags.CONTACTS_CATEGORIES:
+ mGroupsUsed = true;
+ categoriesParser(ops, entity);
+ break;
+
+ case Tags.CONTACTS_COMPRESSED_RTF:
+ // We don't use this, and it isn't necessary to upload, so we'll ignore it
+ skipTag();
+ break;
+
+ default:
+ skipTag();
+ }
+ }
+
+ // We must have first name, last name, or company name
+ String name = null;
+ if (firstName != null || lastName != null) {
+ if (firstName == null) {
+ name = lastName;
+ } else if (lastName == null) {
+ name = firstName;
+ } else {
+ name = firstName + ' ' + lastName;
+ }
+ } else if (companyName != null) {
+ name = companyName;
+ }
+
+ ops.addName(entity, prefix, firstName, lastName, middleName, suffix, name,
+ yomiFirstName, yomiLastName, fileAs);
+ ops.addBusiness(entity, business);
+ ops.addPersonal(entity, personal);
+
+ ops.addUntyped(entity, emails, Email.CONTENT_ITEM_TYPE, -1, MAX_EMAIL_ROWS);
+ ops.addUntyped(entity, ims, Im.CONTENT_ITEM_TYPE, -1, MAX_IM_ROWS);
+ ops.addUntyped(entity, homePhones, Phone.CONTENT_ITEM_TYPE, Phone.TYPE_HOME,
+ MAX_PHONE_ROWS);
+ ops.addUntyped(entity, workPhones, Phone.CONTENT_ITEM_TYPE, Phone.TYPE_WORK,
+ MAX_PHONE_ROWS);
+
+ if (!children.isEmpty()) {
+ ops.addChildren(entity, children);
+ }
+
+ if (work.hasData()) {
+ ops.addPostal(entity, StructuredPostal.TYPE_WORK, work.street, work.city,
+ work.state, work.country, work.code);
+ }
+ if (home.hasData()) {
+ ops.addPostal(entity, StructuredPostal.TYPE_HOME, home.street, home.city,
+ home.state, home.country, home.code);
+ }
+ if (other.hasData()) {
+ ops.addPostal(entity, StructuredPostal.TYPE_OTHER, other.street, other.city,
+ other.state, other.country, other.code);
+ }
+
+ if (companyName != null) {
+ ops.addOrganization(entity, Organization.TYPE_WORK, companyName, title, department,
+ yomiCompanyName, officeLocation);
+ }
+
+ if (entity != null) {
+ // We've been removing rows from the list as they've been found in the xml
+ // Any that are left must have been deleted on the server
+ ArrayList<NamedContentValues> ncvList = entity.getSubValues();
+ for (NamedContentValues ncv: ncvList) {
+ // These rows need to be deleted...
+ Uri u = dataUriFromNamedContentValues(ncv);
+ ops.add(ContentProviderOperation.newDelete(addCallerIsSyncAdapterParameter(u))
+ .build());
+ }
+ }
+ }
+
+ private void categoriesParser(ContactOperations ops, Entity entity) throws IOException {
+ while (nextTag(Tags.CONTACTS_CATEGORIES) != END) {
+ switch (tag) {
+ case Tags.CONTACTS_CATEGORY:
+ ops.addGroup(entity, getValue());
+ break;
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ private void childrenParser(ArrayList<String> children) throws IOException {
+ while (nextTag(Tags.CONTACTS_CHILDREN) != END) {
+ switch (tag) {
+ case Tags.CONTACTS_CHILD:
+ if (children.size() < EasChildren.MAX_CHILDREN) {
+ children.add(getValue());
+ }
+ break;
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ private String bodyParser() throws IOException {
+ String body = null;
+ while (nextTag(Tags.BASE_BODY) != END) {
+ switch (tag) {
+ case Tags.BASE_DATA:
+ body = getValue();
+ break;
+ default:
+ skipTag();
+ }
+ }
+ return body;
+ }
+
+ public void addParser(ContactOperations ops) throws IOException {
+ String serverId = null;
+ while (nextTag(Tags.SYNC_ADD) != END) {
+ switch (tag) {
+ case Tags.SYNC_SERVER_ID: // same as
+ serverId = getValue();
+ break;
+ case Tags.SYNC_APPLICATION_DATA:
+ addData(serverId, ops, null);
+ break;
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ private Cursor getServerIdCursor(String serverId) {
+ mBindArgument[0] = serverId;
+ return mContentResolver.query(mAccountUri, ID_PROJECTION, SERVER_ID_SELECTION,
+ mBindArgument, null);
+ }
+
+ private Cursor getClientIdCursor(String clientId) {
+ mBindArgument[0] = clientId;
+ return mContentResolver.query(mAccountUri, ID_PROJECTION, CLIENT_ID_SELECTION,
+ mBindArgument, null);
+ }
+
+ public void deleteParser(ContactOperations ops) throws IOException {
+ while (nextTag(Tags.SYNC_DELETE) != END) {
+ switch (tag) {
+ case Tags.SYNC_SERVER_ID:
+ String serverId = getValue();
+ // Find the message in this mailbox with the given serverId
+ Cursor c = getServerIdCursor(serverId);
+ try {
+ if (c.moveToFirst()) {
+ userLog("Deleting ", serverId);
+ ops.delete(c.getLong(0));
+ }
+ } finally {
+ c.close();
+ }
+ break;
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ class ServerChange {
+ long id;
+ boolean read;
+
+ ServerChange(long _id, boolean _read) {
+ id = _id;
+ read = _read;
+ }
+ }
+
+ /**
+ * Changes are handled row by row, and only changed/new rows are acted upon
+ * @param ops the array of pending ContactProviderOperations.
+ * @throws IOException
+ */
+ public void changeParser(ContactOperations ops) throws IOException {
+ String serverId = null;
+ Entity entity = null;
+ while (nextTag(Tags.SYNC_CHANGE) != END) {
+ switch (tag) {
+ case Tags.SYNC_SERVER_ID:
+ serverId = getValue();
+ Cursor c = getServerIdCursor(serverId);
+ try {
+ if (c.moveToFirst()) {
+ // TODO Handle deleted individual rows...
+ try {
+ EntityIterator entityIterator =
+ mContentResolver.queryEntities(ContentUris
+ .withAppendedId(RawContacts.CONTENT_URI, c.getLong(0)),
+ null, null, null);
+ if (entityIterator.hasNext()) {
+ entity = entityIterator.next();
+ }
+ userLog("Changing contact ", serverId);
+ } catch (RemoteException e) {
+ }
+ }
+ } finally {
+ c.close();
+ }
+ break;
+ case Tags.SYNC_APPLICATION_DATA:
+ addData(serverId, ops, entity);
+ break;
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ @Override
+ public void commandsParser() throws IOException {
+ while (nextTag(Tags.SYNC_COMMANDS) != END) {
+ if (tag == Tags.SYNC_ADD) {
+ addParser(ops);
+ incrementChangeCount();
+ } else if (tag == Tags.SYNC_DELETE) {
+ deleteParser(ops);
+ incrementChangeCount();
+ } else if (tag == Tags.SYNC_CHANGE) {
+ changeParser(ops);
+ incrementChangeCount();
+ } else
+ skipTag();
+ }
+ }
+
+ @Override
+ public void commit() throws IOException {
+ // Save the syncKey here, using the Helper provider by Contacts provider
+ userLog("Contacts SyncKey saved as: ", mMailbox.mSyncKey);
+ ops.add(SyncStateContract.Helpers.newSetOperation(SyncState.CONTENT_URI,
+ getAccountManagerAccount(), mMailbox.mSyncKey.getBytes()));
+
+ // Execute these all at once...
+ ops.execute();
+
+ if (ops.mResults != null) {
+ ContentValues cv = new ContentValues();
+ cv.put(RawContacts.DIRTY, 0);
+ for (int i = 0; i < ops.mContactIndexCount; i++) {
+ int index = ops.mContactIndexArray[i];
+ Uri u = ops.mResults[index].uri;
+ if (u != null) {
+ String idString = u.getLastPathSegment();
+ mContentResolver.update(
+ addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI), cv,
+ RawContacts._ID + "=" + idString, null);
+ }
+ }
+ }
+ }
+
+ public void addResponsesParser() throws IOException {
+ String serverId = null;
+ String clientId = null;
+ ContentValues cv = new ContentValues();
+ while (nextTag(Tags.SYNC_ADD) != END) {
+ switch (tag) {
+ case Tags.SYNC_SERVER_ID:
+ serverId = getValue();
+ break;
+ case Tags.SYNC_CLIENT_ID:
+ clientId = getValue();
+ break;
+ case Tags.SYNC_STATUS:
+ getValue();
+ break;
+ default:
+ skipTag();
+ }
+ }
+
+ // This is theoretically impossible, but...
+ if (clientId == null || serverId == null) return;
+
+ Cursor c = getClientIdCursor(clientId);
+ try {
+ if (c.moveToFirst()) {
+ cv.put(RawContacts.SOURCE_ID, serverId);
+ cv.put(RawContacts.DIRTY, 0);
+ ops.add(ContentProviderOperation.newUpdate(
+ ContentUris.withAppendedId(
+ addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI),
+ c.getLong(0)))
+ .withValues(cv)
+ .build());
+ userLog("New contact " + clientId + " was given serverId: " + serverId);
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ public void changeResponsesParser() throws IOException {
+ String serverId = null;
+ String status = null;
+ while (nextTag(Tags.SYNC_CHANGE) != END) {
+ switch (tag) {
+ case Tags.SYNC_SERVER_ID:
+ serverId = getValue();
+ break;
+ case Tags.SYNC_STATUS:
+ status = getValue();
+ break;
+ default:
+ skipTag();
+ }
+ }
+ if (serverId != null && status != null) {
+ userLog("Changed contact " + serverId + " failed with status: " + status);
+ }
+ }
+
+
+ @Override
+ public void responsesParser() throws IOException {
+ // Handle server responses here (for Add and Change)
+ while (nextTag(Tags.SYNC_RESPONSES) != END) {
+ if (tag == Tags.SYNC_ADD) {
+ addResponsesParser();
+ } else if (tag == Tags.SYNC_CHANGE) {
+ changeResponsesParser();
+ } else
+ skipTag();
+ }
+ }
+ }
+
+
+ private Uri uriWithAccountAndIsSyncAdapter(Uri uri) {
+ return uri.buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress)
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE)
+ .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
+ .build();
+ }
+
+ /**
+ * SmartBuilder is a wrapper for the Builder class that is used to create/update rows for a
+ * ContentProvider. It has, in addition to the Builder, ContentValues which, if present,
+ * represent the current values of that row, that can be compared against current values to
+ * see whether an update is even necessary. The methods on SmartBuilder are delegated to
+ * the Builder.
+ */
+ private class RowBuilder {
+ Builder builder;
+ ContentValues cv;
+
+ public RowBuilder(Builder _builder) {
+ builder = _builder;
+ }
+
+ public RowBuilder(Builder _builder, NamedContentValues _ncv) {
+ builder = _builder;
+ cv = _ncv.values;
+ }
+
+ RowBuilder withValues(ContentValues values) {
+ builder.withValues(values);
+ return this;
+ }
+
+ RowBuilder withValueBackReference(String key, int previousResult) {
+ builder.withValueBackReference(key, previousResult);
+ return this;
+ }
+
+ ContentProviderOperation build() {
+ return builder.build();
+ }
+
+ RowBuilder withValue(String key, Object value) {
+ builder.withValue(key, value);
+ return this;
+ }
+ }
+
+ private class ContactOperations extends ArrayList<ContentProviderOperation> {
+ private static final long serialVersionUID = 1L;
+ private int mCount = 0;
+ private int mContactBackValue = mCount;
+ // Make an array big enough for the PIM window (max items we can get)
+ private int[] mContactIndexArray =
+ new int[Integer.parseInt(EasSyncService.PIM_WINDOW_SIZE)];
+ private int mContactIndexCount = 0;
+ private ContentProviderResult[] mResults = null;
+
+ @Override
+ public boolean add(ContentProviderOperation op) {
+ super.add(op);
+ mCount++;
+ return true;
+ }
+
+ public void newContact(String serverId) {
+ Builder builder = ContentProviderOperation
+ .newInsert(uriWithAccountAndIsSyncAdapter(RawContacts.CONTENT_URI));
+ ContentValues values = new ContentValues();
+ values.put(RawContacts.SOURCE_ID, serverId);
+ builder.withValues(values);
+ mContactBackValue = mCount;
+ mContactIndexArray[mContactIndexCount++] = mCount;
+ add(builder.build());
+ }
+
+ public void delete(long id) {
+ add(ContentProviderOperation
+ .newDelete(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)
+ .buildUpon()
+ .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
+ .build())
+ .build());
+ }
+
+ public void execute() {
+ synchronized (mService.getSynchronizer()) {
+ if (!mService.isStopped()) {
+ try {
+ if (!isEmpty()) {
+ mService.userLog("Executing ", size(), " CPO's");
+ mResults = mContext.getContentResolver().applyBatch(
+ ContactsContract.AUTHORITY, this);
+ }
+ } catch (RemoteException e) {
+ // There is nothing sensible to be done here
+ Log.e(TAG, "problem inserting contact during server update", e);
+ } catch (OperationApplicationException e) {
+ // There is nothing sensible to be done here
+ Log.e(TAG, "problem inserting contact during server update", e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Given the list of NamedContentValues for an entity, a mime type, and a subtype,
+ * tries to find a match, returning it
+ * @param list the list of NCV's from the contact entity
+ * @param contentItemType the mime type we're looking for
+ * @param type the subtype (e.g. HOME, WORK, etc.)
+ * @return the matching NCV or null if not found
+ */
+ private NamedContentValues findTypedData(ArrayList<NamedContentValues> list,
+ String contentItemType, int type, String stringType) {
+ NamedContentValues result = null;
+
+ // Loop through the ncv's, looking for an existing row
+ for (NamedContentValues namedContentValues: list) {
+ Uri uri = namedContentValues.uri;
+ ContentValues cv = namedContentValues.values;
+ if (Data.CONTENT_URI.equals(uri)) {
+ String mimeType = cv.getAsString(Data.MIMETYPE);
+ if (mimeType.equals(contentItemType)) {
+ if (stringType != null) {
+ if (cv.getAsString(GroupMembership.GROUP_ROW_ID).equals(stringType)) {
+ result = namedContentValues;
+ }
+ // Note Email.TYPE could be ANY type column; they are all defined in
+ // the private CommonColumns class in ContactsContract
+ } else if (type < 0 || cv.getAsInteger(Email.TYPE) == type) {
+ result = namedContentValues;
+ }
+ }
+ }
+ }
+
+ // If we've found an existing data row, we'll delete it. Any rows left at the
+ // end should be deleted...
+ if (result != null) {
+ list.remove(result);
+ }
+
+ // Return the row found (or null)
+ return result;
+ }
+
+ /**
+ * Given the list of NamedContentValues for an entity and a mime type
+ * gather all of the matching NCV's, returning them
+ * @param list the list of NCV's from the contact entity
+ * @param contentItemType the mime type we're looking for
+ * @param type the subtype (e.g. HOME, WORK, etc.)
+ * @return the matching NCVs
+ */
+ private ArrayList<NamedContentValues> findUntypedData(ArrayList<NamedContentValues> list,
+ int type, String contentItemType) {
+ ArrayList<NamedContentValues> result = new ArrayList<NamedContentValues>();
+
+ // Loop through the ncv's, looking for an existing row
+ for (NamedContentValues namedContentValues: list) {
+ Uri uri = namedContentValues.uri;
+ ContentValues cv = namedContentValues.values;
+ if (Data.CONTENT_URI.equals(uri)) {
+ String mimeType = cv.getAsString(Data.MIMETYPE);
+ if (mimeType.equals(contentItemType)) {
+ if (type != -1) {
+ int subtype = cv.getAsInteger(Phone.TYPE);
+ if (type != subtype) {
+ continue;
+ }
+ }
+ result.add(namedContentValues);
+ }
+ }
+ }
+
+ // If we've found an existing data row, we'll delete it. Any rows left at the
+ // end should be deleted...
+ if (result != null) {
+ list.remove(result);
+ }
+
+ // Return the row found (or null)
+ return result;
+ }
+
+ /**
+ * Create a wrapper for a builder (insert or update) that also includes the NCV for
+ * an existing row of this type. If the SmartBuilder's cv field is not null, then
+ * it represents the current (old) values of this field. The caller can then check
+ * whether the field is now different and needs to be updated; if it's not different,
+ * the caller will simply return and not generate a new CPO. Otherwise, the builder
+ * should have its content values set, and the built CPO should be added to the
+ * ContactOperations list.
+ *
+ * @param entity the contact entity (or null if this is a new contact)
+ * @param mimeType the mime type of this row
+ * @param type the subtype of this row
+ * @param stringType for groups, the name of the group (type will be ignored), or null
+ * @return the created SmartBuilder
+ */
+ public RowBuilder createBuilder(Entity entity, String mimeType, int type,
+ String stringType) {
+ RowBuilder builder = null;
+
+ if (entity != null) {
+ NamedContentValues ncv =
+ findTypedData(entity.getSubValues(), mimeType, type, stringType);
+ if (ncv != null) {
+ builder = new RowBuilder(
+ ContentProviderOperation
+ .newUpdate(addCallerIsSyncAdapterParameter(
+ dataUriFromNamedContentValues(ncv))),
+ ncv);
+ }
+ }
+
+ if (builder == null) {
+ builder = newRowBuilder(entity, mimeType);
+ }
+
+ // Return the appropriate builder (insert or update)
+ // Caller will fill in the appropriate values; 4 MIMETYPE is already set
+ return builder;
+ }
+
+ private RowBuilder typedRowBuilder(Entity entity, String mimeType, int type) {
+ return createBuilder(entity, mimeType, type, null);
+ }
+
+ private RowBuilder untypedRowBuilder(Entity entity, String mimeType) {
+ return createBuilder(entity, mimeType, -1, null);
+ }
+
+ private RowBuilder newRowBuilder(Entity entity, String mimeType) {
+ // This is a new row; first get the contactId
+ // If the Contact is new, use the saved back value; otherwise the value in the entity
+ int contactId = mContactBackValue;
+ if (entity != null) {
+ contactId = entity.getEntityValues().getAsInteger(RawContacts._ID);
+ }
+
+ // Create an insert operation with the proper contactId reference
+ RowBuilder builder =
+ new RowBuilder(ContentProviderOperation.newInsert(
+ addCallerIsSyncAdapterParameter(Data.CONTENT_URI)));
+ if (entity == null) {
+ builder.withValueBackReference(Data.RAW_CONTACT_ID, contactId);
+ } else {
+ builder.withValue(Data.RAW_CONTACT_ID, contactId);
+ }
+
+ // Set the mime type of the row
+ builder.withValue(Data.MIMETYPE, mimeType);
+ return builder;
+ }
+
+ /**
+ * Compare a column in a ContentValues with an (old) value, and see if they are the
+ * same. For this purpose, null and an empty string are considered the same.
+ * @param cv a ContentValues object, from a NamedContentValues
+ * @param column a column that might be in the ContentValues
+ * @param oldValue an old value (or null) to check against
+ * @return whether the column's value in the ContentValues matches oldValue
+ */
+ private boolean cvCompareString(ContentValues cv, String column, String oldValue) {
+ if (cv.containsKey(column)) {
+ if (oldValue != null && cv.getAsString(column).equals(oldValue)) {
+ return true;
+ }
+ } else if (oldValue == null || oldValue.length() == 0) {
+ return true;
+ }
+ return false;
+ }
+
+ public void addChildren(Entity entity, ArrayList<String> children) {
+ RowBuilder builder = untypedRowBuilder(entity, EasChildren.CONTENT_ITEM_TYPE);
+ int i = 0;
+ for (String child: children) {
+ builder.withValue(EasChildren.ROWS[i++], child);
+ }
+ add(builder.build());
+ }
+
+ public void addGroup(Entity entity, String group) {
+ RowBuilder builder =
+ createBuilder(entity, GroupMembership.CONTENT_ITEM_TYPE, -1, group);
+ builder.withValue(GroupMembership.GROUP_SOURCE_ID, group);
+ add(builder.build());
+ }
+
+ public void addBirthday(Entity entity, String birthday) {
+ RowBuilder builder =
+ typedRowBuilder(entity, Event.CONTENT_ITEM_TYPE, Event.TYPE_BIRTHDAY);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, Event.START_DATE, birthday)) {
+ return;
+ }
+ builder.withValue(Event.START_DATE, birthday);
+ builder.withValue(Event.TYPE, Event.TYPE_BIRTHDAY);
+ add(builder.build());
+ }
+
+ public void addName(Entity entity, String prefix, String givenName, String familyName,
+ String middleName, String suffix, String displayName, String yomiFirstName,
+ String yomiLastName, String fileAs) {
+ RowBuilder builder = untypedRowBuilder(entity, StructuredName.CONTENT_ITEM_TYPE);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, StructuredName.GIVEN_NAME, givenName) &&
+ cvCompareString(cv, StructuredName.FAMILY_NAME, familyName) &&
+ cvCompareString(cv, StructuredName.MIDDLE_NAME, middleName) &&
+ cvCompareString(cv, StructuredName.PREFIX, prefix) &&
+ cvCompareString(cv, StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName) &&
+ cvCompareString(cv, StructuredName.PHONETIC_FAMILY_NAME, yomiLastName) &&
+ //cvCompareString(cv, StructuredName.DISPLAY_NAME, fileAs) &&
+ cvCompareString(cv, StructuredName.SUFFIX, suffix)) {
+ return;
+ }
+ builder.withValue(StructuredName.GIVEN_NAME, givenName);
+ builder.withValue(StructuredName.FAMILY_NAME, familyName);
+ builder.withValue(StructuredName.MIDDLE_NAME, middleName);
+ builder.withValue(StructuredName.SUFFIX, suffix);
+ builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, yomiFirstName);
+ builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, yomiLastName);
+ builder.withValue(StructuredName.PREFIX, prefix);
+ //builder.withValue(StructuredName.DISPLAY_NAME, fileAs);
+ add(builder.build());
+ }
+
+ public void addPersonal(Entity entity, EasPersonal personal) {
+ RowBuilder builder = untypedRowBuilder(entity, EasPersonal.CONTENT_ITEM_TYPE);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, EasPersonal.ANNIVERSARY, personal.anniversary) &&
+ cvCompareString(cv, EasPersonal.FILE_AS , personal.fileAs)) {
+ return;
+ }
+ if (!personal.hasData()) {
+ return;
+ }
+ builder.withValue(EasPersonal.FILE_AS, personal.fileAs);
+ builder.withValue(EasPersonal.ANNIVERSARY, personal.anniversary);
+ add(builder.build());
+ }
+
+ public void addBusiness(Entity entity, EasBusiness business) {
+ RowBuilder builder = untypedRowBuilder(entity, EasBusiness.CONTENT_ITEM_TYPE);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, EasBusiness.ACCOUNT_NAME, business.accountName) &&
+ cvCompareString(cv, EasBusiness.CUSTOMER_ID, business.customerId) &&
+ cvCompareString(cv, EasBusiness.GOVERNMENT_ID, business.governmentId)) {
+ return;
+ }
+ if (!business.hasData()) {
+ return;
+ }
+ builder.withValue(EasBusiness.ACCOUNT_NAME, business.accountName);
+ builder.withValue(EasBusiness.CUSTOMER_ID, business.customerId);
+ builder.withValue(EasBusiness.GOVERNMENT_ID, business.governmentId);
+ add(builder.build());
+ }
+
+ public void addPhoto(Entity entity, String photo) {
+ RowBuilder builder = untypedRowBuilder(entity, Photo.CONTENT_ITEM_TYPE);
+ // We're always going to add this; it's not worth trying to figure out whether the
+ // picture is the same as the one stored.
+ byte[] pic = Base64.decodeBase64(photo.getBytes());
+ builder.withValue(Photo.PHOTO, pic);
+ add(builder.build());
+ }
+
+ public void addPhone(Entity entity, int type, String phone) {
+ RowBuilder builder = typedRowBuilder(entity, Phone.CONTENT_ITEM_TYPE, type);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, Phone.NUMBER, phone)) {
+ return;
+ }
+ builder.withValue(Phone.TYPE, type);
+ builder.withValue(Phone.NUMBER, phone);
+ add(builder.build());
+ }
+
+ public void addWebpage(Entity entity, String url) {
+ RowBuilder builder = untypedRowBuilder(entity, Website.CONTENT_ITEM_TYPE);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, Website.URL, url)) {
+ return;
+ }
+ builder.withValue(Website.TYPE, Website.TYPE_WORK);
+ builder.withValue(Website.URL, url);
+ add(builder.build());
+ }
+
+ public void addRelation(Entity entity, int type, String value) {
+ RowBuilder builder = typedRowBuilder(entity, Relation.CONTENT_ITEM_TYPE, type);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, Relation.DATA, value)) {
+ return;
+ }
+ builder.withValue(Relation.TYPE, type);
+ builder.withValue(Relation.DATA, value);
+ add(builder.build());
+ }
+
+ public void addNickname(Entity entity, String name) {
+ RowBuilder builder =
+ typedRowBuilder(entity, Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE_DEFAULT);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, Nickname.NAME, name)) {
+ return;
+ }
+ builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT);
+ builder.withValue(Nickname.NAME, name);
+ add(builder.build());
+ }
+
+ public void addPostal(Entity entity, int type, String street, String city, String state,
+ String country, String code) {
+ RowBuilder builder = typedRowBuilder(entity, StructuredPostal.CONTENT_ITEM_TYPE,
+ type);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, StructuredPostal.CITY, city) &&
+ cvCompareString(cv, StructuredPostal.STREET, street) &&
+ cvCompareString(cv, StructuredPostal.COUNTRY, country) &&
+ cvCompareString(cv, StructuredPostal.POSTCODE, code) &&
+ cvCompareString(cv, StructuredPostal.REGION, state)) {
+ return;
+ }
+ builder.withValue(StructuredPostal.TYPE, type);
+ builder.withValue(StructuredPostal.CITY, city);
+ builder.withValue(StructuredPostal.STREET, street);
+ builder.withValue(StructuredPostal.COUNTRY, country);
+ builder.withValue(StructuredPostal.POSTCODE, code);
+ builder.withValue(StructuredPostal.REGION, state);
+ add(builder.build());
+ }
+
+ /**
+ * We now are dealing with up to maxRows typeless rows of mimeType data. We need to try to
+ * match them with existing rows; if there's a match, everything's great. Otherwise, we
+ * either need to add a new row for the data, or we have to replace an existing one
+ * that no longer matches. This is similar to the way Emails are handled.
+ */
+ public void addUntyped(Entity entity, ArrayList<UntypedRow> rows, String mimeType,
+ int type, int maxRows) {
+ // Make a list of all same type rows in the existing entity
+ ArrayList<NamedContentValues> oldValues = EMPTY_ARRAY_NAMEDCONTENTVALUES;
+ ArrayList<NamedContentValues> entityValues = EMPTY_ARRAY_NAMEDCONTENTVALUES;
+ if (entity != null) {
+ oldValues = findUntypedData(entityValues, type, mimeType);
+ entityValues = entity.getSubValues();
+ }
+
+ // These will be rows needing replacement with new values
+ ArrayList<UntypedRow> rowsToReplace = new ArrayList<UntypedRow>();
+
+ // The count of existing rows
+ int numRows = oldValues.size();
+ for (UntypedRow row: rows) {
+ boolean found = false;
+ // If we already have this row, mark it
+ for (NamedContentValues ncv: oldValues) {
+ ContentValues cv = ncv.values;
+ String data = cv.getAsString(COMMON_DATA_ROW);
+ int rowType = -1;
+ if (cv.containsKey(COMMON_TYPE_ROW)) {
+ rowType = cv.getAsInteger(COMMON_TYPE_ROW);
+ }
+ if (row.isSameAs(rowType, data)) {
+ cv.put(FOUND_DATA_ROW, true);
+ // Remove this to indicate it's still being used
+ entityValues.remove(ncv);
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ // If we don't, there are two possibilities
+ if (numRows < maxRows) {
+ // If there are available rows, add a new one
+ RowBuilder builder = newRowBuilder(entity, mimeType);
+ row.addValues(builder);
+ add(builder.build());
+ numRows++;
+ } else {
+ // Otherwise, say we need to replace a row with this
+ rowsToReplace.add(row);
+ }
+ }
+ }
+
+ // Go through rows needing replacement
+ for (UntypedRow row: rowsToReplace) {
+ for (NamedContentValues ncv: oldValues) {
+ ContentValues cv = ncv.values;
+ // Find a row that hasn't been used (i.e. doesn't match current rows)
+ if (!cv.containsKey(FOUND_DATA_ROW)) {
+ // And update it
+ RowBuilder builder = new RowBuilder(
+ ContentProviderOperation
+ .newUpdate(addCallerIsSyncAdapterParameter(
+ dataUriFromNamedContentValues(ncv))),
+ ncv);
+ row.addValues(builder);
+ add(builder.build());
+ }
+ }
+ }
+ }
+
+ public void addOrganization(Entity entity, int type, String company, String title,
+ String department, String yomiCompanyName, String officeLocation) {
+ RowBuilder builder = typedRowBuilder(entity, Organization.CONTENT_ITEM_TYPE, type);
+ ContentValues cv = builder.cv;
+ if (cv != null && cvCompareString(cv, Organization.COMPANY, company) &&
+ cvCompareString(cv, Organization.PHONETIC_NAME, yomiCompanyName) &&
+ cvCompareString(cv, Organization.DEPARTMENT, department) &&
+ cvCompareString(cv, Organization.TITLE, title) &&
+ cvCompareString(cv, Organization.OFFICE_LOCATION, officeLocation)) {
+ return;
+ }
+ builder.withValue(Organization.TYPE, type);
+ builder.withValue(Organization.COMPANY, company);
+ builder.withValue(Organization.TITLE, title);
+ builder.withValue(Organization.DEPARTMENT, department);
+ builder.withValue(Organization.PHONETIC_NAME, yomiCompanyName);
+ builder.withValue(Organization.OFFICE_LOCATION, officeLocation);
+ add(builder.build());
+ }
+
+ public void addNote(Entity entity, String note) {
+ RowBuilder builder = typedRowBuilder(entity, Note.CONTENT_ITEM_TYPE, -1);
+ ContentValues cv = builder.cv;
+ if (note == null) return;
+ note = note.replaceAll("\r\n", "\n");
+ if (cv != null && cvCompareString(cv, Note.NOTE, note)) {
+ return;
+ }
+
+ // Reject notes with nothing in them. Often, we get something from Outlook when
+ // nothing was ever entered. Sigh.
+ int len = note.length();
+ int i = 0;
+ for (; i < len; i++) {
+ char c = note.charAt(i);
+ if (!Character.isWhitespace(c)) {
+ break;
+ }
+ }
+ if (i == len) return;
+
+ builder.withValue(Note.NOTE, note);
+ add(builder.build());
+ }
+ }
+
+ /**
+ * Generate the uri for the data row associated with this NamedContentValues object
+ * @param ncv the NamedContentValues object
+ * @return a uri that can be used to refer to this row
+ */
+ public Uri dataUriFromNamedContentValues(NamedContentValues ncv) {
+ long id = ncv.values.getAsLong(RawContacts._ID);
+ Uri dataUri = ContentUris.withAppendedId(ncv.uri, id);
+ return dataUri;
+ }
+
+ @Override
+ public void cleanup() {
+ // Mark the changed contacts dirty = 0
+ // Permanently delete the user deletions
+ ContactOperations ops = new ContactOperations();
+ for (Long id: mUpdatedIdList) {
+ ops.add(ContentProviderOperation
+ .newUpdate(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)
+ .buildUpon()
+ .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
+ .build())
+ .withValue(RawContacts.DIRTY, 0).build());
+ }
+ for (Long id: mDeletedIdList) {
+ ops.add(ContentProviderOperation
+ .newDelete(ContentUris.withAppendedId(RawContacts.CONTENT_URI, id)
+ .buildUpon()
+ .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
+ .build())
+ .build());
+ }
+ ops.execute();
+ ContentResolver cr = mContext.getContentResolver();
+ if (mGroupsUsed) {
+ // Make sure the title column is set for all of our groups
+ // And that all of our groups are visible
+ // TODO Perhaps the visible part should only happen when the group is created, but
+ // this is fine for now.
+ Uri groupsUri = uriWithAccountAndIsSyncAdapter(Groups.CONTENT_URI);
+ Cursor c = cr.query(groupsUri, new String[] {Groups.SOURCE_ID, Groups.TITLE},
+ Groups.TITLE + " IS NULL", null, null);
+ ContentValues values = new ContentValues();
+ values.put(Groups.GROUP_VISIBLE, 1);
+ try {
+ while (c.moveToNext()) {
+ String sourceId = c.getString(0);
+ values.put(Groups.TITLE, sourceId);
+ cr.update(uriWithAccountAndIsSyncAdapter(groupsUri), values,
+ Groups.SOURCE_ID + "=?", new String[] {sourceId});
+ }
+ } finally {
+ c.close();
+ }
+ }
+ }
+
+ @Override
+ public String getCollectionName() {
+ return "Contacts";
+ }
+
+ private void sendEmail(Serializer s, ContentValues cv, int count, String displayName)
+ throws IOException {
+ // Get both parts of the email address (a newly created one in the UI won't have a name)
+ String addr = cv.getAsString(Email.DATA);
+ String name = cv.getAsString(Email.DISPLAY_NAME);
+ if (name == null) {
+ if (displayName != null) {
+ name = displayName;
+ } else {
+ name = addr;
+ }
+ }
+ // Compose address from name and addr
+ if (addr != null) {
+ String value = '\"' + name + "\" <" + addr + '>';
+ if (count < MAX_EMAIL_ROWS) {
+ s.data(EMAIL_TAGS[count], value);
+ }
+ }
+ }
+
+ private void sendIm(Serializer s, ContentValues cv, int count) throws IOException {
+ String value = cv.getAsString(Im.DATA);
+ if (value == null) return;
+ if (count < MAX_IM_ROWS) {
+ s.data(IM_TAGS[count], value);
+ }
+ }
+
+ private void sendOnePostal(Serializer s, ContentValues cv, int[] fieldNames)
+ throws IOException{
+ if (cv.containsKey(StructuredPostal.CITY)) {
+ s.data(fieldNames[0], cv.getAsString(StructuredPostal.CITY));
+ }
+ if (cv.containsKey(StructuredPostal.COUNTRY)) {
+ s.data(fieldNames[1], cv.getAsString(StructuredPostal.COUNTRY));
+ }
+ if (cv.containsKey(StructuredPostal.POSTCODE)) {
+ s.data(fieldNames[2], cv.getAsString(StructuredPostal.POSTCODE));
+ }
+ if (cv.containsKey(StructuredPostal.REGION)) {
+ s.data(fieldNames[3], cv.getAsString(StructuredPostal.REGION));
+ }
+ if (cv.containsKey(StructuredPostal.STREET)) {
+ s.data(fieldNames[4], cv.getAsString(StructuredPostal.STREET));
+ }
+ }
+
+ private void sendStructuredPostal(Serializer s, ContentValues cv) throws IOException {
+ switch (cv.getAsInteger(StructuredPostal.TYPE)) {
+ case StructuredPostal.TYPE_HOME:
+ sendOnePostal(s, cv, HOME_ADDRESS_TAGS);
+ break;
+ case StructuredPostal.TYPE_WORK:
+ sendOnePostal(s, cv, WORK_ADDRESS_TAGS);
+ break;
+ case StructuredPostal.TYPE_OTHER:
+ sendOnePostal(s, cv, OTHER_ADDRESS_TAGS);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private String sendStructuredName(Serializer s, ContentValues cv) throws IOException {
+ String displayName = null;
+ if (cv.containsKey(StructuredName.FAMILY_NAME)) {
+ s.data(Tags.CONTACTS_LAST_NAME, cv.getAsString(StructuredName.FAMILY_NAME));
+ }
+ if (cv.containsKey(StructuredName.GIVEN_NAME)) {
+ s.data(Tags.CONTACTS_FIRST_NAME, cv.getAsString(StructuredName.GIVEN_NAME));
+ }
+ if (cv.containsKey(StructuredName.MIDDLE_NAME)) {
+ s.data(Tags.CONTACTS_MIDDLE_NAME, cv.getAsString(StructuredName.MIDDLE_NAME));
+ }
+ if (cv.containsKey(StructuredName.SUFFIX)) {
+ s.data(Tags.CONTACTS_SUFFIX, cv.getAsString(StructuredName.SUFFIX));
+ }
+ if (cv.containsKey(StructuredName.PHONETIC_GIVEN_NAME)) {
+ s.data(Tags.CONTACTS_YOMI_FIRST_NAME,
+ cv.getAsString(StructuredName.PHONETIC_GIVEN_NAME));
+ }
+ if (cv.containsKey(StructuredName.PHONETIC_FAMILY_NAME)) {
+ s.data(Tags.CONTACTS_YOMI_LAST_NAME,
+ cv.getAsString(StructuredName.PHONETIC_FAMILY_NAME));
+ }
+ if (cv.containsKey(StructuredName.PREFIX)) {
+ s.data(Tags.CONTACTS_TITLE, cv.getAsString(StructuredName.PREFIX));
+ }
+ if (cv.containsKey(StructuredName.DISPLAY_NAME)) {
+ displayName = cv.getAsString(StructuredName.DISPLAY_NAME);
+ s.data(Tags.CONTACTS_FILE_AS, displayName);
+ }
+ return displayName;
+ }
+
+ private void sendBusiness(Serializer s, ContentValues cv) throws IOException {
+ if (cv.containsKey(EasBusiness.ACCOUNT_NAME)) {
+ s.data(Tags.CONTACTS2_ACCOUNT_NAME, cv.getAsString(EasBusiness.ACCOUNT_NAME));
+ }
+ if (cv.containsKey(EasBusiness.CUSTOMER_ID)) {
+ s.data(Tags.CONTACTS2_CUSTOMER_ID, cv.getAsString(EasBusiness.CUSTOMER_ID));
+ }
+ if (cv.containsKey(EasBusiness.GOVERNMENT_ID)) {
+ s.data(Tags.CONTACTS2_GOVERNMENT_ID, cv.getAsString(EasBusiness.GOVERNMENT_ID));
+ }
+ }
+
+ private void sendPersonal(Serializer s, ContentValues cv) throws IOException {
+ if (cv.containsKey(EasPersonal.ANNIVERSARY)) {
+ s.data(Tags.CONTACTS_ANNIVERSARY, cv.getAsString(EasPersonal.ANNIVERSARY));
+ }
+ if (cv.containsKey(EasPersonal.FILE_AS)) {
+ s.data(Tags.CONTACTS_FILE_AS, cv.getAsString(EasPersonal.FILE_AS));
+ }
+ }
+
+ private void sendBirthday(Serializer s, ContentValues cv) throws IOException {
+ if (cv.containsKey(Event.START_DATE)) {
+ s.data(Tags.CONTACTS_BIRTHDAY, cv.getAsString(Event.START_DATE));
+ }
+ }
+
+ private void sendOrganization(Serializer s, ContentValues cv) throws IOException {
+ if (cv.containsKey(Organization.TITLE)) {
+ s.data(Tags.CONTACTS_JOB_TITLE, cv.getAsString(Organization.TITLE));
+ }
+ if (cv.containsKey(Organization.COMPANY)) {
+ s.data(Tags.CONTACTS_COMPANY_NAME, cv.getAsString(Organization.COMPANY));
+ }
+ if (cv.containsKey(Organization.DEPARTMENT)) {
+ s.data(Tags.CONTACTS_DEPARTMENT, cv.getAsString(Organization.DEPARTMENT));
+ }
+ if (cv.containsKey(Organization.OFFICE_LOCATION)) {
+ s.data(Tags.CONTACTS_OFFICE_LOCATION, cv.getAsString(Organization.OFFICE_LOCATION));
+ }
+ }
+
+ private void sendNickname(Serializer s, ContentValues cv) throws IOException {
+ if (cv.containsKey(Nickname.NAME)) {
+ s.data(Tags.CONTACTS2_NICKNAME, cv.getAsString(Nickname.NAME));
+ }
+ }
+
+ private void sendWebpage(Serializer s, ContentValues cv) throws IOException {
+ if (cv.containsKey(Website.URL)) {
+ s.data(Tags.CONTACTS_WEBPAGE, cv.getAsString(Website.URL));
+ }
+ }
+
+ private void sendNote(Serializer s, ContentValues cv) throws IOException {
+ if (cv.containsKey(Note.NOTE)) {
+ // EAS won't accept note data with raw newline characters
+ String note = cv.getAsString(Note.NOTE).replaceAll("\n", "\r\n");
+ // Format of upsync data depends on protocol version
+ if (mService.mProtocolVersionDouble >= 12.0) {
+ s.start(Tags.BASE_BODY);
+ s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_TEXT).data(Tags.BASE_DATA, note);
+ s.end();
+ } else {
+ s.data(Tags.CONTACTS_BODY, note);
+ }
+ }
+ }
+
+ private void sendChildren(Serializer s, ContentValues cv) throws IOException {
+ boolean first = true;
+ for (int i = 0; i < EasChildren.MAX_CHILDREN; i++) {
+ String row = EasChildren.ROWS[i];
+ if (cv.containsKey(row)) {
+ if (first) {
+ s.start(Tags.CONTACTS_CHILDREN);
+ first = false;
+ }
+ s.data(Tags.CONTACTS_CHILD, cv.getAsString(row));
+ }
+ }
+ if (!first) {
+ s.end();
+ }
+ }
+
+ private void sendPhone(Serializer s, ContentValues cv, int workCount, int homeCount)
+ throws IOException {
+ String value = cv.getAsString(Phone.NUMBER);
+ if (value == null) return;
+ switch (cv.getAsInteger(Phone.TYPE)) {
+ case Phone.TYPE_WORK:
+ if (workCount < MAX_PHONE_ROWS) {
+ s.data(WORK_PHONE_TAGS[workCount], value);
+ }
+ break;
+ case Phone.TYPE_MMS:
+ s.data(Tags.CONTACTS2_MMS, value);
+ break;
+ case Phone.TYPE_ASSISTANT:
+ s.data(Tags.CONTACTS_ASSISTANT_TELEPHONE_NUMBER, value);
+ break;
+ case Phone.TYPE_FAX_WORK:
+ s.data(Tags.CONTACTS_BUSINESS_FAX_NUMBER, value);
+ break;
+ case Phone.TYPE_COMPANY_MAIN:
+ s.data(Tags.CONTACTS2_COMPANY_MAIN_PHONE, value);
+ break;
+ case Phone.TYPE_HOME:
+ if (homeCount < MAX_PHONE_ROWS) {
+ s.data(HOME_PHONE_TAGS[homeCount], value);
+ }
+ break;
+ case Phone.TYPE_MOBILE:
+ s.data(Tags.CONTACTS_MOBILE_TELEPHONE_NUMBER, value);
+ break;
+ case Phone.TYPE_CAR:
+ s.data(Tags.CONTACTS_CAR_TELEPHONE_NUMBER, value);
+ break;
+ case Phone.TYPE_PAGER:
+ s.data(Tags.CONTACTS_PAGER_NUMBER, value);
+ break;
+ case Phone.TYPE_RADIO:
+ s.data(Tags.CONTACTS_RADIO_TELEPHONE_NUMBER, value);
+ break;
+ case Phone.TYPE_FAX_HOME:
+ s.data(Tags.CONTACTS_HOME_FAX_NUMBER, value);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void sendRelation(Serializer s, ContentValues cv) throws IOException {
+ String value = cv.getAsString(Relation.DATA);
+ if (value == null) return;
+ switch (cv.getAsInteger(Relation.TYPE)) {
+ case Relation.TYPE_ASSISTANT:
+ s.data(Tags.CONTACTS_ASSISTANT_NAME, value);
+ break;
+ case Relation.TYPE_MANAGER:
+ s.data(Tags.CONTACTS2_MANAGER_NAME, value);
+ break;
+ case Relation.TYPE_SPOUSE:
+ s.data(Tags.CONTACTS_SPOUSE, value);
+ break;
+ default:
+ break;
+ }
+ }
+
+ @Override
+ public boolean sendLocalChanges(Serializer s) throws IOException {
+ // First, let's find Contacts that have changed.
+ ContentResolver cr = mService.mContentResolver;
+ Uri uri = RawContacts.CONTENT_URI.buildUpon()
+ .appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress)
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE)
+ .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
+ .build();
+
+ if (getSyncKey().equals("0")) {
+ return false;
+ }
+
+ try {
+ // Get them all atomically
+ EntityIterator ei = cr.queryEntities(uri, RawContacts.DIRTY + "=1", null, null);
+ ContentValues cidValues = new ContentValues();
+ try {
+ boolean first = true;
+ final Uri rawContactUri = addCallerIsSyncAdapterParameter(RawContacts.CONTENT_URI);
+ while (ei.hasNext()) {
+ Entity entity = ei.next();
+ // For each of these entities, create the change commands
+ ContentValues entityValues = entity.getEntityValues();
+ String serverId = entityValues.getAsString(RawContacts.SOURCE_ID);
+ ArrayList<Integer> groupIds = new ArrayList<Integer>();
+ if (first) {
+ s.start(Tags.SYNC_COMMANDS);
+ userLog("Sending Contacts changes to the server");
+ first = false;
+ }
+ if (serverId == null) {
+ // This is a new contact; create a clientId
+ String clientId = "new_" + mMailbox.mId + '_' + System.currentTimeMillis();
+ userLog("Creating new contact with clientId: ", clientId);
+ s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId);
+ // And save it in the raw contact
+ cidValues.put(ContactsContract.RawContacts.SYNC1, clientId);
+ cr.update(ContentUris.
+ withAppendedId(rawContactUri,
+ entityValues.getAsLong(ContactsContract.RawContacts._ID)),
+ cidValues, null, null);
+ } else {
+ if (entityValues.getAsInteger(RawContacts.DELETED) == 1) {
+ userLog("Deleting contact with serverId: ", serverId);
+ s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
+ mDeletedIdList.add(entityValues.getAsLong(RawContacts._ID));
+ continue;
+ }
+ userLog("Upsync change to contact with serverId: " + serverId);
+ s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId);
+ }
+ s.start(Tags.SYNC_APPLICATION_DATA);
+ // Write out the data here
+ int imCount = 0;
+ int emailCount = 0;
+ int homePhoneCount = 0;
+ int workPhoneCount = 0;
+ String displayName = null;
+ ArrayList<ContentValues> emailValues = new ArrayList<ContentValues>();
+ for (NamedContentValues ncv: entity.getSubValues()) {
+ ContentValues cv = ncv.values;
+ String mimeType = cv.getAsString(Data.MIMETYPE);
+ if (mimeType.equals(Email.CONTENT_ITEM_TYPE)) {
+ emailValues.add(cv);
+ } else if (mimeType.equals(Nickname.CONTENT_ITEM_TYPE)) {
+ sendNickname(s, cv);
+ } else if (mimeType.equals(EasChildren.CONTENT_ITEM_TYPE)) {
+ sendChildren(s, cv);
+ } else if (mimeType.equals(EasBusiness.CONTENT_ITEM_TYPE)) {
+ sendBusiness(s, cv);
+ } else if (mimeType.equals(Website.CONTENT_ITEM_TYPE)) {
+ sendWebpage(s, cv);
+ } else if (mimeType.equals(EasPersonal.CONTENT_ITEM_TYPE)) {
+ sendPersonal(s, cv);
+ } else if (mimeType.equals(Phone.CONTENT_ITEM_TYPE)) {
+ sendPhone(s, cv, workPhoneCount, homePhoneCount);
+ int type = cv.getAsInteger(Phone.TYPE);
+ if (type == Phone.TYPE_HOME) homePhoneCount++;
+ if (type == Phone.TYPE_WORK) workPhoneCount++;
+ } else if (mimeType.equals(Relation.CONTENT_ITEM_TYPE)) {
+ sendRelation(s, cv);
+ } else if (mimeType.equals(StructuredName.CONTENT_ITEM_TYPE)) {
+ displayName = sendStructuredName(s, cv);
+ } else if (mimeType.equals(StructuredPostal.CONTENT_ITEM_TYPE)) {
+ sendStructuredPostal(s, cv);
+ } else if (mimeType.equals(Organization.CONTENT_ITEM_TYPE)) {
+ sendOrganization(s, cv);
+ } else if (mimeType.equals(Im.CONTENT_ITEM_TYPE)) {
+ sendIm(s, cv, imCount++);
+ } else if (mimeType.equals(Event.CONTENT_ITEM_TYPE)) {
+ Integer eventType = cv.getAsInteger(Event.TYPE);
+ if (eventType != null && eventType.equals(Event.TYPE_BIRTHDAY)) {
+ sendBirthday(s, cv);
+ }
+ } else if (mimeType.equals(GroupMembership.CONTENT_ITEM_TYPE)) {
+ // We must gather these, and send them together (below)
+ groupIds.add(cv.getAsInteger(GroupMembership.GROUP_ROW_ID));
+ } else if (mimeType.equals(Note.CONTENT_ITEM_TYPE)) {
+ sendNote(s, cv);
+ } else if (mimeType.equals(Photo.CONTENT_ITEM_TYPE)) {
+ // For now, the user can change the photo, but the change won't be
+ // uploaded.
+ } else {
+ userLog("Contacts upsync, unknown data: ", mimeType);
+ }
+ }
+
+ // We do the email rows last, because we need to make sure we've found the
+ // displayName (if one exists); this would be in a StructuredName rnow
+ for (ContentValues cv: emailValues) {
+ sendEmail(s, cv, emailCount++, displayName);
+ }
+
+ // Now, we'll send up groups, if any
+ if (!groupIds.isEmpty()) {
+ boolean groupFirst = true;
+ for (int id: groupIds) {
+ // Since we get id's from the provider, we need to find their names
+ Cursor c = cr.query(ContentUris.withAppendedId(Groups.CONTENT_URI, id),
+ GROUP_PROJECTION, null, null, null);
+ try {
+ // Presumably, this should always succeed, but ...
+ if (c.moveToFirst()) {
+ if (groupFirst) {
+ s.start(Tags.CONTACTS_CATEGORIES);
+ groupFirst = false;
+ }
+ s.data(Tags.CONTACTS_CATEGORY, c.getString(0));
+ }
+ } finally {
+ c.close();
+ }
+ }
+ if (!groupFirst) {
+ s.end();
+ }
+ }
+ s.end().end(); // ApplicationData & Change
+ mUpdatedIdList.add(entityValues.getAsLong(RawContacts._ID));
+ }
+ if (!first) {
+ s.end(); // Commands
+ }
+ } finally {
+ ei.close();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Could not read dirty contacts.");
+ }
+
+ return false;
+ }
+}
diff --git a/src/com/android/exchange/adapter/EmailSyncAdapter.java b/src/com/android/exchange/adapter/EmailSyncAdapter.java
new file mode 100644
index 0000000..6a5d2a5
--- /dev/null
+++ b/src/com/android/exchange/adapter/EmailSyncAdapter.java
@@ -0,0 +1,716 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to 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.android.exchange.adapter;
+
+import com.android.email.mail.Address;
+import com.android.email.provider.AttachmentProvider;
+import com.android.email.provider.EmailContent;
+import com.android.email.provider.EmailProvider;
+import com.android.email.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.AccountColumns;
+import com.android.email.provider.EmailContent.Attachment;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.email.provider.EmailContent.Message;
+import com.android.email.provider.EmailContent.MessageColumns;
+import com.android.email.provider.EmailContent.SyncColumns;
+import com.android.email.service.MailService;
+import com.android.exchange.Eas;
+import com.android.exchange.EasSyncService;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.webkit.MimeTypeMap;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+/**
+ * Sync adapter for EAS email
+ *
+ */
+public class EmailSyncAdapter extends AbstractSyncAdapter {
+
+ private static final int UPDATES_READ_COLUMN = 0;
+ private static final int UPDATES_MAILBOX_KEY_COLUMN = 1;
+ private static final int UPDATES_SERVER_ID_COLUMN = 2;
+ private static final int UPDATES_FLAG_COLUMN = 3;
+ private static final String[] UPDATES_PROJECTION =
+ {MessageColumns.FLAG_READ, MessageColumns.MAILBOX_KEY, SyncColumns.SERVER_ID,
+ MessageColumns.FLAG_FAVORITE};
+ private static final String[] MESSAGE_ID_SUBJECT_PROJECTION =
+ new String[] { Message.RECORD_ID, MessageColumns.SUBJECT };
+
+
+ String[] bindArguments = new String[2];
+
+ ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
+ ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
+
+ public EmailSyncAdapter(Mailbox mailbox, EasSyncService service) {
+ super(mailbox, service);
+ }
+
+ @Override
+ public boolean parse(InputStream is) throws IOException {
+ EasEmailSyncParser p = new EasEmailSyncParser(is, this);
+ return p.parse();
+ }
+
+ public class EasEmailSyncParser extends AbstractSyncParser {
+
+ private static final String WHERE_SERVER_ID_AND_MAILBOX_KEY =
+ SyncColumns.SERVER_ID + "=? and " + MessageColumns.MAILBOX_KEY + "=?";
+
+ private String mMailboxIdAsString;
+
+ ArrayList<Message> newEmails = new ArrayList<Message>();
+ ArrayList<Long> deletedEmails = new ArrayList<Long>();
+ ArrayList<ServerChange> changedEmails = new ArrayList<ServerChange>();
+
+ public EasEmailSyncParser(InputStream in, EmailSyncAdapter adapter) throws IOException {
+ super(in, adapter);
+ mMailboxIdAsString = Long.toString(mMailbox.mId);
+ }
+
+ @Override
+ public void wipe() {
+ mContentResolver.delete(Message.CONTENT_URI,
+ Message.MAILBOX_KEY + "=" + mMailbox.mId, null);
+ mContentResolver.delete(Message.DELETED_CONTENT_URI,
+ Message.MAILBOX_KEY + "=" + mMailbox.mId, null);
+ mContentResolver.delete(Message.UPDATED_CONTENT_URI,
+ Message.MAILBOX_KEY + "=" + mMailbox.mId, null);
+ }
+
+ public void addData (Message msg) throws IOException {
+ ArrayList<Attachment> atts = new ArrayList<Attachment>();
+
+ while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
+ switch (tag) {
+ case Tags.EMAIL_ATTACHMENTS:
+ case Tags.BASE_ATTACHMENTS: // BASE_ATTACHMENTS is used in EAS 12.0 and up
+ attachmentsParser(atts, msg);
+ break;
+ case Tags.EMAIL_TO:
+ msg.mTo = Address.pack(Address.parse(getValue()));
+ break;
+ case Tags.EMAIL_FROM:
+ Address[] froms = Address.parse(getValue());
+ if (froms != null && froms.length > 0) {
+ msg.mDisplayName = froms[0].toFriendly();
+ }
+ msg.mFrom = Address.pack(froms);
+ break;
+ case Tags.EMAIL_CC:
+ msg.mCc = Address.pack(Address.parse(getValue()));
+ break;
+ case Tags.EMAIL_REPLY_TO:
+ msg.mReplyTo = Address.pack(Address.parse(getValue()));
+ break;
+ case Tags.EMAIL_DATE_RECEIVED:
+ String date = getValue();
+ // 2009-02-11T18:03:03.627Z
+ GregorianCalendar cal = new GregorianCalendar();
+ cal.set(Integer.parseInt(date.substring(0, 4)), Integer.parseInt(date
+ .substring(5, 7)) - 1, Integer.parseInt(date.substring(8, 10)),
+ Integer.parseInt(date.substring(11, 13)), Integer.parseInt(date
+ .substring(14, 16)), Integer.parseInt(date
+ .substring(17, 19)));
+ cal.setTimeZone(TimeZone.getTimeZone("GMT"));
+ msg.mTimeStamp = cal.getTimeInMillis();
+ break;
+ case Tags.EMAIL_SUBJECT:
+ msg.mSubject = getValue();
+ break;
+ case Tags.EMAIL_READ:
+ msg.mFlagRead = getValueInt() == 1;
+ break;
+ case Tags.BASE_BODY:
+ bodyParser(msg);
+ break;
+ case Tags.EMAIL_FLAG:
+ msg.mFlagFavorite = flagParser();
+ break;
+ case Tags.EMAIL_BODY:
+ String text = getValue();
+ msg.mText = text;
+ break;
+ default:
+ skipTag();
+ }
+ }
+
+ if (atts.size() > 0) {
+ msg.mAttachments = atts;
+ }
+ }
+
+ private void addParser(ArrayList<Message> emails) throws IOException {
+ Message msg = new Message();
+ msg.mAccountKey = mAccount.mId;
+ msg.mMailboxKey = mMailbox.mId;
+ msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE;
+
+ while (nextTag(Tags.SYNC_ADD) != END) {
+ switch (tag) {
+ case Tags.SYNC_SERVER_ID:
+ msg.mServerId = getValue();
+ break;
+ case Tags.SYNC_APPLICATION_DATA:
+ addData(msg);
+ break;
+ default:
+ skipTag();
+ }
+ }
+ emails.add(msg);
+ }
+
+ // For now, we only care about the "active" state
+ private Boolean flagParser() throws IOException {
+ Boolean state = false;
+ while (nextTag(Tags.EMAIL_FLAG) != END) {
+ switch (tag) {
+ case Tags.EMAIL_FLAG_STATUS:
+ state = getValueInt() == 2;
+ break;
+ default:
+ skipTag();
+ }
+ }
+ return state;
+ }
+
+ private void bodyParser(Message msg) throws IOException {
+ String bodyType = Eas.BODY_PREFERENCE_TEXT;
+ String body = "";
+ while (nextTag(Tags.EMAIL_BODY) != END) {
+ switch (tag) {
+ case Tags.BASE_TYPE:
+ bodyType = getValue();
+ break;
+ case Tags.BASE_DATA:
+ body = getValue();
+ break;
+ default:
+ skipTag();
+ }
+ }
+ // We always ask for TEXT or HTML; there's no third option
+ if (bodyType.equals(Eas.BODY_PREFERENCE_HTML)) {
+ msg.mHtml = body;
+ } else {
+ msg.mText = body;
+ }
+ }
+
+ private void attachmentsParser(ArrayList<Attachment> atts, Message msg) throws IOException {
+ while (nextTag(Tags.EMAIL_ATTACHMENTS) != END) {
+ switch (tag) {
+ case Tags.EMAIL_ATTACHMENT:
+ case Tags.BASE_ATTACHMENT: // BASE_ATTACHMENT is used in EAS 12.0 and up
+ attachmentParser(atts, msg);
+ break;
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ private void attachmentParser(ArrayList<Attachment> atts, Message msg) throws IOException {
+ String fileName = null;
+ String length = null;
+ String location = null;
+
+ while (nextTag(Tags.EMAIL_ATTACHMENT) != END) {
+ switch (tag) {
+ // We handle both EAS 2.5 and 12.0+ attachments here
+ case Tags.EMAIL_DISPLAY_NAME:
+ case Tags.BASE_DISPLAY_NAME:
+ fileName = getValue();
+ break;
+ case Tags.EMAIL_ATT_NAME:
+ case Tags.BASE_FILE_REFERENCE:
+ location = getValue();
+ break;
+ case Tags.EMAIL_ATT_SIZE:
+ case Tags.BASE_ESTIMATED_DATA_SIZE:
+ length = getValue();
+ break;
+ default:
+ skipTag();
+ }
+ }
+
+ if ((fileName != null) && (length != null) && (location != null)) {
+ Attachment att = new Attachment();
+ att.mEncoding = "base64";
+ att.mSize = Long.parseLong(length);
+ att.mFileName = fileName;
+ att.mLocation = location;
+ att.mMimeType = getMimeTypeFromFileName(fileName);
+ atts.add(att);
+ msg.mFlagAttachment = true;
+ }
+ }
+
+ /**
+ * Try to determine a mime type from a file name, defaulting to application/x, where x
+ * is either the extension or (if none) octet-stream
+ * At the moment, this is somewhat lame, since many file types aren't recognized
+ * @param fileName the file name to ponder
+ * @return
+ */
+ // Note: The MimeTypeMap method currently uses a very limited set of mime types
+ // A bug has been filed against this issue.
+ public String getMimeTypeFromFileName(String fileName) {
+ String mimeType;
+ int lastDot = fileName.lastIndexOf('.');
+ String extension = null;
+ if ((lastDot > 0) && (lastDot < fileName.length() - 1)) {
+ extension = fileName.substring(lastDot + 1).toLowerCase();
+ }
+ if (extension == null) {
+ // A reasonable default for now.
+ mimeType = "application/octet-stream";
+ } else {
+ mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
+ if (mimeType == null) {
+ mimeType = "application/" + extension;
+ }
+ }
+ return mimeType;
+ }
+
+ private Cursor getServerIdCursor(String serverId, String[] projection) {
+ bindArguments[0] = serverId;
+ bindArguments[1] = mMailboxIdAsString;
+ return mContentResolver.query(Message.CONTENT_URI, projection,
+ WHERE_SERVER_ID_AND_MAILBOX_KEY, bindArguments, null);
+ }
+
+ private void deleteParser(ArrayList<Long> deletes, int entryTag) throws IOException {
+ while (nextTag(entryTag) != END) {
+ switch (tag) {
+ case Tags.SYNC_SERVER_ID:
+ String serverId = getValue();
+ // Find the message in this mailbox with the given serverId
+ Cursor c = getServerIdCursor(serverId, MESSAGE_ID_SUBJECT_PROJECTION);
+ try {
+ if (c.moveToFirst()) {
+ deletes.add(c.getLong(0));
+ if (Eas.USER_LOG) {
+ userLog("Deleting ", serverId + ", " + c.getString(1));
+ }
+ }
+ } finally {
+ c.close();
+ }
+ break;
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ class ServerChange {
+ long id;
+ Boolean read;
+ Boolean flag;
+
+ ServerChange(long _id, Boolean _read, Boolean _flag) {
+ id = _id;
+ read = _read;
+ flag = _flag;
+ }
+ }
+
+ private void changeParser(ArrayList<ServerChange> changes) throws IOException {
+ String serverId = null;
+ Boolean oldRead = false;
+ Boolean oldFlag = false;
+ long id = 0;
+ while (nextTag(Tags.SYNC_CHANGE) != END) {
+ switch (tag) {
+ case Tags.SYNC_SERVER_ID:
+ serverId = getValue();
+ Cursor c = getServerIdCursor(serverId, Message.LIST_PROJECTION);
+ try {
+ if (c.moveToFirst()) {
+ userLog("Changing ", serverId);
+ oldRead = c.getInt(Message.LIST_READ_COLUMN) == Message.READ;
+ oldFlag = c.getInt(Message.LIST_FAVORITE_COLUMN) == 1;
+ id = c.getLong(Message.LIST_ID_COLUMN);
+ }
+ } finally {
+ c.close();
+ }
+ break;
+ case Tags.SYNC_APPLICATION_DATA:
+ changeApplicationDataParser(changes, oldRead, oldFlag, id);
+ break;
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ private void changeApplicationDataParser(ArrayList<ServerChange> changes, Boolean oldRead,
+ Boolean oldFlag, long id) throws IOException {
+ Boolean read = null;
+ Boolean flag = null;
+ while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
+ switch (tag) {
+ case Tags.EMAIL_READ:
+ read = getValueInt() == 1;
+ break;
+ case Tags.EMAIL_FLAG:
+ flag = flagParser();
+ break;
+ default:
+ skipTag();
+ }
+ }
+ if (((read != null) && !oldRead.equals(read)) ||
+ ((flag != null) && !oldFlag.equals(flag))) {
+ changes.add(new ServerChange(id, read, flag));
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.exchange.adapter.EasContentParser#commandsParser()
+ */
+ @Override
+ public void commandsParser() throws IOException {
+ while (nextTag(Tags.SYNC_COMMANDS) != END) {
+ if (tag == Tags.SYNC_ADD) {
+ addParser(newEmails);
+ incrementChangeCount();
+ } else if (tag == Tags.SYNC_DELETE || tag == Tags.SYNC_SOFT_DELETE) {
+ deleteParser(deletedEmails, tag);
+ incrementChangeCount();
+ } else if (tag == Tags.SYNC_CHANGE) {
+ changeParser(changedEmails);
+ incrementChangeCount();
+ } else
+ skipTag();
+ }
+ }
+
+ @Override
+ public void responsesParser() {
+ }
+
+ @Override
+ public void commit() {
+ int notifyCount = 0;
+
+ // Use a batch operation to handle the changes
+ // TODO New mail notifications? Who looks for these?
+ ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+ for (Message msg: newEmails) {
+ if (!msg.mFlagRead) {
+ notifyCount++;
+ }
+ msg.addSaveOps(ops);
+ }
+ for (Long id : deletedEmails) {
+ ops.add(ContentProviderOperation.newDelete(
+ ContentUris.withAppendedId(Message.CONTENT_URI, id)).build());
+ AttachmentProvider.deleteAllAttachmentFiles(mContext, mAccount.mId, id);
+ }
+ if (!changedEmails.isEmpty()) {
+ // Server wins in a conflict...
+ for (ServerChange change : changedEmails) {
+ ContentValues cv = new ContentValues();
+ if (change.read != null) {
+ cv.put(MessageColumns.FLAG_READ, change.read);
+ }
+ if (change.flag != null) {
+ cv.put(MessageColumns.FLAG_FAVORITE, change.flag);
+ }
+ ops.add(ContentProviderOperation.newUpdate(
+ ContentUris.withAppendedId(Message.CONTENT_URI, change.id))
+ .withValues(cv)
+ .build());
+ }
+ }
+
+ // We only want to update the sync key here
+ ContentValues mailboxValues = new ContentValues();
+ mailboxValues.put(Mailbox.SYNC_KEY, mMailbox.mSyncKey);
+ ops.add(ContentProviderOperation.newUpdate(
+ ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId))
+ .withValues(mailboxValues).build());
+
+ addCleanupOps(ops);
+
+ // No commits if we're stopped
+ synchronized (mService.getSynchronizer()) {
+ if (mService.isStopped()) return;
+ try {
+ mContentResolver.applyBatch(EmailProvider.EMAIL_AUTHORITY, ops);
+ userLog(mMailbox.mDisplayName, " SyncKey saved as: ", mMailbox.mSyncKey);
+ } catch (RemoteException e) {
+ // There is nothing to be done here; fail by returning null
+ } catch (OperationApplicationException e) {
+ // There is nothing to be done here; fail by returning null
+ }
+ }
+
+ if (notifyCount > 0) {
+ // Use the new atomic add URI in EmailProvider
+ // We could add this to the operations being done, but it's not strictly
+ // speaking necessary, as the previous batch preserves the integrity of the
+ // database, whereas this is purely for notification purposes, and is itself atomic
+ ContentValues cv = new ContentValues();
+ cv.put(EmailContent.FIELD_COLUMN_NAME, AccountColumns.NEW_MESSAGE_COUNT);
+ cv.put(EmailContent.ADD_COLUMN_NAME, notifyCount);
+ Uri uri = ContentUris.withAppendedId(Account.ADD_TO_FIELD_URI, mAccount.mId);
+ mContentResolver.update(uri, cv, null, null);
+ MailService.actionNotifyNewMessages(mContext, mAccount.mId);
+ }
+ }
+ }
+
+ @Override
+ public String getCollectionName() {
+ return "Email";
+ }
+
+ private void addCleanupOps(ArrayList<ContentProviderOperation> ops) {
+ // If we've sent local deletions, clear out the deleted table
+ for (Long id: mDeletedIdList) {
+ ops.add(ContentProviderOperation.newDelete(
+ ContentUris.withAppendedId(Message.DELETED_CONTENT_URI, id)).build());
+ }
+ // And same with the updates
+ for (Long id: mUpdatedIdList) {
+ ops.add(ContentProviderOperation.newDelete(
+ ContentUris.withAppendedId(Message.UPDATED_CONTENT_URI, id)).build());
+ }
+ }
+
+ @Override
+ public void cleanup() {
+ if (!mDeletedIdList.isEmpty() || !mUpdatedIdList.isEmpty()) {
+ ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+ addCleanupOps(ops);
+ try {
+ mContext.getContentResolver()
+ .applyBatch(EmailProvider.EMAIL_AUTHORITY, ops);
+ } catch (RemoteException e) {
+ // There is nothing to be done here; fail by returning null
+ } catch (OperationApplicationException e) {
+ // There is nothing to be done here; fail by returning null
+ }
+ }
+ }
+
+ private String formatTwo(int num) {
+ if (num < 10) {
+ return "0" + (char)('0' + num);
+ } else
+ return Integer.toString(num);
+ }
+
+ /**
+ * Create date/time in RFC8601 format. Oddly enough, for calendar date/time, Microsoft uses
+ * a different format that excludes the punctuation (this is why I'm not putting this in a
+ * parent class)
+ */
+ public String formatDateTime(Calendar calendar) {
+ StringBuilder sb = new StringBuilder();
+ //YYYY-MM-DDTHH:MM:SS.MSSZ
+ sb.append(calendar.get(Calendar.YEAR));
+ sb.append('-');
+ sb.append(formatTwo(calendar.get(Calendar.MONTH) + 1));
+ sb.append('-');
+ sb.append(formatTwo(calendar.get(Calendar.DAY_OF_MONTH)));
+ sb.append('T');
+ sb.append(formatTwo(calendar.get(Calendar.HOUR_OF_DAY)));
+ sb.append(':');
+ sb.append(formatTwo(calendar.get(Calendar.MINUTE)));
+ sb.append(':');
+ sb.append(formatTwo(calendar.get(Calendar.SECOND)));
+ sb.append(".000Z");
+ return sb.toString();
+ }
+
+ @Override
+ public boolean sendLocalChanges(Serializer s) throws IOException {
+ ContentResolver cr = mContext.getContentResolver();
+
+ // Never upsync from these folders
+ if (mMailbox.mType == Mailbox.TYPE_DRAFTS || mMailbox.mType == Mailbox.TYPE_OUTBOX) {
+ return false;
+ }
+
+ // Find any of our deleted items
+ Cursor c = cr.query(Message.DELETED_CONTENT_URI, Message.LIST_PROJECTION,
+ MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null);
+ boolean first = true;
+ // We keep track of the list of deleted item id's so that we can remove them from the
+ // deleted table after the server receives our command
+ mDeletedIdList.clear();
+ try {
+ while (c.moveToNext()) {
+ String serverId = c.getString(Message.LIST_SERVER_ID_COLUMN);
+ // Keep going if there's no serverId
+ if (serverId == null) {
+ continue;
+ } else if (first) {
+ s.start(Tags.SYNC_COMMANDS);
+ first = false;
+ }
+ // Send the command to delete this message
+ s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
+ mDeletedIdList.add(c.getLong(Message.LIST_ID_COLUMN));
+ }
+ } finally {
+ c.close();
+ }
+
+ // Find our trash mailbox, since deletions will have been moved there...
+ long trashMailboxId =
+ Mailbox.findMailboxOfType(mContext, mMailbox.mAccountKey, Mailbox.TYPE_TRASH);
+
+ // Do the same now for updated items
+ c = cr.query(Message.UPDATED_CONTENT_URI, Message.LIST_PROJECTION,
+ MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null);
+
+ // We keep track of the list of updated item id's as we did above with deleted items
+ mUpdatedIdList.clear();
+ try {
+ while (c.moveToNext()) {
+ long id = c.getLong(Message.LIST_ID_COLUMN);
+ // Say we've handled this update
+ mUpdatedIdList.add(id);
+ // We have the id of the changed item. But first, we have to find out its current
+ // state, since the updated table saves the opriginal state
+ Cursor currentCursor = cr.query(ContentUris.withAppendedId(Message.CONTENT_URI, id),
+ UPDATES_PROJECTION, null, null, null);
+ try {
+ // If this item no longer exists (shouldn't be possible), just move along
+ if (!currentCursor.moveToFirst()) {
+ continue;
+ }
+ // Keep going if there's no serverId
+ String serverId = currentCursor.getString(UPDATES_SERVER_ID_COLUMN);
+ if (serverId == null) {
+ continue;
+ }
+ // If the message is now in the trash folder, it has been deleted by the user
+ if (currentCursor.getLong(UPDATES_MAILBOX_KEY_COLUMN) == trashMailboxId) {
+ if (first) {
+ s.start(Tags.SYNC_COMMANDS);
+ first = false;
+ }
+ // Send the command to delete this message
+ s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
+ continue;
+ }
+
+ boolean flagChange = false;
+ boolean readChange = false;
+
+ int flag = 0;
+
+ // We can only send flag changes to the server in 12.0 or later
+ if (mService.mProtocolVersionDouble >= 12.0) {
+ flag = currentCursor.getInt(UPDATES_FLAG_COLUMN);
+ if (flag != c.getInt(Message.LIST_FAVORITE_COLUMN)) {
+ flagChange = true;
+ }
+ }
+
+ int read = currentCursor.getInt(UPDATES_READ_COLUMN);
+ if (read != c.getInt(Message.LIST_READ_COLUMN)) {
+ readChange = true;
+ }
+
+ if (!flagChange && !readChange) {
+ // In this case, we've got nothing to send to the server
+ continue;
+ }
+
+ if (first) {
+ s.start(Tags.SYNC_COMMANDS);
+ first = false;
+ }
+ // Send the change to "read" and "favorite" (flagged)
+ s.start(Tags.SYNC_CHANGE)
+ .data(Tags.SYNC_SERVER_ID, c.getString(Message.LIST_SERVER_ID_COLUMN))
+ .start(Tags.SYNC_APPLICATION_DATA);
+ if (readChange) {
+ s.data(Tags.EMAIL_READ, Integer.toString(read));
+ }
+ // "Flag" is a relatively complex concept in EAS 12.0 and above. It is not only
+ // the boolean "favorite" that we think of in Gmail, but it also represents a
+ // follow up action, which can include a subject, start and due dates, and even
+ // recurrences. We don't support any of this as yet, but EAS 12.0 and higher
+ // require that a flag contain a status, a type, and four date fields, two each
+ // for start date and end (due) date.
+ if (flagChange) {
+ if (flag != 0) {
+ // Status 2 = set flag
+ s.start(Tags.EMAIL_FLAG).data(Tags.EMAIL_FLAG_STATUS, "2");
+ // "FollowUp" is the standard type
+ s.data(Tags.EMAIL_FLAG_TYPE, "FollowUp");
+ long now = System.currentTimeMillis();
+ Calendar calendar =
+ GregorianCalendar.getInstance(TimeZone.getTimeZone("GMT"));
+ calendar.setTimeInMillis(now);
+ // Flags are required to have a start date and end date (duplicated)
+ // First, we'll set the current date/time in GMT as the start time
+ String utc = formatDateTime(calendar);
+ s.data(Tags.TASK_START_DATE, utc).data(Tags.TASK_UTC_START_DATE, utc);
+ // And then we'll use one week from today for completion date
+ calendar.setTimeInMillis(now + 1*WEEKS);
+ utc = formatDateTime(calendar);
+ s.data(Tags.TASK_DUE_DATE, utc).data(Tags.TASK_UTC_DUE_DATE, utc);
+ s.end();
+ } else {
+ s.tag(Tags.EMAIL_FLAG);
+ }
+ }
+ s.end().end(); // SYNC_APPLICATION_DATA, SYNC_CHANGE
+ } finally {
+ currentCursor.close();
+ }
+ }
+ } finally {
+ c.close();
+ }
+
+ if (!first) {
+ s.end(); // SYNC_COMMANDS
+ }
+ return false;
+ }
+}
diff --git a/src/com/android/exchange/adapter/FolderSyncParser.java b/src/com/android/exchange/adapter/FolderSyncParser.java
new file mode 100644
index 0000000..337e329
--- /dev/null
+++ b/src/com/android/exchange/adapter/FolderSyncParser.java
@@ -0,0 +1,403 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to 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.android.exchange.adapter;
+
+import com.android.email.provider.AttachmentProvider;
+import com.android.email.provider.EmailContent;
+import com.android.email.provider.EmailProvider;
+import com.android.email.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.AccountColumns;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.email.provider.EmailContent.MailboxColumns;
+import com.android.exchange.Eas;
+import com.android.exchange.MockParserStream;
+import com.android.exchange.SyncManager;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.os.RemoteException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Parse the result of a FolderSync command
+ *
+ * Handles the addition, deletion, and changes to folders in the user's Exchange account.
+ **/
+
+public class FolderSyncParser extends AbstractSyncParser {
+
+ public static final String TAG = "FolderSyncParser";
+
+ // These are defined by the EAS protocol
+ public static final int USER_FOLDER_TYPE = 1;
+ public static final int INBOX_TYPE = 2;
+ public static final int DRAFTS_TYPE = 3;
+ public static final int DELETED_TYPE = 4;
+ public static final int SENT_TYPE = 5;
+ public static final int OUTBOX_TYPE = 6;
+ public static final int TASKS_TYPE = 7;
+ public static final int CALENDAR_TYPE = 8;
+ public static final int CONTACTS_TYPE = 9;
+ public static final int NOTES_TYPE = 10;
+ public static final int JOURNAL_TYPE = 11;
+ public static final int USER_MAILBOX_TYPE = 12;
+
+ public static final List<Integer> mValidFolderTypes = Arrays.asList(INBOX_TYPE, DRAFTS_TYPE,
+ DELETED_TYPE, SENT_TYPE, OUTBOX_TYPE, USER_MAILBOX_TYPE, CALENDAR_TYPE, CONTACTS_TYPE);
+
+ public static final String ALL_BUT_ACCOUNT_MAILBOX = MailboxColumns.ACCOUNT_KEY + "=? and " +
+ MailboxColumns.TYPE + "!=" + Mailbox.TYPE_EAS_ACCOUNT_MAILBOX;
+
+ private static final String WHERE_SERVER_ID_AND_ACCOUNT = MailboxColumns.SERVER_ID + "=? and " +
+ MailboxColumns.ACCOUNT_KEY + "=?";
+
+ private static final String WHERE_DISPLAY_NAME_AND_ACCOUNT = MailboxColumns.DISPLAY_NAME +
+ "=? and " + MailboxColumns.ACCOUNT_KEY + "=?";
+
+ private static final String WHERE_PARENT_SERVER_ID_AND_ACCOUNT =
+ MailboxColumns.PARENT_SERVER_ID +"=? and " + MailboxColumns.ACCOUNT_KEY + "=?";
+
+ private static final String[] MAILBOX_ID_COLUMNS_PROJECTION =
+ new String[] {MailboxColumns.ID, MailboxColumns.SERVER_ID};
+
+ private long mAccountId;
+ private String mAccountIdAsString;
+ private MockParserStream mMock = null;
+ private String[] mBindArguments = new String[2];
+
+ public FolderSyncParser(InputStream in, AbstractSyncAdapter adapter) throws IOException {
+ super(in, adapter);
+ mAccountId = mAccount.mId;
+ mAccountIdAsString = Long.toString(mAccountId);
+ if (in instanceof MockParserStream) {
+ mMock = (MockParserStream)in;
+ }
+ }
+
+ @Override
+ public boolean parse() throws IOException {
+ int status;
+ boolean res = false;
+ boolean resetFolders = false;
+ if (nextTag(START_DOCUMENT) != Tags.FOLDER_FOLDER_SYNC)
+ throw new EasParserException();
+ while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
+ if (tag == Tags.FOLDER_STATUS) {
+ status = getValueInt();
+ if (status != Eas.FOLDER_STATUS_OK) {
+ mService.errorLog("FolderSync failed: " + status);
+ if (status == Eas.FOLDER_STATUS_INVALID_KEY) {
+ mAccount.mSyncKey = "0";
+ mService.errorLog("Bad sync key; RESET and delete all folders");
+ mContentResolver.delete(Mailbox.CONTENT_URI, ALL_BUT_ACCOUNT_MAILBOX,
+ new String[] {Long.toString(mAccountId)});
+ // Stop existing syncs and reconstruct _main
+ SyncManager.folderListReloaded(mAccountId);
+ res = true;
+ resetFolders = true;
+ } else {
+ // Other errors are at the server, so let's throw an error that will
+ // cause this sync to be retried at a later time
+ mService.errorLog("Throwing IOException; will retry later");
+ throw new EasParserException("Folder status error");
+ }
+ }
+ } else if (tag == Tags.FOLDER_SYNC_KEY) {
+ mAccount.mSyncKey = getValue();
+ userLog("New Account SyncKey: ", mAccount.mSyncKey);
+ } else if (tag == Tags.FOLDER_CHANGES) {
+ changesParser();
+ } else
+ skipTag();
+ }
+ synchronized (mService.getSynchronizer()) {
+ if (!mService.isStopped() || resetFolders) {
+ ContentValues cv = new ContentValues();
+ cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
+ mAccount.update(mContext, cv);
+ userLog("Leaving FolderSyncParser with Account syncKey=", mAccount.mSyncKey);
+ }
+ }
+ return res;
+ }
+
+ private Cursor getServerIdCursor(String serverId) {
+ mBindArguments[0] = serverId;
+ mBindArguments[1] = mAccountIdAsString;
+ return mContentResolver.query(Mailbox.CONTENT_URI, EmailContent.ID_PROJECTION,
+ WHERE_SERVER_ID_AND_ACCOUNT, mBindArguments, null);
+ }
+
+ public void deleteParser(ArrayList<ContentProviderOperation> ops) throws IOException {
+ while (nextTag(Tags.FOLDER_DELETE) != END) {
+ switch (tag) {
+ case Tags.FOLDER_SERVER_ID:
+ String serverId = getValue();
+ // Find the mailbox in this account with the given serverId
+ Cursor c = getServerIdCursor(serverId);
+ try {
+ if (c.moveToFirst()) {
+ userLog("Deleting ", serverId);
+ ops.add(ContentProviderOperation.newDelete(
+ ContentUris.withAppendedId(Mailbox.CONTENT_URI,
+ c.getLong(0))).build());
+ AttachmentProvider.deleteAllMailboxAttachmentFiles(mContext,
+ mAccountId, mMailbox.mId);
+ }
+ } finally {
+ c.close();
+ }
+ break;
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ public void addParser(ArrayList<ContentProviderOperation> ops) throws IOException {
+ String name = null;
+ String serverId = null;
+ String parentId = null;
+ int type = 0;
+
+ while (nextTag(Tags.FOLDER_ADD) != END) {
+ switch (tag) {
+ case Tags.FOLDER_DISPLAY_NAME: {
+ name = getValue();
+ break;
+ }
+ case Tags.FOLDER_TYPE: {
+ type = getValueInt();
+ break;
+ }
+ case Tags.FOLDER_PARENT_ID: {
+ parentId = getValue();
+ break;
+ }
+ case Tags.FOLDER_SERVER_ID: {
+ serverId = getValue();
+ break;
+ }
+ default:
+ skipTag();
+ }
+ }
+ if (mValidFolderTypes.contains(type)) {
+ Mailbox m = new Mailbox();
+ m.mDisplayName = name;
+ m.mServerId = serverId;
+ m.mAccountKey = mAccountId;
+ m.mType = Mailbox.TYPE_MAIL;
+ // Note that all mailboxes default to checking "never" (i.e. manual sync only)
+ // We set specific intervals for inbox, contacts, and (eventually) calendar
+ m.mSyncInterval = Mailbox.CHECK_INTERVAL_NEVER;
+ switch (type) {
+ case INBOX_TYPE:
+ m.mType = Mailbox.TYPE_INBOX;
+ m.mSyncInterval = mAccount.mSyncInterval;
+ break;
+ case CONTACTS_TYPE:
+ m.mType = Mailbox.TYPE_CONTACTS;
+ m.mSyncInterval = mAccount.mSyncInterval;
+ break;
+ case OUTBOX_TYPE:
+ // TYPE_OUTBOX mailboxes are known by SyncManager to sync whenever they aren't
+ // empty. The value of mSyncFrequency is ignored for this kind of mailbox.
+ m.mType = Mailbox.TYPE_OUTBOX;
+ break;
+ case SENT_TYPE:
+ m.mType = Mailbox.TYPE_SENT;
+ break;
+ case DRAFTS_TYPE:
+ m.mType = Mailbox.TYPE_DRAFTS;
+ break;
+ case DELETED_TYPE:
+ m.mType = Mailbox.TYPE_TRASH;
+ break;
+ case CALENDAR_TYPE:
+ m.mType = Mailbox.TYPE_CALENDAR;
+ // For now, no sync, since it's not yet implemented
+ break;
+ }
+
+ // Make boxes like Contacts and Calendar invisible in the folder list
+ m.mFlagVisible = (m.mType < Mailbox.TYPE_NOT_EMAIL);
+
+ if (!parentId.equals("0")) {
+ m.mParentServerId = parentId;
+ }
+
+ userLog("Adding mailbox: ", m.mDisplayName);
+ ops.add(ContentProviderOperation
+ .newInsert(Mailbox.CONTENT_URI).withValues(m.toContentValues()).build());
+ }
+
+ return;
+ }
+
+ public void updateParser(ArrayList<ContentProviderOperation> ops) throws IOException {
+ String serverId = null;
+ String displayName = null;
+ String parentId = null;
+ while (nextTag(Tags.FOLDER_UPDATE) != END) {
+ switch (tag) {
+ case Tags.FOLDER_SERVER_ID:
+ serverId = getValue();
+ break;
+ case Tags.FOLDER_DISPLAY_NAME:
+ displayName = getValue();
+ break;
+ case Tags.FOLDER_PARENT_ID:
+ parentId = getValue();
+ break;
+ default:
+ skipTag();
+ break;
+ }
+ }
+ // We'll make a change if one of parentId or displayName are specified
+ // serverId is required, but let's be careful just the same
+ if (serverId != null && (displayName != null || parentId != null)) {
+ Cursor c = getServerIdCursor(serverId);
+ try {
+ // If we find the mailbox (using serverId), make the change
+ if (c.moveToFirst()) {
+ userLog("Updating ", serverId);
+ ContentValues cv = new ContentValues();
+ if (displayName != null) {
+ cv.put(Mailbox.DISPLAY_NAME, displayName);
+ }
+ if (parentId != null) {
+ cv.put(Mailbox.PARENT_SERVER_ID, parentId);
+ }
+ ops.add(ContentProviderOperation.newUpdate(
+ ContentUris.withAppendedId(Mailbox.CONTENT_URI,
+ c.getLong(0))).withValues(cv).build());
+ }
+ } finally {
+ c.close();
+ }
+ }
+ }
+
+ public void changesParser() throws IOException {
+ // Keep track of new boxes, deleted boxes, updated boxes
+ ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+
+ while (nextTag(Tags.FOLDER_CHANGES) != END) {
+ if (tag == Tags.FOLDER_ADD) {
+ addParser(ops);
+ } else if (tag == Tags.FOLDER_DELETE) {
+ deleteParser(ops);
+ } else if (tag == Tags.FOLDER_UPDATE) {
+ updateParser(ops);
+ } else if (tag == Tags.FOLDER_COUNT) {
+ getValueInt();
+ } else
+ skipTag();
+ }
+
+ // The mock stream is used for junit tests, so that the parsing code can be tested
+ // separately from the provider code.
+ // TODO Change tests to not require this; remove references to the mock stream
+ if (mMock != null) {
+ mMock.setResult(null);
+ return;
+ }
+
+ // Create the new mailboxes in a single batch operation
+ // Don't save any data if the service has been stopped
+ synchronized (mService.getSynchronizer()) {
+ if (!ops.isEmpty() && !mService.isStopped()) {
+ userLog("Applying ", ops.size(), " mailbox operations.");
+
+ // Then, we create an update for the account (most importantly, updating the syncKey)
+ ops.add(ContentProviderOperation.newUpdate(
+ ContentUris.withAppendedId(Account.CONTENT_URI, mAccountId)).withValues(
+ mAccount.toContentValues()).build());
+
+ // Finally, we execute the batch
+ try {
+ mContentResolver.applyBatch(EmailProvider.EMAIL_AUTHORITY, ops);
+ userLog("New Account SyncKey: ", mAccount.mSyncKey);
+ } catch (RemoteException e) {
+ // There is nothing to be done here; fail by returning null
+ } catch (OperationApplicationException e) {
+ // There is nothing to be done here; fail by returning null
+ }
+
+ // Look for sync issues and its children and delete them
+ // I'm not aware of any other way to deal with this properly
+ mBindArguments[0] = "Sync Issues";
+ mBindArguments[1] = mAccountIdAsString;
+ Cursor c = mContentResolver.query(Mailbox.CONTENT_URI,
+ MAILBOX_ID_COLUMNS_PROJECTION, WHERE_DISPLAY_NAME_AND_ACCOUNT,
+ mBindArguments, null);
+ String parentServerId = null;
+ long id = 0;
+ try {
+ if (c.moveToFirst()) {
+ id = c.getLong(0);
+ parentServerId = c.getString(1);
+ }
+ } finally {
+ c.close();
+ }
+ if (parentServerId != null) {
+ mContentResolver.delete(ContentUris.withAppendedId(Mailbox.CONTENT_URI, id),
+ null, null);
+ mBindArguments[0] = parentServerId;
+ mContentResolver.delete(Mailbox.CONTENT_URI, WHERE_PARENT_SERVER_ID_AND_ACCOUNT,
+ mBindArguments);
+ }
+ }
+ }
+ }
+
+ /**
+ * Not needed for FolderSync parsing; everything is done within changesParser
+ */
+ @Override
+ public void commandsParser() throws IOException {
+ }
+
+ /**
+ * We don't need to implement commit() because all operations take place atomically within
+ * changesParser
+ */
+ @Override
+ public void commit() throws IOException {
+ }
+
+ @Override
+ public void wipe() {
+ }
+
+ @Override
+ public void responsesParser() throws IOException {
+ }
+
+}
diff --git a/src/com/android/exchange/adapter/Parser.java b/src/com/android/exchange/adapter/Parser.java
new file mode 100644
index 0000000..ffe8039
--- /dev/null
+++ b/src/com/android/exchange/adapter/Parser.java
@@ -0,0 +1,502 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to 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.android.exchange.adapter;
+
+import com.android.exchange.Eas;
+import com.android.exchange.EasException;
+import com.android.exchange.utility.FileLogger;
+
+import org.kxml2.wap.Wbxml;
+
+import android.content.Context;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+/**
+ * Extremely fast and lightweight WBXML parser, implementing only the subset of WBXML that
+ * EAS uses (as defined in the EAS specification)
+ *
+ */
+public abstract class Parser {
+
+ private static final String TAG = "EasParser";
+
+ // The following constants are Wbxml standard
+ public static final int START_DOCUMENT = 0;
+ public static final int DONE = 1;
+ public static final int START = 2;
+ public static final int END = 3;
+ public static final int TEXT = 4;
+ public static final int END_DOCUMENT = 3;
+ private static final int NOT_FETCHED = Integer.MIN_VALUE;
+ private static final int NOT_ENDED = Integer.MIN_VALUE;
+ private static final int EOF_BYTE = -1;
+ private boolean logging = false;
+ private boolean capture = false;
+
+ private ArrayList<Integer> captureArray;
+
+ // The input stream for this parser
+ private InputStream in;
+
+ // The current tag depth
+ private int depth;
+
+ // The upcoming (saved) id from the stream
+ private int nextId = NOT_FETCHED;
+
+ // The current tag table (i.e. the tag table for the current page)
+ private String[] tagTable;
+
+ // An array of tag tables, as defined in EasTags
+ static private String[][] tagTables = new String[24][];
+
+ // The stack of names of tags being processed; used when debug = true
+ private String[] nameArray = new String[32];
+
+ // The stack of tags being processed
+ private int[] startTagArray = new int[32];
+
+ // The following vars are available to all to avoid method calls that represent the state of
+ // the parser at any given time
+ public int endTag = NOT_ENDED;
+
+ public int startTag;
+
+ // The type of the last token read
+ public int type;
+
+ // The current page
+ public int page;
+
+ // The current tag
+ public int tag;
+
+ // The name of the current tag
+ public String name;
+
+ // Whether the current tag is associated with content (a value)
+ private boolean noContent;
+
+ // The value read, as a String. Only one of text or num will be valid, depending on whether the
+ // value was requested as a String or an int (to avoid wasted effort in parsing)
+ public String text;
+
+ // The value read, as an int
+ public int num;
+
+ public class EofException extends IOException {
+ private static final long serialVersionUID = 1L;
+ }
+
+ public class EodException extends IOException {
+ private static final long serialVersionUID = 1L;
+ }
+
+ public class EasParserException extends IOException {
+ private static final long serialVersionUID = 1L;
+
+ EasParserException() {
+ super("WBXML format error");
+ }
+
+ EasParserException(String reason) {
+ super(reason);
+ }
+ }
+
+ public boolean parse() throws IOException, EasException {
+ return false;
+ }
+
+ /**
+ * Initialize the tag tables; they are constant
+ *
+ */
+ {
+ String[][] pages = Tags.pages;
+ for (int i = 0; i < pages.length; i++) {
+ String[] page = pages[i];
+ if (page.length > 0) {
+ tagTables[i] = page;
+ }
+ }
+ }
+
+ public Parser(InputStream in) throws IOException {
+ setInput(in);
+ logging = Eas.PARSER_LOG;
+ }
+
+ /**
+ * Set the debug state of the parser. When debugging is on, every token is logged (Log.v) to
+ * the console.
+ *
+ * @param val the desired state for debug output
+ */
+ public void setDebug(boolean val) {
+ logging = val;
+ }
+
+ /**
+ * Turns on data capture; this is used to create test streams that represent "live" data and
+ * can be used against the various parsers.
+ */
+ public void captureOn() {
+ capture = true;
+ captureArray = new ArrayList<Integer>();
+ }
+
+ /**
+ * Turns off data capture; writes the captured data to a specified file.
+ */
+ public void captureOff(Context context, String file) {
+ try {
+ FileOutputStream out = context.openFileOutput(file, Context.MODE_WORLD_WRITEABLE);
+ out.write(captureArray.toString().getBytes());
+ out.close();
+ } catch (FileNotFoundException e) {
+ // This is debug code; exceptions aren't interesting.
+ } catch (IOException e) {
+ // This is debug code; exceptions aren't interesting.
+ }
+ }
+
+ /**
+ * Return the value of the current tag, as a String
+ *
+ * @return the String value of the current tag
+ * @throws IOException
+ */
+ public String getValue() throws IOException {
+ // The false argument tells getNext to return the value as a String
+ getNext(false);
+ // Save the value
+ String val = text;
+ // Read the next token; it had better be the end of the current tag
+ getNext(false);
+ // If not, throw an exception
+ if (type != END) {
+ throw new IOException("No END found!");
+ }
+ endTag = startTag;
+ return val;
+ }
+
+ /**
+ * Return the value of the current tag, as an integer
+ *
+ * @return the integer value of the current tag
+ * @throws IOException
+ */
+ public int getValueInt() throws IOException {
+ // The true argument to getNext indicates the desire for an integer return value
+ getNext(true);
+ // Save the value
+ int val = num;
+ // Read the next token; it had better be the end of the current tag
+ getNext(false);
+ // If not, throw an exception
+ if (type != END) {
+ throw new IOException("No END found!");
+ }
+ endTag = startTag;
+ return val;
+ }
+
+ /**
+ * Return the next tag found in the stream; special tags END and END_DOCUMENT are used to
+ * mark the end of the current tag and end of document. If we hit end of document without
+ * looking for it, generate an EodException. The tag returned consists of the page number
+ * shifted PAGE_SHIFT bits OR'd with the tag retrieved from the stream. Thus, all tags returned
+ * are unique.
+ *
+ * @param endingTag the tag that would represent the end of the tag we're processing
+ * @return the next tag found
+ * @throws IOException
+ */
+ public int nextTag(int endingTag) throws IOException {
+ // Lose the page information
+ endTag = endingTag &= Tags.PAGE_MASK;
+ while (getNext(false) != DONE) {
+ // If we're a start, set tag to include the page and return it
+ if (type == START) {
+ tag = page | startTag;
+ return tag;
+ // If we're at the ending tag we're looking for, return the END signal
+ } else if (type == END && startTag == endTag) {
+ return END;
+ }
+ }
+ // We're at end of document here. If we're looking for it, return END_DOCUMENT
+ if (endTag == START_DOCUMENT) {
+ return END_DOCUMENT;
+ }
+ // Otherwise, we've prematurely hit end of document, so exception out
+ // EodException is a subclass of IOException; this will be treated as an IO error by
+ // SyncManager.
+ throw new EodException();
+ }
+
+ /**
+ * Skip anything found in the stream until the end of the current tag is reached. This can be
+ * used to ignore stretches of xml that aren't needed by the parser.
+ *
+ * @throws IOException
+ */
+ public void skipTag() throws IOException {
+ int thisTag = startTag;
+ // Just loop until we hit the end of the current tag
+ while (getNext(false) != DONE) {
+ if (type == END && startTag == thisTag) {
+ return;
+ }
+ }
+
+ // If we're at end of document, that's bad
+ throw new EofException();
+ }
+
+ /**
+ * Retrieve the next token from the input stream
+ *
+ * @return the token found
+ * @throws IOException
+ */
+ public int nextToken() throws IOException {
+ getNext(false);
+ return type;
+ }
+
+ /**
+ * Initializes the parser with an input stream; reads the first 4 bytes (which are always the
+ * same in EAS, and then sets the tag table to point to page 0 (by definition, the starting
+ * page).
+ *
+ * @param in the InputStream associated with this parser
+ * @throws IOException
+ */
+ public void setInput(InputStream in) throws IOException {
+ this.in = in;
+ readByte(); // version
+ readInt(); // ?
+ readInt(); // 106 (UTF-8)
+ readInt(); // string table length
+ tagTable = tagTables[0];
+ }
+
+ void log(String str) {
+ int cr = str.indexOf('\n');
+ if (cr > 0) {
+ str = str.substring(0, cr);
+ }
+ Log.v(TAG, str);
+ if (Eas.FILE_LOG) {
+ FileLogger.log(TAG, str);
+ }
+ }
+
+ /**
+ * Return the next piece of data from the stream. The return value indicates the type of data
+ * that has been retrieved - START (start of tag), END (end of tag), DONE (end of stream), or
+ * TEXT (the value of a tag)
+ *
+ * @param asInt whether a TEXT value should be parsed as a String or an int.
+ * @return the type of data retrieved
+ * @throws IOException
+ */
+ private final int getNext(boolean asInt) throws IOException {
+ int savedEndTag = endTag;
+ if (type == END) {
+ depth--;
+ } else {
+ endTag = NOT_ENDED;
+ }
+
+ if (noContent) {
+ type = END;
+ noContent = false;
+ endTag = savedEndTag;
+ return type;
+ }
+
+ text = null;
+ name = null;
+
+ int id = nextId ();
+ while (id == Wbxml.SWITCH_PAGE) {
+ nextId = NOT_FETCHED;
+ // Get the new page number
+ int pg = readByte();
+ // Save the shifted page to add into the startTag in nextTag
+ page = pg << Tags.PAGE_SHIFT;
+ // Retrieve the current tag table
+ tagTable = tagTables[pg];
+ id = nextId();
+ }
+ nextId = NOT_FETCHED;
+
+ switch (id) {
+ case EOF_BYTE:
+ // End of document
+ type = DONE;
+ break;
+
+ case Wbxml.END:
+ // End of tag
+ type = END;
+ if (logging) {
+ name = nameArray[depth];
+ //log("</" + name + '>');
+ }
+ // Retrieve the now-current startTag from our stack
+ startTag = endTag = startTagArray[depth];
+ break;
+
+ case Wbxml.STR_I:
+ // Inline string
+ type = TEXT;
+ if (asInt) {
+ num = readInlineInt();
+ } else {
+ text = readInlineString();
+ }
+ if (logging) {
+ name = tagTable[startTag - 5];
+ log(name + ": " + (asInt ? Integer.toString(num) : text));
+ }
+ break;
+
+ default:
+ // Start of tag
+ type = START;
+ // The tag is in the low 6 bits
+ startTag = id & 0x3F;
+ // If the high bit is set, there is content (a value) to be read
+ noContent = (id & 0x40) == 0;
+ depth++;
+ if (logging) {
+ name = tagTable[startTag - 5];
+ //log('<' + name + '>');
+ nameArray[depth] = name;
+ }
+ // Save the startTag to our stack
+ startTagArray[depth] = startTag;
+ }
+
+ // Return the type of data we're dealing with
+ return type;
+ }
+
+ /**
+ * Read an int from the input stream, and capture it if necessary for debugging. Seems a small
+ * price to pay...
+ *
+ * @return the int read
+ * @throws IOException
+ */
+ private int read() throws IOException {
+ int i;
+ i = in.read();
+ if (capture) {
+ captureArray.add(i);
+ }
+ return i;
+ }
+
+ private int nextId() throws IOException {
+ if (nextId == NOT_FETCHED) {
+ nextId = read();
+ }
+ return nextId;
+ }
+
+ private int readByte() throws IOException {
+ int i = read();
+ if (i == EOF_BYTE) {
+ throw new EofException();
+ }
+ return i;
+ }
+
+ /**
+ * Read an integer from the stream; this is called when the parser knows that what follows is
+ * an inline string representing an integer (e.g. the Read tag in Email has a value known to
+ * be either "0" or "1")
+ *
+ * @return the integer as parsed from the stream
+ * @throws IOException
+ */
+ private int readInlineInt() throws IOException {
+ int result = 0;
+
+ while (true) {
+ int i = readByte();
+ // Inline strings are always terminated with a zero byte
+ if (i == 0) {
+ return result;
+ }
+ if (i >= '0' && i <= '9') {
+ result = (result * 10) + (i - '0');
+ } else {
+ throw new IOException("Non integer");
+ }
+ }
+ }
+
+ private int readInt() throws IOException {
+ int result = 0;
+ int i;
+
+ do {
+ i = readByte();
+ result = (result << 7) | (i & 0x7f);
+ } while ((i & 0x80) != 0);
+
+ return result;
+ }
+
+ /**
+ * Read an inline string from the stream
+ *
+ * @return the String as parsed from the stream
+ * @throws IOException
+ */
+ private String readInlineString() throws IOException {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream(256);
+ while (true) {
+ int i = read();
+ if (i == 0) {
+ break;
+ } else if (i == EOF_BYTE) {
+ throw new EofException();
+ }
+ outputStream.write(i);
+ }
+ outputStream.flush();
+ String res = outputStream.toString("UTF-8");
+ outputStream.close();
+ return res;
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/exchange/adapter/PingParser.java b/src/com/android/exchange/adapter/PingParser.java
new file mode 100644
index 0000000..0731ac0
--- /dev/null
+++ b/src/com/android/exchange/adapter/PingParser.java
@@ -0,0 +1,90 @@
+/* Copyright (C) 2008-2009 Marc Blank
+ * Licensed to 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.android.exchange.adapter;
+
+import com.android.exchange.EasSyncService;
+import com.android.exchange.StaleFolderListException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+/**
+ * Parse the result of a Ping command.
+ *
+ * If there are folders with changes, add the serverId of those folders to the syncList array.
+ * If the folder list needs to be reloaded, throw a StaleFolderListException, which will be caught
+ * by the sync server, which will sync the updated folder list.
+ */
+public class PingParser extends Parser {
+ private ArrayList<String> syncList = new ArrayList<String>();
+ private EasSyncService mService;
+ private int mSyncStatus = 0;
+
+ public ArrayList<String> getSyncList() {
+ return syncList;
+ }
+
+ public int getSyncStatus() {
+ return mSyncStatus;
+ }
+
+ public PingParser(InputStream in, EasSyncService service) throws IOException {
+ super(in);
+ mService = service;
+ }
+
+ public void parsePingFolders(ArrayList<String> syncList) throws IOException {
+ while (nextTag(Tags.PING_FOLDERS) != END) {
+ if (tag == Tags.PING_FOLDER) {
+ // Here we'll keep track of which mailboxes need syncing
+ String serverId = getValue();
+ syncList.add(serverId);
+ mService.userLog("Changes found in: ", serverId);
+ } else {
+ skipTag();
+ }
+ }
+ }
+
+ @Override
+ public boolean parse() throws IOException, StaleFolderListException {
+ boolean res = false;
+ if (nextTag(START_DOCUMENT) != Tags.PING_PING) {
+ throw new IOException();
+ }
+ while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
+ if (tag == Tags.PING_STATUS) {
+ int status = getValueInt();
+ mSyncStatus = status;
+ mService.userLog("Ping completed, status = ", status);
+ if (status == 2) {
+ res = true;
+ } else if (status == 7 || status == 4) {
+ // Status of 7 or 4 indicate a stale folder list
+ throw new StaleFolderListException();
+ }
+ } else if (tag == Tags.PING_FOLDERS) {
+ parsePingFolders(syncList);
+ } else {
+ skipTag();
+ }
+ }
+ return res;
+ }
+}
+
diff --git a/src/com/android/exchange/adapter/Serializer.java b/src/com/android/exchange/adapter/Serializer.java
new file mode 100644
index 0000000..32909bf
--- /dev/null
+++ b/src/com/android/exchange/adapter/Serializer.java
@@ -0,0 +1,190 @@
+/* Copyright (c) 2002,2003, Stefan Haustein, Oberhausen, Rhld., Germany
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE. */
+
+//Contributors: Jonathan Cox, Bogdan Onoiu, Jerry Tian
+//Simplified for Google, Inc. by Marc Blank
+
+package com.android.exchange.adapter;
+
+import com.android.exchange.Eas;
+import com.android.exchange.utility.FileLogger;
+
+import org.kxml2.wap.Wbxml;
+
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Hashtable;
+
+public class Serializer {
+
+ private static final String TAG = "Serializer";
+ private static final boolean logging = false; // DO NOT CHECK IN WITH THIS TRUE!
+
+ private static final int NOT_PENDING = -1;
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ByteArrayOutputStream buf = new ByteArrayOutputStream();
+
+ String pending;
+ int pendingTag = NOT_PENDING;
+ int depth;
+ String name;
+ String[] nameStack = new String[20];
+
+ Hashtable<String, Object> tagTable = new Hashtable<String, Object>();
+
+ private int tagPage;
+
+ public Serializer() {
+ super();
+ try {
+ startDocument();
+ //logging = Eas.PARSER_LOG;
+ } catch (IOException e) {
+ // Nothing to be done
+ }
+ }
+
+ void log(String str) {
+ int cr = str.indexOf('\n');
+ if (cr > 0) {
+ str = str.substring(0, cr);
+ }
+ Log.v(TAG, str);
+ if (Eas.FILE_LOG) {
+ FileLogger.log(TAG, str);
+ }
+ }
+
+ public void done() throws IOException {
+ if (depth != 0) {
+ throw new IOException("Done received with unclosed tags");
+ }
+ writeInteger(out, 0);
+ out.write(buf.toByteArray());
+ out.flush();
+ }
+
+ public void startDocument() throws IOException{
+ out.write(0x03); // version 1.3
+ out.write(0x01); // unknown or missing public identifier
+ out.write(106);
+ }
+
+ public void checkPendingTag(boolean degenerated) throws IOException {
+ if (pendingTag == NOT_PENDING)
+ return;
+
+ int page = pendingTag >> Tags.PAGE_SHIFT;
+ int tag = pendingTag & Tags.PAGE_MASK;
+ if (page != tagPage) {
+ tagPage = page;
+ buf.write(Wbxml.SWITCH_PAGE);
+ buf.write(page);
+ }
+
+ buf.write(degenerated ? tag : tag | 64);
+ if (logging) {
+ String name = Tags.pages[page][tag - 5];
+ nameStack[depth] = name;
+ log("<" + name + '>');
+ }
+ pendingTag = NOT_PENDING;
+ }
+
+ public Serializer start(int tag) throws IOException {
+ checkPendingTag(false);
+ pendingTag = tag;
+ depth++;
+ return this;
+ }
+
+ public Serializer end() throws IOException {
+ if (pendingTag >= 0) {
+ checkPendingTag(true);
+ } else {
+ buf.write(Wbxml.END);
+ if (logging) {
+ log("</" + nameStack[depth] + '>');
+ }
+ }
+ depth--;
+ return this;
+ }
+
+ public Serializer tag(int t) throws IOException {
+ start(t);
+ end();
+ return this;
+ }
+
+ public Serializer data(int tag, String value) throws IOException {
+ start(tag);
+ text(value);
+ end();
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return out.toString();
+ }
+
+ public byte[] toByteArray() {
+ return out.toByteArray();
+ }
+
+ public Serializer text(String text) throws IOException {
+ checkPendingTag(false);
+ buf.write(Wbxml.STR_I);
+ writeLiteralString(buf, text);
+ if (logging) {
+ log(text);
+ }
+ return this;
+ }
+
+ void writeInteger(OutputStream out, int i) throws IOException {
+ byte[] buf = new byte[5];
+ int idx = 0;
+
+ do {
+ buf[idx++] = (byte) (i & 0x7f);
+ i = i >> 7;
+ } while (i != 0);
+
+ while (idx > 1) {
+ out.write(buf[--idx] | 0x80);
+ }
+ out.write(buf[0]);
+ if (logging) {
+ log(Integer.toString(i));
+ }
+ }
+
+ void writeLiteralString(OutputStream out, String s) throws IOException {
+ byte[] data = s.getBytes("UTF-8");
+ out.write(data);
+ out.write(0);
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/exchange/adapter/Tags.java b/src/com/android/exchange/adapter/Tags.java
new file mode 100644
index 0000000..d822181
--- /dev/null
+++ b/src/com/android/exchange/adapter/Tags.java
@@ -0,0 +1,518 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to 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.android.exchange.adapter;
+
+/**
+ * The wbxml tags for EAS are all defined here.
+ *
+ * The static final int's, of the form <page>_<tag> = <constant> are used in parsing incoming
+ * responses from the server (i.e. EasParser and its subclasses).
+ *
+ * The array of String arrays is used to construct server requests with EasSerializer. One thing
+ * we might do eventually is to "precompile" these requests, in part, although they should be
+ * fairly fast to begin with (each tag requires one HashMap lookup, and there aren't all that many
+ * of them in a given command).
+ *
+ */
+public class Tags {
+
+ // Wbxml page definitions for EAS
+ public static final int AIRSYNC = 0x00;
+ public static final int CONTACTS = 0x01;
+ public static final int EMAIL = 0x02;
+ public static final int CALENDAR = 0x04;
+ public static final int MOVE = 0x05;
+ public static final int GIE = 0x06;
+ public static final int FOLDER = 0x07;
+ public static final int TASK = 0x09;
+ public static final int CONTACTS2 = 0x0C;
+ public static final int PING = 0x0D;
+ public static final int GAL = 0x10;
+ public static final int BASE = 0x11;
+
+ // Shift applied to page numbers to generate tag
+ public static final int PAGE_SHIFT = 6;
+ public static final int PAGE_MASK = 0x3F; // 6 bits
+
+ public static final int SYNC_PAGE = 0 << PAGE_SHIFT;
+ public static final int SYNC_SYNC = SYNC_PAGE + 5;
+ public static final int SYNC_RESPONSES = SYNC_PAGE + 6;
+ public static final int SYNC_ADD = SYNC_PAGE + 7;
+ public static final int SYNC_CHANGE = SYNC_PAGE + 8;
+ public static final int SYNC_DELETE = SYNC_PAGE + 9;
+ public static final int SYNC_FETCH = SYNC_PAGE + 0xA;
+ public static final int SYNC_SYNC_KEY = SYNC_PAGE + 0xB;
+ public static final int SYNC_CLIENT_ID = SYNC_PAGE + 0xC;
+ public static final int SYNC_SERVER_ID = SYNC_PAGE + 0xD;
+ public static final int SYNC_STATUS = SYNC_PAGE + 0xE;
+ public static final int SYNC_COLLECTION = SYNC_PAGE + 0xF;
+ public static final int SYNC_CLASS = SYNC_PAGE + 0x10;
+ public static final int SYNC_VERSION = SYNC_PAGE + 0x11;
+ public static final int SYNC_COLLECTION_ID = SYNC_PAGE + 0x12;
+ public static final int SYNC_GET_CHANGES = SYNC_PAGE + 0x13;
+ public static final int SYNC_MORE_AVAILABLE = SYNC_PAGE + 0x14;
+ public static final int SYNC_WINDOW_SIZE = SYNC_PAGE + 0x15;
+ public static final int SYNC_COMMANDS = SYNC_PAGE + 0x16;
+ public static final int SYNC_OPTIONS = SYNC_PAGE + 0x17;
+ public static final int SYNC_FILTER_TYPE = SYNC_PAGE + 0x18;
+ public static final int SYNC_TRUNCATION = SYNC_PAGE + 0x19;
+ public static final int SYNC_RTF_TRUNCATION = SYNC_PAGE + 0x1A;
+ public static final int SYNC_CONFLICT = SYNC_PAGE + 0x1B;
+ public static final int SYNC_COLLECTIONS = SYNC_PAGE + 0x1C;
+ public static final int SYNC_APPLICATION_DATA = SYNC_PAGE + 0x1D;
+ public static final int SYNC_DELETES_AS_MOVES = SYNC_PAGE + 0x1E;
+ public static final int SYNC_NOTIFY_GUID = SYNC_PAGE + 0x1F;
+ public static final int SYNC_SUPPORTED = SYNC_PAGE + 0x20;
+ public static final int SYNC_SOFT_DELETE = SYNC_PAGE + 0x21;
+ public static final int SYNC_MIME_SUPPORT = SYNC_PAGE + 0x22;
+ public static final int SYNC_MIME_TRUNCATION = SYNC_PAGE + 0x23;
+ public static final int SYNC_WAIT = SYNC_PAGE + 0x24;
+ public static final int SYNC_LIMIT = SYNC_PAGE + 0x25;
+ public static final int SYNC_PARTIAL = SYNC_PAGE + 0x26;
+
+
+ public static final int GIE_PAGE = GIE << PAGE_SHIFT;
+ public static final int GIE_GET_ITEM_ESTIMATE = GIE_PAGE + 5;
+ public static final int GIE_VERSION = GIE_PAGE + 6;
+ public static final int GIE_COLLECTIONS = GIE_PAGE + 7;
+ public static final int GIE_COLLECTION = GIE_PAGE + 8;
+ public static final int GIE_CLASS = GIE_PAGE + 9;
+ public static final int GIE_COLLECTION_ID = GIE_PAGE + 0xA;
+ public static final int GIE_DATE_TIME = GIE_PAGE + 0xB;
+ public static final int GIE_ESTIMATE = GIE_PAGE + 0xC;
+ public static final int GIE_RESPONSE = GIE_PAGE + 0xD;
+ public static final int GIE_STATUS = GIE_PAGE + 0xE;
+
+ public static final int CONTACTS_PAGE = CONTACTS << PAGE_SHIFT;
+ public static final int CONTACTS_ANNIVERSARY = CONTACTS_PAGE + 5;
+ public static final int CONTACTS_ASSISTANT_NAME = CONTACTS_PAGE + 6;
+ public static final int CONTACTS_ASSISTANT_TELEPHONE_NUMBER = CONTACTS_PAGE + 7;
+ public static final int CONTACTS_BIRTHDAY = CONTACTS_PAGE + 8;
+ public static final int CONTACTS_BODY = CONTACTS_PAGE + 9;
+ public static final int CONTACTS_BODY_SIZE = CONTACTS_PAGE + 0xA;
+ public static final int CONTACTS_BODY_TRUNCATED = CONTACTS_PAGE + 0xB;
+ public static final int CONTACTS_BUSINESS2_TELEPHONE_NUMBER = CONTACTS_PAGE + 0xC;
+ public static final int CONTACTS_BUSINESS_ADDRESS_CITY = CONTACTS_PAGE + 0xD;
+ public static final int CONTACTS_BUSINESS_ADDRESS_COUNTRY = CONTACTS_PAGE + 0xE;
+ public static final int CONTACTS_BUSINESS_ADDRESS_POSTAL_CODE = CONTACTS_PAGE + 0xF;
+ public static final int CONTACTS_BUSINESS_ADDRESS_STATE = CONTACTS_PAGE + 0x10;
+ public static final int CONTACTS_BUSINESS_ADDRESS_STREET = CONTACTS_PAGE + 0x11;
+ public static final int CONTACTS_BUSINESS_FAX_NUMBER = CONTACTS_PAGE + 0x12;
+ public static final int CONTACTS_BUSINESS_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x13;
+ public static final int CONTACTS_CAR_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x14;
+ public static final int CONTACTS_CATEGORIES = CONTACTS_PAGE + 0x15;
+ public static final int CONTACTS_CATEGORY = CONTACTS_PAGE + 0x16;
+ public static final int CONTACTS_CHILDREN = CONTACTS_PAGE + 0x17;
+ public static final int CONTACTS_CHILD = CONTACTS_PAGE + 0x18;
+ public static final int CONTACTS_COMPANY_NAME = CONTACTS_PAGE + 0x19;
+ public static final int CONTACTS_DEPARTMENT = CONTACTS_PAGE + 0x1A;
+ public static final int CONTACTS_EMAIL1_ADDRESS = CONTACTS_PAGE + 0x1B;
+ public static final int CONTACTS_EMAIL2_ADDRESS = CONTACTS_PAGE + 0x1C;
+ public static final int CONTACTS_EMAIL3_ADDRESS = CONTACTS_PAGE + 0x1D;
+ public static final int CONTACTS_FILE_AS = CONTACTS_PAGE + 0x1E;
+ public static final int CONTACTS_FIRST_NAME = CONTACTS_PAGE + 0x1F;
+ public static final int CONTACTS_HOME2_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x20;
+ public static final int CONTACTS_HOME_ADDRESS_CITY = CONTACTS_PAGE + 0x21;
+ public static final int CONTACTS_HOME_ADDRESS_COUNTRY = CONTACTS_PAGE + 0x22;
+ public static final int CONTACTS_HOME_ADDRESS_POSTAL_CODE = CONTACTS_PAGE + 0x23;
+ public static final int CONTACTS_HOME_ADDRESS_STATE = CONTACTS_PAGE + 0x24;
+ public static final int CONTACTS_HOME_ADDRESS_STREET = CONTACTS_PAGE + 0x25;
+ public static final int CONTACTS_HOME_FAX_NUMBER = CONTACTS_PAGE + 0x26;
+ public static final int CONTACTS_HOME_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x27;
+ public static final int CONTACTS_JOB_TITLE = CONTACTS_PAGE + 0x28;
+ public static final int CONTACTS_LAST_NAME = CONTACTS_PAGE + 0x29;
+ public static final int CONTACTS_MIDDLE_NAME = CONTACTS_PAGE + 0x2A;
+ public static final int CONTACTS_MOBILE_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x2B;
+ public static final int CONTACTS_OFFICE_LOCATION = CONTACTS_PAGE + 0x2C;
+ public static final int CONTACTS_OTHER_ADDRESS_CITY = CONTACTS_PAGE + 0x2D;
+ public static final int CONTACTS_OTHER_ADDRESS_COUNTRY = CONTACTS_PAGE + 0x2E;
+ public static final int CONTACTS_OTHER_ADDRESS_POSTAL_CODE = CONTACTS_PAGE + 0x2F;
+ public static final int CONTACTS_OTHER_ADDRESS_STATE = CONTACTS_PAGE + 0x30;
+ public static final int CONTACTS_OTHER_ADDRESS_STREET = CONTACTS_PAGE + 0x31;
+ public static final int CONTACTS_PAGER_NUMBER = CONTACTS_PAGE + 0x32;
+ public static final int CONTACTS_RADIO_TELEPHONE_NUMBER = CONTACTS_PAGE + 0x33;
+ public static final int CONTACTS_SPOUSE = CONTACTS_PAGE + 0x34;
+ public static final int CONTACTS_SUFFIX = CONTACTS_PAGE + 0x35;
+ public static final int CONTACTS_TITLE = CONTACTS_PAGE + 0x36;
+ public static final int CONTACTS_WEBPAGE = CONTACTS_PAGE + 0x37;
+ public static final int CONTACTS_YOMI_COMPANY_NAME = CONTACTS_PAGE + 0x38;
+ public static final int CONTACTS_YOMI_FIRST_NAME = CONTACTS_PAGE + 0x39;
+ public static final int CONTACTS_YOMI_LAST_NAME = CONTACTS_PAGE + 0x3A;
+ public static final int CONTACTS_COMPRESSED_RTF = CONTACTS_PAGE + 0x3B;
+ public static final int CONTACTS_PICTURE = CONTACTS_PAGE + 0x3C;
+
+ public static final int CALENDAR_PAGE = CALENDAR << PAGE_SHIFT;
+ public static final int CALENDAR_TIME_ZONE = CALENDAR_PAGE + 5;
+ public static final int CALENDAR_ALL_DAY_EVENT = CALENDAR_PAGE + 6;
+ public static final int CALENDAR_ATTENDEES = CALENDAR_PAGE + 7;
+ public static final int CALENDAR_ATTENDEE = CALENDAR_PAGE + 8;
+ public static final int CALENDAR_ATTENDEE_EMAIL = CALENDAR_PAGE + 9;
+ public static final int CALENDAR_ATTENDEE_NAME = CALENDAR_PAGE + 0xA;
+ public static final int CALENDAR_BODY = CALENDAR_PAGE + 0xB;
+ public static final int CALENDAR_BODY_TRUNCATED = CALENDAR_PAGE + 0xC;
+ public static final int CALENDAR_BUSY_STATUS = CALENDAR_PAGE + 0xD;
+ public static final int CALENDAR_CATEGORIES = CALENDAR_PAGE + 0xE;
+ public static final int CALENDAR_CATEGORY = CALENDAR_PAGE + 0xF;
+ public static final int CALENDAR_COMPRESSED_RTF = CALENDAR_PAGE + 0x10;
+ public static final int CALENDAR_DTSTAMP = CALENDAR_PAGE + 0x11;
+ public static final int CALENDAR_END_TIME = CALENDAR_PAGE + 0x12;
+ public static final int CALENDAR_EXCEPTION = CALENDAR_PAGE + 0x13;
+ public static final int CALENDAR_EXCEPTIONS = CALENDAR_PAGE + 0x14;
+ public static final int CALENDAR_EXCEPTION_IS_DELETED = CALENDAR_PAGE + 0x15;
+ public static final int CALENDAR_EXCEPTION_START_TIME = CALENDAR_PAGE + 0x16;
+ public static final int CALENDAR_LOCATION = CALENDAR_PAGE + 0x17;
+ public static final int CALENDAR_MEETING_STATUS = CALENDAR_PAGE + 0x18;
+ public static final int CALENDAR_ORGANIZER_EMAIL = CALENDAR_PAGE + 0x19;
+ public static final int CALENDAR_ORGANIZER_NAME = CALENDAR_PAGE + 0x1A;
+ public static final int CALENDAR_RECURRENCE = CALENDAR_PAGE + 0x1B;
+ public static final int CALENDAR_RECURRENCE_TYPE = CALENDAR_PAGE + 0x1C;
+ public static final int CALENDAR_RECURRENCE_UNTIL = CALENDAR_PAGE + 0x1D;
+ public static final int CALENDAR_RECURRENCE_OCCURRENCES = CALENDAR_PAGE + 0x1E;
+ public static final int CALENDAR_RECURRENCE_INTERVAL = CALENDAR_PAGE + 0x1F;
+ public static final int CALENDAR_RECURRENCE_DAYOFWEEK = CALENDAR_PAGE + 0x20;
+ public static final int CALENDAR_RECURRENCE_DAYOFMONTH = CALENDAR_PAGE + 0x21;
+ public static final int CALENDAR_RECURRENCE_WEEKOFMONTH = CALENDAR_PAGE + 0x22;
+ public static final int CALENDAR_RECURRENCE_MONTHOFYEAR = CALENDAR_PAGE + 0x23;
+ public static final int CALENDAR_REMINDER_MINS_BEFORE = CALENDAR_PAGE + 0x24;
+ public static final int CALENDAR_SENSITIVITY = CALENDAR_PAGE + 0x25;
+ public static final int CALENDAR_SUBJECT = CALENDAR_PAGE + 0x26;
+ public static final int CALENDAR_START_TIME = CALENDAR_PAGE + 0x27;
+ public static final int CALENDAR_UID = CALENDAR_PAGE + 0x28;
+ public static final int CALENDAR_ATTENDEE_STATUS = CALENDAR_PAGE + 0x29;
+ public static final int CALENDAR_ATTENDEE_TYPE = CALENDAR_PAGE + 0x2A;
+
+ public static final int FOLDER_PAGE = FOLDER << PAGE_SHIFT;
+ public static final int FOLDER_FOLDERS = FOLDER_PAGE + 5;
+ public static final int FOLDER_FOLDER = FOLDER_PAGE + 6;
+ public static final int FOLDER_DISPLAY_NAME = FOLDER_PAGE + 7;
+ public static final int FOLDER_SERVER_ID = FOLDER_PAGE + 8;
+ public static final int FOLDER_PARENT_ID = FOLDER_PAGE + 9;
+ public static final int FOLDER_TYPE = FOLDER_PAGE + 0xA;
+ public static final int FOLDER_RESPONSE = FOLDER_PAGE + 0xB;
+ public static final int FOLDER_STATUS = FOLDER_PAGE + 0xC;
+ public static final int FOLDER_CONTENT_CLASS = FOLDER_PAGE + 0xD;
+ public static final int FOLDER_CHANGES = FOLDER_PAGE + 0xE;
+ public static final int FOLDER_ADD = FOLDER_PAGE + 0xF;
+ public static final int FOLDER_DELETE = FOLDER_PAGE + 0x10;
+ public static final int FOLDER_UPDATE = FOLDER_PAGE + 0x11;
+ public static final int FOLDER_SYNC_KEY = FOLDER_PAGE + 0x12;
+ public static final int FOLDER_FOLDER_CREATE = FOLDER_PAGE + 0x13;
+ public static final int FOLDER_FOLDER_DELETE= FOLDER_PAGE + 0x14;
+ public static final int FOLDER_FOLDER_UPDATE = FOLDER_PAGE + 0x15;
+ public static final int FOLDER_FOLDER_SYNC = FOLDER_PAGE + 0x16;
+ public static final int FOLDER_COUNT = FOLDER_PAGE + 0x17;
+ public static final int FOLDER_VERSION = FOLDER_PAGE + 0x18;
+
+ public static final int EMAIL_PAGE = EMAIL << PAGE_SHIFT;
+ public static final int EMAIL_ATTACHMENT = EMAIL_PAGE + 5;
+ public static final int EMAIL_ATTACHMENTS = EMAIL_PAGE + 6;
+ public static final int EMAIL_ATT_NAME = EMAIL_PAGE + 7;
+ public static final int EMAIL_ATT_SIZE = EMAIL_PAGE + 8;
+ public static final int EMAIL_ATT0ID = EMAIL_PAGE + 9;
+ public static final int EMAIL_ATT_METHOD = EMAIL_PAGE + 0xA;
+ public static final int EMAIL_ATT_REMOVED = EMAIL_PAGE + 0xB;
+ public static final int EMAIL_BODY = EMAIL_PAGE + 0xC;
+ public static final int EMAIL_BODY_SIZE = EMAIL_PAGE + 0xD;
+ public static final int EMAIL_BODY_TRUNCATED = EMAIL_PAGE + 0xE;
+ public static final int EMAIL_DATE_RECEIVED = EMAIL_PAGE + 0xF;
+ public static final int EMAIL_DISPLAY_NAME = EMAIL_PAGE + 0x10;
+ public static final int EMAIL_DISPLAY_TO = EMAIL_PAGE + 0x11;
+ public static final int EMAIL_IMPORTANCE = EMAIL_PAGE + 0x12;
+ public static final int EMAIL_MESSAGE_CLASS = EMAIL_PAGE + 0x13;
+ public static final int EMAIL_SUBJECT = EMAIL_PAGE + 0x14;
+ public static final int EMAIL_READ = EMAIL_PAGE + 0x15;
+ public static final int EMAIL_TO = EMAIL_PAGE + 0x16;
+ public static final int EMAIL_CC = EMAIL_PAGE + 0x17;
+ public static final int EMAIL_FROM = EMAIL_PAGE + 0x18;
+ public static final int EMAIL_REPLY_TO = EMAIL_PAGE + 0x19;
+ public static final int EMAIL_ALL_DAY_EVENT = EMAIL_PAGE + 0x1A;
+ public static final int EMAIL_CATEGORIES = EMAIL_PAGE + 0x1B;
+ public static final int EMAIL_CATEGORY = EMAIL_PAGE + 0x1C;
+ public static final int EMAIL_DTSTAMP = EMAIL_PAGE + 0x1D;
+ public static final int EMAIL_END_TIME = EMAIL_PAGE + 0x1E;
+ public static final int EMAIL_INSTANCE_TYPE = EMAIL_PAGE + 0x1F;
+ public static final int EMAIL_INTD_BUSY_STATUS = EMAIL_PAGE + 0x20;
+ public static final int EMAIL_LOCATION = EMAIL_PAGE + 0x21;
+ public static final int EMAIL_MEETING_REQUEST = EMAIL_PAGE + 0x22;
+ public static final int EMAIL_ORGANIZER = EMAIL_PAGE + 0x23;
+ public static final int EMAIL_RECURRENCE_ID = EMAIL_PAGE + 0x24;
+ public static final int EMAIL_REMINDER = EMAIL_PAGE + 0x25;
+ public static final int EMAIL_RESPONSE_REQUESTED = EMAIL_PAGE + 0x26;
+ public static final int EMAIL_RECURRENCES = EMAIL_PAGE + 0x27;
+ public static final int EMAIL_RECURRENCE = EMAIL_PAGE + 0x28;
+ public static final int EMAIL_RECURRENCE_TYPE = EMAIL_PAGE + 0x29;
+ public static final int EMAIL_RECURRENCE_UNTIL = EMAIL_PAGE + 0x2A;
+ public static final int EMAIL_RECURRENCE_OCCURRENCES = EMAIL_PAGE + 0x2B;
+ public static final int EMAIL_RECURRENCE_INTERVAL = EMAIL_PAGE + 0x2C;
+ public static final int EMAIL_RECURRENCE_DAYOFWEEK = EMAIL_PAGE + 0x2D;
+ public static final int EMAIL_RECURRENCE_DAYOFMONTH = EMAIL_PAGE + 0x2E;
+ public static final int EMAIL_RECURRENCE_WEEKOFMONTH = EMAIL_PAGE + 0x2F;
+ public static final int EMAIL_RECURRENCE_MONTHOFYEAR = EMAIL_PAGE + 0x30;
+ public static final int EMAIL_START_TIME = EMAIL_PAGE + 0x31;
+ public static final int EMAIL_SENSITIVITY = EMAIL_PAGE + 0x32;
+ public static final int EMAIL_TIME_ZONE = EMAIL_PAGE + 0x33;
+ public static final int EMAIL_GLOBAL_OBJID = EMAIL_PAGE + 0x34;
+ public static final int EMAIL_THREAD_TOPIC = EMAIL_PAGE + 0x35;
+ public static final int EMAIL_MIME_DATA = EMAIL_PAGE + 0x36;
+ public static final int EMAIL_MIME_TRUNCATED = EMAIL_PAGE + 0x37;
+ public static final int EMAIL_MIME_SIZE = EMAIL_PAGE + 0x38;
+ public static final int EMAIL_INTERNET_CPID = EMAIL_PAGE + 0x39;
+ public static final int EMAIL_FLAG = EMAIL_PAGE + 0x3A;
+ public static final int EMAIL_FLAG_STATUS = EMAIL_PAGE + 0x3B;
+ public static final int EMAIL_CONTENT_CLASS = EMAIL_PAGE + 0x3C;
+ public static final int EMAIL_FLAG_TYPE = EMAIL_PAGE + 0x3D;
+ public static final int EMAIL_COMPLETE_TIME = EMAIL_PAGE + 0x3E;
+
+ public static final int TASK_PAGE = TASK << PAGE_SHIFT;
+ public static final int TASK_BODY = TASK_PAGE + 5;
+ public static final int TASK_BODY_SIZE = TASK_PAGE + 6;
+ public static final int TASK_BODY_TRUNCATED = TASK_PAGE + 7;
+ public static final int TASK_CATEGORIES = TASK_PAGE + 8;
+ public static final int TASK_CATEGORY = TASK_PAGE + 9;
+ public static final int TASK_COMPLETE = TASK_PAGE + 0xA;
+ public static final int TASK_DATE_COMPLETED = TASK_PAGE + 0xB;
+ public static final int TASK_DUE_DATE = TASK_PAGE + 0xC;
+ public static final int TASK_UTC_DUE_DATE = TASK_PAGE + 0xD;
+ public static final int TASK_IMPORTANCE = TASK_PAGE + 0xE;
+ public static final int TASK_RECURRENCE = TASK_PAGE + 0xF;
+ public static final int TASK_RECURRENCE_TYPE = TASK_PAGE + 0x10;
+ public static final int TASK_RECURRENCE_START = TASK_PAGE + 0x11;
+ public static final int TASK_RECURRENCE_UNTIL = TASK_PAGE + 0x12;
+ public static final int TASK_RECURRENCE_OCCURRENCES = TASK_PAGE + 0x13;
+ public static final int TASK_RECURRENCE_INTERVAL = TASK_PAGE + 0x14;
+ public static final int TASK_RECURRENCE_DAY_OF_MONTH = TASK_PAGE + 0x15;
+ public static final int TASK_RECURRENCE_DAY_OF_WEEK = TASK_PAGE + 0x16;
+ public static final int TASK_RECURRENCE_WEEK_OF_MONTH = TASK_PAGE + 0x17;
+ public static final int TASK_RECURRENCE_MONTH_OF_YEAR = TASK_PAGE + 0x18;
+ public static final int TASK_RECURRENCE_REGENERATE = TASK_PAGE + 0x19;
+ public static final int TASK_RECURRENCE_DEAD_OCCUR = TASK_PAGE + 0x1A;
+ public static final int TASK_REMINDER_SET = TASK_PAGE + 0x1B;
+ public static final int TASK_REMINDER_TIME = TASK_PAGE + 0x1C;
+ public static final int TASK_SENSITIVITY = TASK_PAGE + 0x1D;
+ public static final int TASK_START_DATE = TASK_PAGE + 0x1E;
+ public static final int TASK_UTC_START_DATE = TASK_PAGE + 0x1F;
+ public static final int TASK_SUBJECT = TASK_PAGE + 0x20;
+ public static final int COMPRESSED_RTF = TASK_PAGE + 0x21;
+ public static final int ORDINAL_DATE = TASK_PAGE + 0x22;
+ public static final int SUBORDINAL_DATE = TASK_PAGE + 0x23;
+
+ public static final int MOVE_PAGE = MOVE << PAGE_SHIFT;
+ public static final int MOVE_MOVE_ITEMS = MOVE_PAGE + 5;
+ public static final int MOVE_MOVE = MOVE_PAGE + 6;
+ public static final int MOVE_SRCMSGID = MOVE_PAGE + 7;
+ public static final int MOVE_SRCFLDID = MOVE_PAGE + 8;
+ public static final int MOVE_DSTFLDID = MOVE_PAGE + 9;
+ public static final int MOVE_RESPONSE = MOVE_PAGE + 0xA;
+ public static final int MOVE_STATUS = MOVE_PAGE + 0xB;
+ public static final int MOVE_DSTMSGID = MOVE_PAGE + 0xC;
+
+ public static final int CONTACTS2_PAGE = CONTACTS2 << PAGE_SHIFT;
+ public static final int CONTACTS2_CUSTOMER_ID = CONTACTS2_PAGE + 5;
+ public static final int CONTACTS2_GOVERNMENT_ID = CONTACTS2_PAGE + 6;
+ public static final int CONTACTS2_IM_ADDRESS = CONTACTS2_PAGE + 7;
+ public static final int CONTACTS2_IM_ADDRESS_2 = CONTACTS2_PAGE + 8;
+ public static final int CONTACTS2_IM_ADDRESS_3 = CONTACTS2_PAGE + 9;
+ public static final int CONTACTS2_MANAGER_NAME = CONTACTS2_PAGE + 0xA;
+ public static final int CONTACTS2_COMPANY_MAIN_PHONE = CONTACTS2_PAGE + 0xB;
+ public static final int CONTACTS2_ACCOUNT_NAME = CONTACTS2_PAGE + 0xC;
+ public static final int CONTACTS2_NICKNAME = CONTACTS2_PAGE + 0xD;
+ public static final int CONTACTS2_MMS = CONTACTS2_PAGE + 0xE;
+
+ // The Ping constants are used by EasSyncService, and need to be public
+ public static final int PING_PAGE = PING << PAGE_SHIFT;
+ public static final int PING_PING = PING_PAGE + 5;
+ public static final int PING_AUTD_STATE = PING_PAGE + 6;
+ public static final int PING_STATUS = PING_PAGE + 7;
+ public static final int PING_HEARTBEAT_INTERVAL = PING_PAGE + 8;
+ public static final int PING_FOLDERS = PING_PAGE + 9;
+ public static final int PING_FOLDER = PING_PAGE + 0xA;
+ public static final int PING_ID = PING_PAGE + 0xB;
+ public static final int PING_CLASS = PING_PAGE + 0xC;
+ public static final int PING_MAX_FOLDERS = PING_PAGE + 0xD;
+
+ public static final int BASE_PAGE = BASE << PAGE_SHIFT;
+ public static final int BASE_BODY_PREFERENCE = BASE_PAGE + 5;
+ public static final int BASE_TYPE = BASE_PAGE + 6;
+ public static final int BASE_TRUNCATION_SIZE = BASE_PAGE + 7;
+ public static final int BASE_ALL_OR_NONE = BASE_PAGE + 8;
+ public static final int BASE_RESERVED = BASE_PAGE + 9;
+ public static final int BASE_BODY = BASE_PAGE + 0xA;
+ public static final int BASE_DATA = BASE_PAGE + 0xB;
+ public static final int BASE_ESTIMATED_DATA_SIZE = BASE_PAGE + 0xC;
+ public static final int BASE_TRUNCATED = BASE_PAGE + 0xD;
+ public static final int BASE_ATTACHMENTS = BASE_PAGE + 0xE;
+ public static final int BASE_ATTACHMENT = BASE_PAGE + 0xF;
+ public static final int BASE_DISPLAY_NAME = BASE_PAGE + 0x10;
+ public static final int BASE_FILE_REFERENCE = BASE_PAGE + 0x11;
+ public static final int BASE_METHOD = BASE_PAGE + 0x12;
+ public static final int BASE_CONTENT_ID = BASE_PAGE + 0x13;
+ public static final int BASE_CONTENT_LOCATION = BASE_PAGE + 0x14;
+ public static final int BASE_IS_INLINE = BASE_PAGE + 0x15;
+ public static final int BASE_NATIVE_BODY_TYPE = BASE_PAGE + 0x16;
+ public static final int BASE_CONTENT_TYPE = BASE_PAGE + 0x17;
+
+ static public String[][] pages = {
+ { // 0x00 AirSync
+ "Sync", "Responses", "Add", "Change", "Delete", "Fetch", "SyncKey", "ClientId",
+ "ServerId", "Status", "Collection", "Class", "Version", "CollectionId", "GetChanges",
+ "MoreAvailable", "WindowSize", "Commands", "Options", "FilterType", "Truncation",
+ "RTFTruncation", "Conflict", "Collections", "ApplicationData", "DeletesAsMoves",
+ "NotifyGUID", "Supported", "SoftDelete", "MIMESupport", "MIMETruncation", "Wait",
+ "Limit", "Partial"
+ },
+ {
+ // 0x01 Contacts
+ "Anniversary", "AssistantName", "AssistantTelephoneNumber", "Birthday", "ContactsBody",
+ "ContactsBodySize", "ContactsBodyTruncated", "Business2TelephoneNumber",
+ "BusinessAddressCity",
+ "BusinessAddressCountry", "BusinessAddressPostalCode", "BusinessAddressState",
+ "BusinessAddressStreet", "BusinessFaxNumber", "BusinessTelephoneNumber",
+ "CarTelephoneNumber", "ContactsCategories", "ContactsCategory", "Children", "Child",
+ "CompanyName", "Department", "Email1Address", "Email2Address", "Email3Address",
+ "FileAs", "FirstName", "Home2TelephoneNumber", "HomeAddressCity", "HomeAddressCountry",
+ "HomeAddressPostalCode", "HomeAddressState", "HomeAddressStreet", "HomeFaxNumber",
+ "HomeTelephoneNumber", "JobTitle", "LastName", "MiddleName", "MobileTelephoneNumber",
+ "OfficeLocation", "OtherAddressCity", "OtherAddressCountry",
+ "OtherAddressPostalCode", "OtherAddressState", "OtherAddressStreet", "PagerNumber",
+ "RadioTelephoneNumber", "Spouse", "Suffix", "Title", "Webpage", "YomiCompanyName",
+ "YomiFirstName", "YomiLastName", "CompressedRTF", "Picture"
+ },
+ {
+ // 0x02 Email
+ "Attachment", "Attachments", "AttName", "AttSize", "Add0Id", "AttMethod", "AttRemoved",
+ "Body", "BodySize", "BodyTruncated", "DateReceived", "DisplayName", "DisplayTo",
+ "Importance", "MessageClass", "Subject", "Read", "To", "CC", "From", "ReplyTo",
+ "AllDayEvent", "Categories", "Category", "DTStamp", "EndTime", "InstanceType",
+ "IntDBusyStatus", "Location", "MeetingRequest", "Organizer", "RecurrenceId", "Reminder",
+ "ResponseRequested", "Recurrences", "Recurence", "Recurrence_Type", "Recurrence_Until",
+ "Recurrence_Occurrences", "Recurrence_Interval", "Recurrence_DayOfWeek",
+ "Recurrence_DayOfMonth", "Recurrence_WeekOfMonth", "Recurrence_MonthOfYear",
+ "StartTime", "Sensitivity", "TimeZone", "GlobalObjId", "ThreadTopic", "MIMEData",
+ "MIMETruncated", "MIMESize", "InternetCPID", "Flag", "FlagStatus", "EmailContentClass",
+ "FlagType", "CompleteTime"
+ },
+ {
+ // 0x03 AirNotify
+ },
+ {
+ // 0x04 Calendar
+ "CalTimeZone", "CalAllDayEvent", "CalAttendees", "CalAttendee", "CalAttendee_Email",
+ "CalAttendee_Name", "CalBody", "CalBodyTruncated", "CalBusyStatus", "CalCategories",
+ "CalCategory", "CalCompressed_RTF", "CalDTStamp", "CalEndTime", "CalExeption",
+ "CalExceptions", "CalException_IsDeleted", "CalException_StartTime", "CalLocation",
+ "CalMeetingStatus", "CalOrganizer_Email", "CalOrganizer_Name", "CalRecurrence",
+ "CalRecurrence_Type", "CalRecurrence_Until", "CalRecurrence_Occurrences",
+ "CalRecurrence_Interval", "CalRecurrence_DayOfWeek", "CalRecurrence_DayOfMonth",
+ "CalRecurrence_WeekOfMonth", "CalRecurrence_MonthOfYear", "CalReminder_MinsBefore",
+ "CalSensitivity", "CalSubject", "CalStartTime", "CalUID", "CalAttendee_Status",
+ "CalAttendee_Type"
+ },
+ {
+ // 0x05 Move
+ "MoveItems", "Move", "SrcMsgId", "SrcFldId", "DstFldId", "MoveResponse", "MoveStatus",
+ "DstMsgId"
+ },
+ {
+ // 0x06 ItemEstimate
+ "GetItemEstimate", "Version", "Collection", "Collection", "Class", "CollectionId",
+ "DateTime", "Estimate", "Response", "Status"
+ },
+ {
+ // 0x07 FolderHierarchy
+ "Folders", "Folder", "FolderDisplayName", "FolderServerId", "FolderParentId", "Type",
+ "FolderResponse", "FolderStatus", "FolderContentClass", "Changes", "FolderAdd",
+ "FolderDelete", "FolderUpdate", "FolderSyncKey", "FolderFolderCreate",
+ "FolderFolderDelete", "FolderFolderUpdate", "FolderSync", "Count", "FolderVersion"
+ },
+ {
+ // 0x08 MeetingResponse
+ },
+ {
+ // 0x09 Tasks
+ "Body", "BodySize", "BodyTruncated", "Categories", "Category", "Complete",
+ "DateCompleted", "DueDate", "UTCDueDate", "Importance", "Recurrence", "RecurrenceType",
+ "RecurrenceStart", "RecurrenceUntil", "RecurrenceOccurrences", "RecurrenceInterval",
+ "RecurrenceDOM", "RecurrenceDOW", "RecurrenceWOM", "RecurrenceMOY",
+ "RecurrenceRegenerate", "RecurrenceDeadOccur", "ReminderSet", "ReminderTime",
+ "Sensitivity", "StartDate", "UTCStartDate", "Subject", "CompressedRTF", "OrdinalDate",
+ "SubordinalDate"
+ },
+ {
+ // 0x0A ResolveRecipients
+ },
+ {
+ // 0x0B ValidateCert
+ },
+ {
+ // 0x0C Contacts2
+ "CustomerId", "GovernmentId", "IMAddress", "IMAddress2", "IMAddress3", "ManagerName",
+ "CompanyMainPhone", "AccountName", "NickName", "MMS"
+ },
+ {
+ // 0x0D Ping
+ "Ping", "AutdState", "PingStatus", "HeartbeatInterval", "PingFolders", "PingFolder",
+ "PingId", "PingClass", "MaxFolders"
+ },
+ {
+ // 0x0E Provision
+ "Provision", "Policies", "Policy", "PolicyType", "PolicyKey", "Data", "ProvisionStatus",
+ "RemoteWipe", "EASProvidionDoc", "DevicePasswordEnabled",
+ "AlphanumericDevicePasswordRequired",
+ "DeviceEncryptionEnabled", "-unused-", "AttachmentsEnabled", "MinDevicePasswordLength",
+ "MaxInactivityTimeDeviceLock", "MaxDevicePasswordFailedAttempts", "MaxAttachmentSize",
+ "AllowSimpleDevicePassword", "DevicePasswordExpiration", "DevicePasswordHistory",
+ "AllowStorageCard", "AllowCamera", "RequireDeviceEncryption",
+ "AllowUnsignedApplications", "AllowUnsignedInstallationPackages",
+ "MinDevicePasswordComplexCharacters", "AllowWiFi", "AllowTextMessaging",
+ "AllowPOPIMAPEmail", "AllowBluetooth", "AllowIrDA", "RequireManualSyncWhenRoaming",
+ "AllowDesktopSync",
+ "MaxCalendarAgeFilder", "AllowHTMLEmail", "MaxEmailAgeFilder",
+ "MaxEmailBodyTruncationSize", "MaxEmailHTMLBodyTruncationSize",
+ "RequireSignedSMIMEMessages", "RequireEncryptedSMIMEMessages",
+ "RequireSignedSMIMEAlgorithm", "RequireEncryptionSMIMEAlgorithm",
+ "AllowSMIMEEncryptionAlgorithmNegotiation", "AllowSMIMESoftCerts", "AllowBrowser",
+ "AllowConsumerEmail", "AllowRemoteDesktop", "AllowInternetSharing",
+ "UnapprovedInROMApplicationList", "ApplicationName", "ApprovedApplicationList", "Hash"
+ },
+ {
+ // 0x0F Search
+ },
+ {
+ // 0x10 Gal
+ "GalDisplayName", "GalPhone", "GalOffice", "GalTitle", "GalCompany", "GalAlias",
+ "GalFirstName", "GalLastName", "GalHomePhone", "GalMobilePhone", "GalEmailAddress"
+ },
+ {
+ // 0x11 AirSyncBase
+ "BodyPreference", "BodyPreferenceType", "BodyPreferenceTruncationSize", "AllOrNone",
+ "--unused--", "BaseBody", "BaseData", "BaseEstimatedDataSize", "BaseTruncated",
+ "BaseAttachments", "BaseAttachment", "BaseDisplayName", "FileReference", "BaseMethod",
+ "BaseContentId", "BaseContentLocation", "BaseIsInline", "BaseNativeBodyType",
+ "BaseContentType"
+ },
+ {
+ // 0x12 Settings
+ },
+ {
+ // 0x13 DocumentLibrary
+ },
+ {
+ // 0x14 ItemOperations
+ }
+ };
+}
diff --git a/src/com/android/exchange/adapter/patent_disclaimer.txt b/src/com/android/exchange/adapter/patent_disclaimer.txt
new file mode 100644
index 0000000..8a715a3
--- /dev/null
+++ b/src/com/android/exchange/adapter/patent_disclaimer.txt
@@ -0,0 +1,9 @@
+-----------------------------
+THIS IS NOT A GRANT OF PATENT RIGHTS.
+
+Google makes no representation or warranty that the source code made available hereunder is
+unencumbered by third-party patents. Those intending to use this source code in hardware or
+software products are advised that implementations of this code, including in open source software
+or shareware, may require patent licenses from the relevant patent holders.
+
+-----------------------------
diff --git a/src/com/android/exchange/patent_disclaimer.txt b/src/com/android/exchange/patent_disclaimer.txt
new file mode 100644
index 0000000..8a715a3
--- /dev/null
+++ b/src/com/android/exchange/patent_disclaimer.txt
@@ -0,0 +1,9 @@
+-----------------------------
+THIS IS NOT A GRANT OF PATENT RIGHTS.
+
+Google makes no representation or warranty that the source code made available hereunder is
+unencumbered by third-party patents. Those intending to use this source code in hardware or
+software products are advised that implementations of this code, including in open source software
+or shareware, may require patent licenses from the relevant patent holders.
+
+-----------------------------
diff --git a/src/com/android/exchange/utility/FileLogger.java b/src/com/android/exchange/utility/FileLogger.java
new file mode 100644
index 0000000..c5f4635
--- /dev/null
+++ b/src/com/android/exchange/utility/FileLogger.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2009 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.android.exchange.utility;
+
+import android.content.Context;
+
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Date;
+
+public class FileLogger {
+ private static FileLogger LOGGER = null;
+ private static FileWriter mLogWriter = null;
+ public static String LOG_FILE_NAME = "/sdcard/emaillog.txt";
+
+ public synchronized static FileLogger getLogger (Context c) {
+ LOGGER = new FileLogger();
+ return LOGGER;
+ }
+
+ private FileLogger() {
+ try {
+ mLogWriter = new FileWriter(LOG_FILE_NAME, true);
+ } catch (IOException e) {
+ // Doesn't matter
+ }
+ }
+
+ static public synchronized void close() {
+ if (mLogWriter != null) {
+ try {
+ mLogWriter.close();
+ } catch (IOException e) {
+ // Doesn't matter
+ }
+ mLogWriter = null;
+ }
+ }
+
+ static public synchronized void log(Exception e) {
+ if (mLogWriter != null) {
+ log("Exception", "Stack trace follows...");
+ PrintWriter pw = new PrintWriter(mLogWriter);
+ e.printStackTrace(pw);
+ pw.flush();
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ static public synchronized void log(String prefix, String str) {
+ if (LOGGER == null) {
+ LOGGER = new FileLogger();
+ log("Logger", "\r\n\r\n --- New Log ---");
+ }
+ Date d = new Date();
+ int hr = d.getHours();
+ int min = d.getMinutes();
+ int sec = d.getSeconds();
+
+ // I don't use DateFormat here because (in my experience), it's much slower
+ StringBuffer sb = new StringBuffer(256);
+ sb.append('[');
+ sb.append(hr);
+ sb.append(':');
+ if (min < 10)
+ sb.append('0');
+ sb.append(min);
+ sb.append(':');
+ if (sec < 10) {
+ sb.append('0');
+ }
+ sb.append(sec);
+ sb.append("] ");
+ if (prefix != null) {
+ sb.append(prefix);
+ sb.append("| ");
+ }
+ sb.append(str);
+ sb.append("\r\n");
+ String s = sb.toString();
+
+ if (mLogWriter != null) {
+ try {
+ mLogWriter.write(s);
+ mLogWriter.flush();
+ } catch (IOException e) {
+ // Doesn't matter
+ }
+ }
+ }
+}
diff --git a/src/com/android/exchange/utility/patent_disclaimer.txt b/src/com/android/exchange/utility/patent_disclaimer.txt
new file mode 100644
index 0000000..8a715a3
--- /dev/null
+++ b/src/com/android/exchange/utility/patent_disclaimer.txt
@@ -0,0 +1,9 @@
+-----------------------------
+THIS IS NOT A GRANT OF PATENT RIGHTS.
+
+Google makes no representation or warranty that the source code made available hereunder is
+unencumbered by third-party patents. Those intending to use this source code in hardware or
+software products are advised that implementations of this code, including in open source software
+or shareware, may require patent licenses from the relevant patent holders.
+
+-----------------------------
diff --git a/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java b/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java
new file mode 100644
index 0000000..a3e7dae
--- /dev/null
+++ b/tests/src/com/android/exchange/EasEmailSyncAdapterTests.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2009 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.android.exchange;
+
+import com.android.email.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.exchange.adapter.EmailSyncAdapter;
+import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser;
+
+import android.test.AndroidTestCase;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+public class EasEmailSyncAdapterTests extends AndroidTestCase {
+
+ /**
+ * Create and return a short, simple InputStream that has at least four bytes, which is all
+ * that's required to initialize an EasParser (the parent class of EasEmailSyncParser)
+ * @return the InputStream
+ */
+ public InputStream getTestInputStream() {
+ return new ByteArrayInputStream(new byte[] {0, 0, 0, 0, 0});
+ }
+
+ EasSyncService getTestService() {
+ Account account = new Account();
+ account.mId = -1;
+ Mailbox mailbox = new Mailbox();
+ mailbox.mId = -1;
+ EasSyncService service = new EasSyncService();
+ service.mContext = getContext();
+ service.mMailbox = mailbox;
+ service.mAccount = account;
+ return service;
+ }
+
+ EmailSyncAdapter getTestSyncAdapter() {
+ EasSyncService service = getTestService();
+ EmailSyncAdapter adapter = new EmailSyncAdapter(service.mMailbox, service);
+ return adapter;
+ }
+
+ /**
+ * Check functionality for getting mime type from a file name (using its extension)
+ * The default for all unknown files is application/octet-stream
+ */
+ public void testGetMimeTypeFromFileName() throws IOException {
+ EasSyncService service = getTestService();
+ EmailSyncAdapter adapter = new EmailSyncAdapter(service.mMailbox, service);
+ EasEmailSyncParser p = adapter.new EasEmailSyncParser(getTestInputStream(), adapter);
+ // Test a few known types
+ String mimeType = p.getMimeTypeFromFileName("foo.jpg");
+ assertEquals("image/jpeg", mimeType);
+ // Make sure this is case insensitive
+ mimeType = p.getMimeTypeFromFileName("foo.JPG");
+ assertEquals("image/jpeg", mimeType);
+ mimeType = p.getMimeTypeFromFileName("this_is_a_weird_filename.gif");
+ assertEquals("image/gif", mimeType);
+ // Test an illegal file name ending with the extension prefix
+ mimeType = p.getMimeTypeFromFileName("foo.");
+ assertEquals("application/octet-stream", mimeType);
+ // Test a really awful name
+ mimeType = p.getMimeTypeFromFileName(".....");
+ assertEquals("application/octet-stream", mimeType);
+ // Test a bare file name (no extension)
+ mimeType = p.getMimeTypeFromFileName("foo");
+ assertEquals("application/octet-stream", mimeType);
+ // And no name at all (null isn't a valid input)
+ mimeType = p.getMimeTypeFromFileName("");
+ assertEquals("application/octet-stream", mimeType);
+ }
+
+ public void testFormatDateTime() throws IOException {
+ EmailSyncAdapter adapter = getTestSyncAdapter();
+ GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
+ // Calendar is odd, months are zero based, so the first 11 below is December...
+ calendar.set(2008, 11, 11, 18, 19, 20);
+ String date = adapter.formatDateTime(calendar);
+ assertEquals("2008-12-11T18:19:20.000Z", date);
+ calendar.clear();
+ calendar.set(2012, 0, 2, 23, 0, 1);
+ date = adapter.formatDateTime(calendar);
+ assertEquals("2012-01-02T23:00:01.000Z", date);
+ }
+}
diff --git a/tests/src/com/android/exchange/EasSyncServiceTests.java b/tests/src/com/android/exchange/EasSyncServiceTests.java
new file mode 100644
index 0000000..45f4882
--- /dev/null
+++ b/tests/src/com/android/exchange/EasSyncServiceTests.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2009 Marc Blank
+ * Licensed to 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.android.exchange;
+
+import android.content.Context;
+import android.test.AndroidTestCase;
+
+import java.io.File;
+import java.io.IOException;
+
+public class EasSyncServiceTests extends AndroidTestCase {
+ Context mMockContext;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mMockContext = getContext();
+ }
+
+ /**
+ * Test that our unique file name algorithm works as expected.
+ * @throws IOException
+ */
+ public void testCreateUniqueFile() throws IOException {
+ // Delete existing files, if they exist
+ EasSyncService svc = new EasSyncService();
+ svc.mContext = mMockContext;
+ try {
+ String fileName = "A11achm3n1.doc";
+ File uniqueFile = svc.createUniqueFileInternal(null, fileName);
+ assertEquals(fileName, uniqueFile.getName());
+ if (uniqueFile.createNewFile()) {
+ uniqueFile = svc.createUniqueFileInternal(null, fileName);
+ assertEquals("A11achm3n1-2.doc", uniqueFile.getName());
+ if (uniqueFile.createNewFile()) {
+ uniqueFile = svc.createUniqueFileInternal(null, fileName);
+ assertEquals("A11achm3n1-3.doc", uniqueFile.getName());
+ }
+ }
+ fileName = "A11achm3n1";
+ uniqueFile = svc.createUniqueFileInternal(null, fileName);
+ assertEquals(fileName, uniqueFile.getName());
+ if (uniqueFile.createNewFile()) {
+ uniqueFile = svc.createUniqueFileInternal(null, fileName);
+ assertEquals("A11achm3n1-2", uniqueFile.getName());
+ }
+ } finally {
+ // These are the files that should be created earlier in the test. Make sure
+ // they are deleted for the next go-around
+ File directory = getContext().getFilesDir();
+ String[] fileNames = new String[] {"A11achm3n1.doc", "A11achm3n1-2.doc", "A11achm3n1"};
+ int length = fileNames.length;
+ for (int i = 0; i < length; i++) {
+ File file = new File(directory, fileNames[i]);
+ if (file.exists()) {
+ file.delete();
+ }
+ }
+ }
+ }
+
+
+}
diff --git a/tests/src/com/android/exchange/TagsTests.java b/tests/src/com/android/exchange/TagsTests.java
new file mode 100644
index 0000000..1a87c5c
--- /dev/null
+++ b/tests/src/com/android/exchange/TagsTests.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2009 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.android.exchange;
+
+import com.android.exchange.adapter.Tags;
+
+import android.test.AndroidTestCase;
+
+import java.util.HashMap;
+
+public class TagsTests extends AndroidTestCase {
+
+ // Make sure there are no duplicates in the tags table
+ // This test is no longer required - tags can be duplicated
+ public void disable_testNoDuplicates() {
+ String[][] allTags = Tags.pages;
+ HashMap<String, Boolean> map = new HashMap<String, Boolean>();
+ for (String[] page: allTags) {
+ for (String tag: page) {
+ assertTrue(tag, !map.containsKey(tag));
+ map.put(tag, true);
+ }
+ }
+ }
+}