am eaf951c5: am 75753107: reconcile main tree with open-source eclair
Merge commit 'eaf951c59631829c84ac71b413cf7b2ff186be17'
* commit 'eaf951c59631829c84ac71b413cf7b2ff186be17':
android-2.1_r1 snapshot
diff --git a/Android.mk b/Android.mk
index 277d0e5..f1e3f85 100644
--- a/Android.mk
+++ b/Android.mk
@@ -15,10 +15,14 @@
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
+LOCAL_MODULE_TAGS := optional
+
LOCAL_SRC_FILES := $(call all-java-files-under, src)
+# EXCHANGE-REMOVE-SECTION-START
LOCAL_SRC_FILES += \
- src/com/android/exchange/IEmailService.aidl \
- src/com/android/exchange/IEmailServiceCallback.aidl
+ src/com/android/email/service/IEmailService.aidl \
+ src/com/android/email/service/IEmailServiceCallback.aidl
+# EXCHANGE-REMOVE-SECTION-END
LOCAL_PACKAGE_NAME := Email
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 9b376af..8eaa175 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -31,6 +31,8 @@
<!-- For EAS purposes; could be removed when EAS has a permanent home -->
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
+ <uses-permission android:name="android.permission.WRITE_CALENDAR"/>
+ <uses-permission android:name="android.permission.READ_CALENDAR"/>
<!-- Only required if a store implements push mail and needs to keep network open -->
<uses-permission android:name="android.permission.WAKE_LOCK"/>
@@ -83,11 +85,13 @@
android:label="@string/account_setup_outgoing_title"
>
</activity>
+ <!--EXCHANGE-REMOVE-SECTION-START-->
<activity
android:name=".activity.setup.AccountSetupExchange"
android:label="@string/account_setup_exchange_title"
>
</activity>
+ <!--EXCHANGE-REMOVE-SECTION-END-->
<activity
android:name=".activity.setup.AccountSetupOptions"
android:label="@string/account_setup_options_title"
@@ -111,6 +115,11 @@
android:label="@string/account_settings_action"
>
</activity>
+ <activity
+ android:name=".activity.setup.AccountSecurity"
+ android.label="@string/account_security_title"
+ >
+ </activity>
<activity
android:name=".activity.Debug"
@@ -171,6 +180,7 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
+ <!--EXCHANGE-REMOVE-SECTION-START-->
<receiver android:name="com.android.exchange.EmailSyncAlarmReceiver"/>
<receiver android:name="com.android.exchange.MailboxAlarmReceiver"/>
<receiver android:name="com.android.exchange.BootReceiver" android:enabled="true">
@@ -178,6 +188,7 @@
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
+ <!--EXCHANGE-REMOVE-SECTION-END-->
<receiver android:name=".service.BootReceiver" android:enabled="true">
<intent-filter>
@@ -191,12 +202,36 @@
</intent-filter>
</receiver>
+ <!-- Support for DeviceAdmin / DevicePolicyManager. See SecurityPolicy class for impl. -->
+ <receiver
+ android:name=".SecurityPolicy$PolicyAdmin"
+ android:label="@string/device_admin_label"
+ android:description="@string/device_admin_description"
+ android:permission="android.permission.BIND_DEVICE_ADMIN" >
+ <meta-data
+ android:name="android.app.device_admin"
+ android:resource="@xml/device_admin" />
+ <intent-filter>
+ <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+ </intent-filter>
+ </receiver>
+
+ <receiver
+ android:name=".OneTimeInitializer"
+ android:enabled="true"
+ >
+ <intent-filter>
+ <action android:name="android.intent.action.BOOT_COMPLETED" />
+ </intent-filter>
+ </receiver>
+
<service
android:name=".service.MailService"
android:enabled="false"
>
</service>
+ <!--EXCHANGE-REMOVE-SECTION-START-->
<!--Required stanza to register the ContactsSyncAdapterService with SyncManager -->
<service
android:name="com.android.exchange.ContactsSyncAdapterService"
@@ -208,6 +243,17 @@
android:resource="@xml/syncadapter_contacts" />
</service>
+ <!--Required stanza to register the CalendarSyncAdapterService with SyncManager -->
+ <service
+ android:name="com.android.exchange.CalendarSyncAdapterService"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.content.SyncAdapter" />
+ </intent-filter>
+ <meta-data android:name="android.content.SyncAdapter"
+ android:resource="@xml/syncadapter_calendar" />
+ </service>
+
<!-- Add android:process=":remote" below to enable SyncManager as a separate process -->
<service
android:name="com.android.exchange.SyncManager"
@@ -216,13 +262,37 @@
</service>
<!--Required stanza to register the EasAuthenticatorService with AccountManager -->
- <service android:name=".service.EasAuthenticatorService" android:exported="true">
+ <service
+ android:name=".service.EasAuthenticatorService"
+ android:exported="true"
+ android:enabled="true"
+ >
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
- <meta-data android:name="android.accounts.AccountAuthenticator"
- android:resource="@xml/authenticator" />
+ <meta-data
+ android:name="android.accounts.AccountAuthenticator"
+ android:resource="@xml/authenticator"
+ />
</service>
+ <!--
+ EasAuthenticatorService with the altenative label. Disabled by default,
+ and OneTimeInitializer enables it if the vendor policy tells so.
+ -->
+ <service
+ android:name=".service.EasAuthenticatorServiceAlternate"
+ android:exported="true"
+ android:enabled="false"
+ >
+ <intent-filter>
+ <action android:name="android.accounts.AccountAuthenticator" />
+ </intent-filter>
+ <meta-data
+ android:name="android.accounts.AccountAuthenticator"
+ android:resource="@xml/authenticator_alternate"
+ />
+ </service>
+ <!--EXCHANGE-REMOVE-SECTION-END-->
<provider
android:name=".provider.AttachmentProvider"
diff --git a/res/drawable-hdpi/divider_vertical_light_opaque.9.png b/res/drawable-hdpi/divider_vertical_light_opaque.9.png
new file mode 100755
index 0000000..8f35315
--- /dev/null
+++ b/res/drawable-hdpi/divider_vertical_light_opaque.9.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
deleted file mode 100644
index 4d71060..0000000
--- a/res/drawable-hdpi/expander_ic_folder_maximized.9.png
+++ /dev/null
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
deleted file mode 100644
index 0c04943..0000000
--- a/res/drawable-hdpi/expander_ic_folder_minimized.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/divider_vertical_light_opaque.9.png b/res/drawable-mdpi/divider_vertical_light_opaque.9.png
new file mode 100755
index 0000000..8f35315
--- /dev/null
+++ b/res/drawable-mdpi/divider_vertical_light_opaque.9.png
Binary files differ
diff --git a/res/drawable-mdpi/expander_ic_folder_maximized.9.png b/res/drawable-mdpi/expander_ic_folder_maximized.9.png
deleted file mode 100644
index c7ff2ac..0000000
--- a/res/drawable-mdpi/expander_ic_folder_maximized.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/expander_ic_folder_minimized.9.png b/res/drawable-mdpi/expander_ic_folder_minimized.9.png
deleted file mode 100644
index 5b5fdb0..0000000
--- a/res/drawable-mdpi/expander_ic_folder_minimized.9.png
+++ /dev/null
Binary files differ
diff --git a/res/drawable-mdpi/icon.png b/res/drawable-mdpi/icon.png
index 590ed70..e874589 100644
--- a/res/drawable-mdpi/icon.png
+++ b/res/drawable-mdpi/icon.png
Binary files differ
diff --git a/res/drawable/expander_ic_folder.xml b/res/drawable/expander_ic_folder.xml
deleted file mode 100644
index d82c00a..0000000
--- a/res/drawable/expander_ic_folder.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2008 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.
--->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:state_expanded="true"
- android:drawable="@drawable/expander_ic_folder_maximized" />
- <item android:drawable="@drawable/expander_ic_folder_minimized" />
-</selector>
diff --git a/res/drawable/folder_message_list_child_background.xml b/res/drawable/folder_message_list_child_background.xml
deleted file mode 100644
index a39fb29..0000000
--- a/res/drawable/folder_message_list_child_background.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2008 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.
--->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:state_window_focused="false" android:state_selected="true"
- android:drawable="@color/folder_message_list_child_background" />
- <item android:state_selected="true"
- android:drawable="@android:color/transparent" />
- <item android:state_pressed="true" android:state_selected="false"
- android:drawable="@android:color/transparent" />
- <item android:state_selected="false"
- android:drawable="@color/folder_message_list_child_background" />
-</selector>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index d018b12..807bb6f 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -19,25 +19,7 @@
<!-- 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>
+ <string name="account_setup_failed_security_policies_required"></string>
<!-- Permissions label for reading attachments -->
<string name="read_attachment_label">Read Email attachments</string>
@@ -199,14 +181,14 @@
<!-- Version number, shown only on debug screen -->
<string name="debug_version_fmt">Version: <xliff:g id="version">%s</xliff:g></string>
- <!-- Checkbox label, shown only on debug screen -->
- <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>
+ <!-- Do Not Translate. Checkbox label, shown only on debug screen -->
+ <string name="debug_enable_debug_logging_label" translatable="false">Enable extra debug logging?</string>
+ <!-- Do Not Translate. Checkbox label, shown only on debug screen -->
+ <string name="debug_enable_sensitive_logging_label" translatable="false">Enable sensitive information debug logging? (May show passwords in logs.)</string>
+ <!-- Do Not Translate. Checkbox label, shown only on debug screen -->
+ <string name="debug_enable_exchange_logging_label" translatable="false">Enable exchange parser logging? (Extremely verbose)</string>
+ <!-- Do Not Translate. Checkbox label, shown only on debug screen -->
+ <string name="debug_enable_exchange_file_logging_label" translatable="false">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>
@@ -295,12 +277,21 @@
<!-- Title of screen when setting up new email account -->
<string name="account_setup_basics_title">Set up email</string>
+ <!-- Title of the screen when adding exchange account -->
+ <string name="account_setup_basics_exchange_title">
+ Add an Exchange account</string>
+ <!-- Title of the screen when adding exchange account -->
+ <string name="account_setup_basics_exchange_title_alternate">
+ Add an Exchange ActiveSync account</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, enthusiastic welcome message (in EAS mode). -->
+ <string name="accounts_welcome_exchange_alternate">
+ You can configure an Exchange ActiveSync account in just a few steps.</string>
<!-- On "Set up email" screen, brief instructions -->
<!-- DEPRECATED - REMOVE -->
<string name="account_setup_basics_instructions"></string>
@@ -356,6 +347,10 @@
<!-- Do Not Translate. "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</string>
+ <!-- Do Not Translate. "Add new email account" screen, button name in
+ response to what type of account this is -->
+ <string name="account_setup_account_type_exchange_action_alternate">
+ Microsoft Exchange ActiveSync</string>
<!-- "Incoming server settings" screen, label for text field -->
<string name="account_setup_incoming_title">Incoming server settings</string>
@@ -447,7 +442,11 @@
<!-- 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>
+ <string name="account_setup_options_sync_contacts_label">Sync contacts from this account.
+ </string>
+ <!-- In Account setup options screen, optional check box to also sync contacts -->
+ <string name="account_setup_options_sync_calendar_label">Sync calendar 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 -->
@@ -488,8 +487,33 @@
<string name="account_setup_failed_security">Unable to open connection to server due to security error.</string>
<!-- Additional diagnostic text when server connection failed due to io error (connection) -->
<string name="account_setup_failed_ioerror">Unable to open connection to server.</string>
- <!-- Additional diagnostic text when validation failed due to required provisioning not being supported -->
- <string name="account_setup_failed_security_policies_required">This Exchange ActiveSync server requires security features your phone does not support.</string>
+
+ <!-- Dialog title when validation requires security provisioning (e.g. support
+ for device lock PIN, or remote wipe.) and we ask the user permission before continuing -->
+ <string name="account_setup_security_required_title">Remote security administration</string>
+ <!-- Additional diagnostic text when validation requires security provisioning (e.g. support
+ for device lock PIN, or remote wipe.) and we ask the user permission before continuing. -->
+ <string name="account_setup_security_policies_required_fmt">
+ The server <xliff:g id="server">%s</xliff:g> requires that you allow it to remotely control
+ some security features of your phone. Do you wish to finish setting up this account?
+ </string>
+ <!-- Additional diagnostic text when validation failed due to required provisioning not
+ being supported -->
+ <string name="account_setup_failed_security_policies_unsupported">
+ This server requires security features your phone does not support.</string>
+
+ <!-- Notification ticker when device security required -->
+ <string name="security_notification_ticker_fmt">
+ Account \"<xliff:g id="account">%s</xliff:g>\" requires security settings update.
+ </string>
+ <!-- Notification content title when device security required -->
+ <string name="security_notification_content_title">Update Security Settings</string>
+ <!-- Title of the activity that dispatches changes to device security. Not normally seen. -->
+ <string name="account_security_title">Device Security</string>
+ <!-- Additional diagnostic text when the email app asserts control of the phone. -->
+ <string name="account_security_policy_explanation_fmt">
+ The server <xliff:g id="server">%s</xliff:g> requires that you allow it to remotely control
+ some security features of your phone.</string>
<!-- "Setup could not finish" dialog action button -->
<string name="account_setup_failed_dlg_edit_details_action">Edit details</string>
@@ -518,13 +542,23 @@
<string name="account_settings_description_label">Account name</string>
<!-- On Settings screen, setting option name -->
<string name="account_settings_name_label">Your name</string>
+ <!-- On Settings screen, setting option name -->
+ <string name="account_settings_signature_label">Signature</string>
+ <!-- On Settings screen, setting option name -->
+ <string name="account_settings_signature_hint">Append text to messages you send</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>
+ <string name="account_settings_sync_contacts_summary">Also sync contacts from this account
+ </string>
+ <!-- On settings screen, sync calendar check box label -->
+ <string name="account_settings_sync_calendar_enable">Sync calendar</string>
+ <!-- On settings screen, sync calendar summary text -->
+ <string name="account_settings_sync_calendar_summary">Also sync calendar from this account
+ </string>
<!-- On Settings screen, setting check box label -->
<string name="account_settings_vibrate_enable">Vibrate</string>
@@ -557,7 +591,16 @@
<!-- Name of Microsoft Exchange account type; used by AccountManager -->
<string name="exchange_name">Corporate</string>
+ <!-- Name of Microsoft Exchange account type; used by AccountManager -->
+ <string name="exchange_name_alternate">Microsoft Exchange ActiveSync</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>
+
+ <!-- Strings that support the DeviceAdmin / DevicePolicyManager API -->
+ <!-- Name of the DeviceAdmin (seen in settings & in user confirmation screen) -->
+ <string name="device_admin_label">Email</string>
+ <!-- Long-form description of the DeviceAdmin (2nd line in settings & in user conf. screen) -->
+ <string name="device_admin_description">Enables server-specified security policies</string>
+
</resources>
diff --git a/res/xml/syncadapter_calendar.xml b/res/xml/syncadapter_calendar.xml
new file mode 100644
index 0000000..a6c0fc6
--- /dev/null
+++ b/res/xml/syncadapter_calendar.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2010, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<!-- The attributes in this XML file provide configuration information -->
+<!-- for the SyncAdapter. -->
+
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+ android:contentAuthority="com.android.calendar"
+ android:accountType="com.android.exchange"
+/>
diff --git a/src/com/android/exchange/AbstractSyncService.java b/src/com/android/exchange/AbstractSyncService.java
index 425424d..08583ec 100644
--- a/src/com/android/exchange/AbstractSyncService.java
+++ b/src/com/android/exchange/AbstractSyncService.java
@@ -76,8 +76,8 @@
protected Object mSynchronizer = new Object();
protected volatile long mRequestTime = 0;
- protected ArrayList<PartRequest> mPartRequests = new ArrayList<PartRequest>();
- protected PartRequest mPendingPartRequest = null;
+ protected ArrayList<Request> mRequests = new ArrayList<Request>();
+ protected PartRequest mPendingRequest = null;
/**
* Sent by SyncManager to request that the service stop itself cleanly
@@ -282,54 +282,23 @@
}
/**
- * PartRequest handling (common functionality)
- * Can be overridden if desired, but IMAP/EAS both use the next three methods as-is
+ * Request handling (common functionality)
+ * Can be overridden if desired
*/
- public void addPartRequest(PartRequest req) {
- synchronized (mPartRequests) {
- mPartRequests.add(req);
+ public void addRequest(Request req) {
+ synchronized (mRequests) {
+ mRequests.add(req);
mRequestTime = System.currentTimeMillis();
}
}
- public void removePartRequest(PartRequest req) {
- synchronized (mPartRequests) {
- mPartRequests.remove(req);
+ public void removeRequest(Request req) {
+ synchronized (mRequests) {
+ mRequests.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
diff --git a/src/com/android/exchange/BootReceiver.java b/src/com/android/exchange/BootReceiver.java
index 5312bfb..1ebfa7b 100644
--- a/src/com/android/exchange/BootReceiver.java
+++ b/src/com/android/exchange/BootReceiver.java
@@ -16,6 +16,8 @@
package com.android.exchange;
+import com.android.email.ExchangeUtils;
+
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -25,6 +27,6 @@
@Override
public void onReceive(Context context, Intent intent) {
Log.d("Exchange", "BootReceiver onReceive");
- context.startService(new Intent(context, SyncManager.class));
+ ExchangeUtils.startExchangeService(context);
}
}
diff --git a/src/com/android/exchange/CalendarSyncAdapterService.java b/src/com/android/exchange/CalendarSyncAdapterService.java
new file mode 100644
index 0000000..d2f4ecf
--- /dev/null
+++ b/src/com/android/exchange/CalendarSyncAdapterService.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.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.os.Bundle;
+import android.os.IBinder;
+import android.provider.Calendar.Events;
+import android.util.Log;
+
+public class CalendarSyncAdapterService extends Service {
+ private static final String TAG = "EAS CalendarSyncAdapterService";
+ private static SyncAdapterImpl sSyncAdapter = null;
+ private static final Object sSyncAdapterLock = new Object();
+
+ private static final String ACCOUNT_AND_TYPE_CALENDAR =
+ MailboxColumns.ACCOUNT_KEY + "=? AND " + MailboxColumns.TYPE + '=' + Mailbox.TYPE_CALENDAR;
+
+ public CalendarSyncAdapterService() {
+ 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 {
+ CalendarSyncAdapterService.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 calendar
+ * 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();
+ boolean logging = Eas.USER_LOG;
+ if (logging) {
+ Log.d(TAG, "performSync");
+ }
+ if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD)) {
+ Cursor c = cr.query(Events.CONTENT_URI,
+ new String[] {Events._ID}, Events._SYNC_DIRTY + "=1", null, null);
+ try {
+ if (!c.moveToFirst()) {
+ if (logging) {
+ Log.d(TAG, "Upload sync; no changes");
+ }
+ return;
+ }
+ } finally {
+ c.close();
+ }
+ }
+
+ // Find the (EmailProvider) account associated with this email address
+ Cursor accountCursor =
+ cr.query(EmailContent.Account.CONTENT_URI,
+ EmailContent.ID_PROJECTION, AccountColumns.EMAIL_ADDRESS + "=?",
+ new String[] {account.name}, null);
+ try {
+ if (accountCursor.moveToFirst()) {
+ long accountId = accountCursor.getLong(0);
+ // Now, find the calendar mailbox associated with the account
+ Cursor mailboxCursor = cr.query(Mailbox.CONTENT_URI, EmailContent.ID_PROJECTION,
+ ACCOUNT_AND_TYPE_CALENDAR, new String[] {Long.toString(accountId)}, null);
+ try {
+ if (mailboxCursor.moveToFirst()) {
+ if (logging) {
+ Log.d(TAG, "Calendar 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/ContactsSyncAdapterService.java b/src/com/android/exchange/ContactsSyncAdapterService.java
index 69fe792..b39d789 100644
--- a/src/com/android/exchange/ContactsSyncAdapterService.java
+++ b/src/com/android/exchange/ContactsSyncAdapterService.java
@@ -16,6 +16,7 @@
package com.android.exchange;
+import com.android.email.Email;
import com.android.email.provider.EmailContent;
import com.android.email.provider.EmailContent.AccountColumns;
import com.android.email.provider.EmailContent.Mailbox;
@@ -98,7 +99,7 @@
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)
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE)
.build();
Cursor c = cr.query(uri,
new String[] {RawContacts._ID}, RawContacts.DIRTY + "=1", null, null);
diff --git a/src/com/android/exchange/Eas.java b/src/com/android/exchange/Eas.java
index c005544..86db9ab 100644
--- a/src/com/android/exchange/Eas.java
+++ b/src/com/android/exchange/Eas.java
@@ -38,7 +38,6 @@
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
diff --git a/src/com/android/exchange/EasAuthenticationException.java b/src/com/android/exchange/EasAuthenticationException.java
new file mode 100644
index 0000000..f5b14b9
--- /dev/null
+++ b/src/com/android/exchange/EasAuthenticationException.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange;
+
+import java.io.IOException;
+
+/**
+ * Use this to be able to distinguish login (authentication) failures from other I/O
+ * exceptions during a sync, as they are handled very differently.
+ */
+public class EasAuthenticationException extends IOException {
+ private static final long serialVersionUID = 1L;
+
+ EasAuthenticationException() {
+ super();
+ }
+}
diff --git a/src/com/android/exchange/EasOutboxService.java b/src/com/android/exchange/EasOutboxService.java
index 38c9603..d827d51 100644
--- a/src/com/android/exchange/EasOutboxService.java
+++ b/src/com/android/exchange/EasOutboxService.java
@@ -26,6 +26,7 @@
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.EmailServiceStatus;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
index bba2ddf..5aa9fd9 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -17,6 +17,8 @@
package com.android.exchange;
+import com.android.email.SecurityPolicy;
+import com.android.email.SecurityPolicy.PolicySet;
import com.android.email.codec.binary.Base64;
import com.android.email.mail.AuthenticationFailedException;
import com.android.email.mail.MessagingException;
@@ -28,12 +30,17 @@
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.service.EmailServiceProxy;
+import com.android.email.service.EmailServiceStatus;
import com.android.exchange.adapter.AbstractSyncAdapter;
import com.android.exchange.adapter.AccountSyncAdapter;
+import com.android.exchange.adapter.CalendarSyncAdapter;
import com.android.exchange.adapter.ContactsSyncAdapter;
import com.android.exchange.adapter.EmailSyncAdapter;
import com.android.exchange.adapter.FolderSyncParser;
+import com.android.exchange.adapter.MeetingResponseParser;
import com.android.exchange.adapter.PingParser;
+import com.android.exchange.adapter.ProvisionParser;
import com.android.exchange.adapter.Serializer;
import com.android.exchange.adapter.Tags;
import com.android.exchange.adapter.Parser.EasParserException;
@@ -42,25 +49,35 @@
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
+import org.apache.http.client.ClientProtocolException;
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.entity.StringEntity;
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 org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
+import android.os.Bundle;
import android.os.RemoteException;
import android.os.SystemClock;
+import android.util.Log;
+import android.util.Xml;
+import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -94,6 +111,11 @@
// Define our default protocol version as 2.5 (Exchange 2003)
static private final String DEFAULT_PROTOCOL_VERSION = "2.5";
+ static private final String AUTO_DISCOVER_SCHEMA_PREFIX =
+ "http://schemas.microsoft.com/exchange/autodiscover/mobilesync/";
+ static private final String AUTO_DISCOVER_PAGE = "/autodiscover/autodiscover.xml";
+ static private final int AUTO_DISCOVER_REDIRECT_CODE = 451;
+
/**
* 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
@@ -121,6 +143,9 @@
static private final int PING_FALLBACK_INBOX = 5;
static private final int PING_FALLBACK_PIM = 25;
+ // MSFT's custom HTTP result code indicating the need to provision
+ static private final int HTTP_NEED_PROVISIONING = 449;
+
// Reasonable default
public String mProtocolVersion = DEFAULT_PROTOCOL_VERSION;
public Double mProtocolVersionDouble;
@@ -144,12 +169,22 @@
// Whether we've ever lowered the heartbeat
private boolean mPingHeartbeatDropped = false;
// Whether a POST was aborted due to watchdog timeout
- private boolean mAborted = false;
+ private boolean mPostAborted = false;
+ // Whether or not the sync service is valid (usable)
+ public boolean mIsValid = true;
public EasSyncService(Context _context, Mailbox _mailbox) {
super(_context, _mailbox);
mContentResolver = _context.getContentResolver();
+ if (mAccount == null) {
+ mIsValid = false;
+ return;
+ }
HostAuth ha = HostAuth.restoreHostAuthWithId(_context, mAccount.mHostAuthKeyRecv);
+ if (ha == null) {
+ mIsValid = false;
+ return;
+ }
mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0;
mTrustSsl = (ha.mFlags & HostAuth.FLAG_TRUST_ALL_CERTIFICATES) != 0;
}
@@ -168,7 +203,7 @@
synchronized(getSynchronizer()) {
if (mPendingPost != null) {
userLog("Aborting pending POST!");
- mAborted = true;
+ mPostAborted = true;
mPendingPost.abort();
}
}
@@ -190,7 +225,20 @@
* @return whether or not the code represents an authentication error
*/
protected boolean isAuthError(int code) {
- return ((code == HttpStatus.SC_UNAUTHORIZED) || (code == HttpStatus.SC_FORBIDDEN));
+ return (code == HttpStatus.SC_UNAUTHORIZED) || (code == HttpStatus.SC_FORBIDDEN);
+ }
+
+ private void setupProtocolVersion(EasSyncService service, Header versionHeader) {
+ String versions = versionHeader.getValue();
+ if (versions != null) {
+ if (versions.contains("12.0")) {
+ service.mProtocolVersion = "12.0";
+ }
+ service.mProtocolVersionDouble = Double.parseDouble(service.mProtocolVersion);
+ if (service.mAccount != null) {
+ service.mAccount.mProtocolVersion = service.mProtocolVersion;
+ }
+ }
}
@Override
@@ -205,7 +253,9 @@
svc.mPassword = password;
svc.mSsl = ssl;
svc.mTrustSsl = trustCertificates;
- svc.mDeviceId = SyncManager.getDeviceId();
+ // We mustn't use the "real" device id or we'll screw up current accounts
+ // Any string will do, but we'll go for "validate"
+ svc.mDeviceId = "validate";
HttpResponse resp = svc.sendHttpClientOptions();
int code = resp.getStatusLine().getStatusCode();
userLog("Validation (OPTIONS) response: " + code);
@@ -218,6 +268,9 @@
throw new MessagingException(MessagingException.IOERROR);
}
+ // Make sure we've got the right protocol version set up
+ setupProtocolVersion(svc, versions);
+
// Run second test here for provisioning failures...
Serializer s = new Serializer();
userLog("Try folder sync");
@@ -225,8 +278,16 @@
.end().end().done();
resp = svc.sendHttpClientPost("FolderSync", s.toByteArray());
code = resp.getStatusLine().getStatusCode();
- if (code == HttpStatus.SC_FORBIDDEN) {
- throw new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED);
+ // We'll get one of the following responses if policies are required by the server
+ if (code == HttpStatus.SC_FORBIDDEN || code == HTTP_NEED_PROVISIONING) {
+ // Get the policies and see if we are able to support them
+ if (svc.canProvision()) {
+ // If so, send the advisory Exception (the account may be created later)
+ throw new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED);
+ } else
+ // If not, send the unsupported Exception (the account won't be created)
+ throw new MessagingException(
+ MessagingException.SECURITY_POLICIES_UNSUPPORTED);
}
userLog("Validation successful");
return;
@@ -251,6 +312,302 @@
}
+ /**
+ * Gets the redirect location from the HTTP headers and uses that to modify the HttpPost so that
+ * it can be reused
+ *
+ * @param resp the HttpResponse that indicates a redirect (451)
+ * @param post the HttpPost that was originally sent to the server
+ * @return the HttpPost, updated with the redirect location
+ */
+ private HttpPost getRedirect(HttpResponse resp, HttpPost post) {
+ Header locHeader = resp.getFirstHeader("X-MS-Location");
+ if (locHeader != null) {
+ String loc = locHeader.getValue();
+ // If we've gotten one and it shows signs of looking like an address, we try
+ // sending our request there
+ if (loc != null && loc.startsWith("http")) {
+ post.setURI(URI.create(loc));
+ return post;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Send the POST command to the autodiscover server, handling a redirect, if necessary, and
+ * return the HttpResponse
+ *
+ * @param client the HttpClient to be used for the request
+ * @param post the HttpPost we're going to send
+ * @return an HttpResponse from the original or redirect server
+ * @throws IOException on any IOException within the HttpClient code
+ * @throws MessagingException
+ */
+ private HttpResponse postAutodiscover(HttpClient client, HttpPost post)
+ throws IOException, MessagingException {
+ userLog("Posting autodiscover to: " + post.getURI());
+ HttpResponse resp = client.execute(post);
+ int code = resp.getStatusLine().getStatusCode();
+ // On a redirect, try the new location
+ if (code == AUTO_DISCOVER_REDIRECT_CODE) {
+ post = getRedirect(resp, post);
+ if (post != null) {
+ userLog("Posting autodiscover to redirect: " + post.getURI());
+ return client.execute(post);
+ }
+ } else if (code == HttpStatus.SC_UNAUTHORIZED) {
+ // 401 (Unauthorized) is for true auth errors when used in Autodiscover
+ // 403 (and others) we'll just punt on
+ throw new MessagingException(MessagingException.AUTHENTICATION_FAILED);
+ } else if (code != HttpStatus.SC_OK) {
+ // We'll try the next address if this doesn't work
+ userLog("Code: " + code + ", throwing IOException");
+ throw new IOException();
+ }
+ return resp;
+ }
+
+ /**
+ * Use the Exchange 2007 AutoDiscover feature to try to retrieve server information using
+ * only an email address and the password
+ *
+ * @param userName the user's email address
+ * @param password the user's password
+ * @return a HostAuth ready to be saved in an Account or null (failure)
+ */
+ public Bundle tryAutodiscover(String userName, String password) throws RemoteException {
+ XmlSerializer s = Xml.newSerializer();
+ ByteArrayOutputStream os = new ByteArrayOutputStream(1024);
+ HostAuth hostAuth = new HostAuth();
+ Bundle bundle = new Bundle();
+ bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
+ MessagingException.NO_ERROR);
+ try {
+ // Build the XML document that's sent to the autodiscover server(s)
+ s.setOutput(os, "UTF-8");
+ s.startDocument("UTF-8", false);
+ s.startTag(null, "Autodiscover");
+ s.attribute(null, "xmlns", AUTO_DISCOVER_SCHEMA_PREFIX + "requestschema/2006");
+ s.startTag(null, "Request");
+ s.startTag(null, "EMailAddress").text(userName).endTag(null, "EMailAddress");
+ s.startTag(null, "AcceptableResponseSchema");
+ s.text(AUTO_DISCOVER_SCHEMA_PREFIX + "responseschema/2006");
+ s.endTag(null, "AcceptableResponseSchema");
+ s.endTag(null, "Request");
+ s.endTag(null, "Autodiscover");
+ s.endDocument();
+ String req = os.toString();
+
+ // Initialize the user name and password
+ mUserName = userName;
+ mPassword = password;
+ // Make sure the authentication string is created (mAuthString)
+ makeUriString("foo", null);
+
+ // Split out the domain name
+ int amp = userName.indexOf('@');
+ // The UI ensures that userName is a valid email address
+ if (amp < 0) {
+ throw new RemoteException();
+ }
+ String domain = userName.substring(amp + 1);
+
+ // There are up to four attempts here; the two URLs that we're supposed to try per the
+ // specification, and up to one redirect for each (handled in postAutodiscover)
+
+ // Try the domain first and see if we can get a response
+ HttpPost post = new HttpPost("https://" + domain + AUTO_DISCOVER_PAGE);
+ setHeaders(post);
+ post.setHeader("Content-Type", "text/xml");
+ post.setEntity(new StringEntity(req));
+ HttpClient client = getHttpClient(COMMAND_TIMEOUT);
+ HttpResponse resp;
+ try {
+ resp = postAutodiscover(client, post);
+ } catch (ClientProtocolException e1) {
+ return null;
+ } catch (IOException e1) {
+ // We catch the IOException here because we have an alternate address to try
+ post.setURI(URI.create("https://autodiscover." + domain + AUTO_DISCOVER_PAGE));
+ // If we fail here, we're out of options, so we let the outer try catch the
+ // IOException and return null
+ resp = postAutodiscover(client, post);
+ }
+
+ // Get the "final" code; if it's not 200, just return null
+ int code = resp.getStatusLine().getStatusCode();
+ userLog("Code: " + code);
+ if (code != HttpStatus.SC_OK) return null;
+
+ // At this point, we have a 200 response (SC_OK)
+ HttpEntity e = resp.getEntity();
+ InputStream is = e.getContent();
+ try {
+ // The response to Autodiscover is regular XML (not WBXML)
+ // If we ever get an error in this process, we'll just punt and return null
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ XmlPullParser parser = factory.newPullParser();
+ parser.setInput(is, "UTF-8");
+ int type = parser.getEventType();
+ if (type == XmlPullParser.START_DOCUMENT) {
+ type = parser.next();
+ if (type == XmlPullParser.START_TAG) {
+ String name = parser.getName();
+ if (name.equals("Autodiscover")) {
+ hostAuth = new HostAuth();
+ parseAutodiscover(parser, hostAuth);
+ // On success, we'll have a server address and login
+ if (hostAuth.mAddress != null && hostAuth.mLogin != null) {
+ // Fill in the rest of the HostAuth
+ hostAuth.mPassword = password;
+ hostAuth.mPort = 443;
+ hostAuth.mProtocol = "eas";
+ hostAuth.mFlags =
+ HostAuth.FLAG_SSL | HostAuth.FLAG_AUTHENTICATE;
+ bundle.putParcelable(
+ EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH, hostAuth);
+ } else {
+ bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
+ MessagingException.UNSPECIFIED_EXCEPTION);
+ }
+ }
+ }
+ }
+ } catch (XmlPullParserException e1) {
+ // This would indicate an I/O error of some sort
+ // We will simply return null and user can configure manually
+ }
+ // There's no reason at all for exceptions to be thrown, and it's ok if so.
+ // We just won't do auto-discover; user can configure manually
+ } catch (IllegalArgumentException e) {
+ bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
+ MessagingException.UNSPECIFIED_EXCEPTION);
+ } catch (IllegalStateException e) {
+ bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
+ MessagingException.UNSPECIFIED_EXCEPTION);
+ } catch (IOException e) {
+ userLog("IOException in Autodiscover", e);
+ bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
+ MessagingException.IOERROR);
+ } catch (MessagingException e) {
+ bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
+ MessagingException.AUTHENTICATION_FAILED);
+ }
+ return bundle;
+ }
+
+ void parseServer(XmlPullParser parser, HostAuth hostAuth)
+ throws XmlPullParserException, IOException {
+ boolean mobileSync = false;
+ while (true) {
+ int type = parser.next();
+ if (type == XmlPullParser.END_TAG && parser.getName().equals("Server")) {
+ break;
+ } else if (type == XmlPullParser.START_TAG) {
+ String name = parser.getName();
+ if (name.equals("Type")) {
+ if (parser.nextText().equals("MobileSync")) {
+ mobileSync = true;
+ }
+ } else if (mobileSync && name.equals("Url")) {
+ String url = parser.nextText().toLowerCase();
+ // This will look like https://<server address>/Microsoft-Server-ActiveSync
+ // We need to extract the <server address>
+ if (url.startsWith("https://") &&
+ url.endsWith("/microsoft-server-activesync")) {
+ int lastSlash = url.lastIndexOf('/');
+ hostAuth.mAddress = url.substring(8, lastSlash);
+ userLog("Autodiscover, server: " + hostAuth.mAddress);
+ }
+ }
+ }
+ }
+ }
+
+ void parseSettings(XmlPullParser parser, HostAuth hostAuth)
+ throws XmlPullParserException, IOException {
+ while (true) {
+ int type = parser.next();
+ if (type == XmlPullParser.END_TAG && parser.getName().equals("Settings")) {
+ break;
+ } else if (type == XmlPullParser.START_TAG) {
+ String name = parser.getName();
+ if (name.equals("Server")) {
+ parseServer(parser, hostAuth);
+ }
+ }
+ }
+ }
+
+ void parseAction(XmlPullParser parser, HostAuth hostAuth)
+ throws XmlPullParserException, IOException {
+ while (true) {
+ int type = parser.next();
+ if (type == XmlPullParser.END_TAG && parser.getName().equals("Action")) {
+ break;
+ } else if (type == XmlPullParser.START_TAG) {
+ String name = parser.getName();
+ if (name.equals("Error")) {
+ // Should parse the error
+ } else if (name.equals("Redirect")) {
+ Log.d(TAG, "Redirect: " + parser.nextText());
+ } else if (name.equals("Settings")) {
+ parseSettings(parser, hostAuth);
+ }
+ }
+ }
+ }
+
+ void parseUser(XmlPullParser parser, HostAuth hostAuth)
+ throws XmlPullParserException, IOException {
+ while (true) {
+ int type = parser.next();
+ if (type == XmlPullParser.END_TAG && parser.getName().equals("User")) {
+ break;
+ } else if (type == XmlPullParser.START_TAG) {
+ String name = parser.getName();
+ if (name.equals("EMailAddress")) {
+ String addr = parser.nextText();
+ hostAuth.mLogin = addr;
+ userLog("Autodiscover, login: " + addr);
+ } else if (name.equals("DisplayName")) {
+ String dn = parser.nextText();
+ userLog("Autodiscover, user: " + dn);
+ }
+ }
+ }
+ }
+
+ void parseResponse(XmlPullParser parser, HostAuth hostAuth)
+ throws XmlPullParserException, IOException {
+ while (true) {
+ int type = parser.next();
+ if (type == XmlPullParser.END_TAG && parser.getName().equals("Response")) {
+ break;
+ } else if (type == XmlPullParser.START_TAG) {
+ String name = parser.getName();
+ if (name.equals("User")) {
+ parseUser(parser, hostAuth);
+ } else if (name.equals("Action")) {
+ parseAction(parser, hostAuth);
+ }
+ }
+ }
+ }
+
+ void parseAutodiscover(XmlPullParser parser, HostAuth hostAuth)
+ throws XmlPullParserException, IOException {
+ while (true) {
+ int type = parser.nextTag();
+ if (type == XmlPullParser.END_TAG && parser.getName().equals("Autodiscover")) {
+ break;
+ } else if (type == XmlPullParser.START_TAG && parser.getName().equals("Response")) {
+ parseResponse(parser, hostAuth);
+ }
+ }
+ }
+
private void doStatusCallback(long messageId, long attachmentId, int status) {
try {
SyncManager.callback().loadAttachmentStatus(messageId, attachmentId, status, 0);
@@ -306,7 +663,7 @@
* @throws IOException
*/
protected void getAttachment(PartRequest req) throws IOException {
- Attachment att = req.att;
+ Attachment att = req.mAttachment;
Message msg = Message.restoreMessageWithId(mContext, att.mMessageKey);
doProgressCallback(msg.mId, att.mId, 0);
@@ -318,9 +675,9 @@
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);
+ File f = (req.mDestination != null)
+ ? new File(req.mDestination)
+ : createUniqueFileInternal(req.mDestination, att.mFileName);
if (f != null) {
// Ensure that the target directory exists
File destDir = f.getParentFile();
@@ -332,7 +689,7 @@
// len < 0 means "chunked" transfer-encoding
if (len != 0) {
try {
- mPendingPartRequest = req;
+ mPendingRequest = req;
byte[] bytes = new byte[CHUNK_SIZE];
int length = len;
// Loop terminates 1) when EOF is reached or 2) if an IOException occurs
@@ -362,12 +719,12 @@
errorLog("totalRead is greater than attachment length?");
break;
}
- int pct = (totalRead * 100 / length);
+ int pct = (totalRead * 100) / length;
doProgressCallback(msg.mId, att.mId, pct);
}
}
} finally {
- mPendingPartRequest = null;
+ mPendingRequest = null;
}
}
os.flush();
@@ -375,8 +732,8 @@
// EmailProvider will throw an exception if we try to update an unsaved attachment
if (att.isSaved()) {
- String contentUriString = (req.contentUriString != null)
- ? req.contentUriString
+ String contentUriString = (req.mContentUriString != null)
+ ? req.mContentUriString
: "file://" + f.getAbsolutePath();
ContentValues cv = new ContentValues();
cv.put(AttachmentColumns.CONTENT_URI, contentUriString);
@@ -389,6 +746,37 @@
}
}
+ /**
+ * Responds to a meeting request. The MeetingResponseRequest is basically our
+ * wrapper for the meetingResponse service call
+ * @param req the request (message id and response code)
+ * @throws IOException
+ */
+ protected void sendMeetingResponse(MeetingResponseRequest req) throws IOException {
+ Message msg = Message.restoreMessageWithId(mContext, req.mMessageId);
+ Serializer s = new Serializer();
+ s.start(Tags.MREQ_MEETING_RESPONSE).start(Tags.MREQ_REQUEST);
+ s.data(Tags.MREQ_USER_RESPONSE, Integer.toString(req.mResponse));
+ s.data(Tags.MREQ_COLLECTION_ID, Long.toString(msg.mMailboxKey));
+ s.data(Tags.MREQ_REQ_ID, msg.mServerId);
+ s.end().end().done();
+ HttpResponse res = sendHttpClientPost("MeetingResponse", s.toByteArray());
+ int status = res.getStatusLine().getStatusCode();
+ if (status == HttpStatus.SC_OK) {
+ HttpEntity e = res.getEntity();
+ int len = (int)e.getContentLength();
+ InputStream is = res.getEntity().getContent();
+ if (len != 0) {
+ new MeetingResponseParser(is, this).parse();
+ }
+ } else if (isAuthError(status)) {
+ throw new EasAuthenticationException();
+ } else {
+ userLog("Meeting response request failed, code: " + status);
+ throw new IOException();
+ }
+ }
+
@SuppressWarnings("deprecation")
private String makeUriString(String cmd, String extra) throws IOException {
// Cache the authentication string and the command string
@@ -511,6 +899,29 @@
}
}
+ // TODO This is Exchange 2007 only at this point
+ private boolean canProvision() throws IOException {
+ Serializer s = new Serializer();
+ s.start(Tags.PROVISION_PROVISION).start(Tags.PROVISION_POLICIES);
+ s.start(Tags.PROVISION_POLICY).data(Tags.PROVISION_POLICY_TYPE, "MS-EAS-Provisioning-WBXML")
+ .end().end().end().done();
+ HttpResponse resp = sendHttpClientPost("Provision", s.toByteArray());
+ int code = resp.getStatusLine().getStatusCode();
+ if (code == HttpStatus.SC_OK) {
+ InputStream is = resp.getEntity().getContent();
+ ProvisionParser pp = new ProvisionParser(is, this);
+ if (pp.parse()) {
+ // If true, we received policies from the server
+ // Retrieve them and write them to the framework
+ PolicySet ps = pp.getPolicySet();
+ if (SecurityPolicy.getInstance(mContext).isSupported(ps)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
/**
* Performs FolderSync
*
@@ -518,8 +929,6 @@
* @throws EasParserException
*/
public void runAccountMailbox() throws IOException, EasParserException {
- // We'll reuse this ContentValues object
- ContentValues cv = new ContentValues();
// Initialize exit status to success
mExitStatus = EmailServiceStatus.SUCCESS;
try {
@@ -533,7 +942,7 @@
if (mAccount.mSyncKey == null) {
mAccount.mSyncKey = "0";
userLog("Account syncKey INIT to 0");
- cv.clear();
+ ContentValues cv = new ContentValues();
cv.put(AccountColumns.SYNC_KEY, mAccount.mSyncKey);
mAccount.update(mContext, cv);
}
@@ -544,7 +953,7 @@
}
// When we first start up, change all mailboxes to push.
- cv.clear();
+ 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,
@@ -597,25 +1006,40 @@
}
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)) {
+ 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;
+ }
+ }
+ // // EVERYTHING IN THE code==403 BLOCK IS PLACEHOLDER/SAMPLE CODE
+ } else if (code == 403) { // security error
+ // Reality: Find out from server what policies are required
+ // Fake: Hardcode the policies
+ SecurityPolicy sp = SecurityPolicy.getInstance(mContext);
+ PolicySet ps = new PolicySet(4, PolicySet.PASSWORD_MODE_SIMPLE, 0, 0, true);
+ // Update the account
+ if (ps.writeAccount(mAccount, "securitySyncKey", true, mContext)) {
+ sp.updatePolicies(mAccount.mId);
+ }
+ // Notify that we are blocked because of policies
+ sp.policiesRequired(mAccount.mId);
+ // and exit (don't sync in this condition)
+ mExitStatus = EXIT_LOGIN_FAILURE;
+ // END PLACEHOLDER CODE
+ } else if (isAuthError(code)) {
mExitStatus = EXIT_LOGIN_FAILURE;
} else {
userLog("FolderSync response error: ", code);
@@ -713,7 +1137,6 @@
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
@@ -825,7 +1248,7 @@
// 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"))) {
+ if (mPostAborted || (hasMessage && message.contains("reset by peer"))) {
long pingLength = SystemClock.elapsedRealtime() - pingTime;
if ((pingHeartbeat > PING_MIN_HEARTBEAT) &&
(pingHeartbeat > mPingHighWaterMark)) {
@@ -835,7 +1258,7 @@
pingHeartbeat = PING_MIN_HEARTBEAT;
}
userLog("Decreased ping heartbeat to ", pingHeartbeat, "s");
- } else if (mAborted || (pingLength < 2000)) {
+ } else if (mPostAborted || (pingLength < 2000)) {
userLog("Abort or NAT type return < 2 seconds; throwing IOException");
throw e;
} else {
@@ -955,7 +1378,7 @@
return pp.getSyncStatus();
}
- private String getFilterType() {
+ private String getEmailFilter() {
String filter = Eas.FILTER_1_WEEK;
switch (mAccount.mSyncLookback) {
case com.android.email.Account.SYNC_WINDOW_1_DAY: {
@@ -1004,18 +1427,29 @@
return;
}
+ // Now, handle various requests
while (true) {
- PartRequest req = null;
- synchronized (mPartRequests) {
- if (mPartRequests.isEmpty()) {
+ Request req = null;
+ synchronized (mRequests) {
+ if (mRequests.isEmpty()) {
break;
} else {
- req = mPartRequests.get(0);
+ req = mRequests.get(0);
}
}
- getAttachment(req);
- synchronized(mPartRequests) {
- mPartRequests.remove(req);
+
+ // Our two request types are PartRequest (loading attachment) and
+ // MeetingResponseRequest (respond to a meeting request)
+ if (req instanceof PartRequest) {
+ getAttachment((PartRequest)req);
+ } else if (req instanceof MeetingResponseRequest) {
+ sendMeetingResponse((MeetingResponseRequest)req);
+ }
+
+ // If there's an exception handling the request, we'll throw it
+ // Otherwise, we remove the request
+ synchronized(mRequests) {
+ mRequests.remove(req);
}
}
@@ -1041,8 +1475,11 @@
// 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());
+ if (className.equals("Email")) {
+ s.data(Tags.SYNC_FILTER_TYPE, getEmailFilter());
+ } else if (className.equals("Calendar")) {
+ // TODO Force one month for calendar until we can set this!
+ s.data(Tags.SYNC_FILTER_TYPE, Eas.FILTER_1_MONTH);
}
// Set the truncation amount for all classes
if (mProtocolVersionDouble >= 12.0) {
@@ -1095,6 +1532,7 @@
TAG = mThread.getName();
HostAuth ha = HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv);
+ if (ha == null) return false;
mHostAddress = ha.mAddress;
mUserName = ha.mLogin;
mPassword = ha.mPassword;
@@ -1132,6 +1570,8 @@
AbstractSyncAdapter target;
if (mMailbox.mType == Mailbox.TYPE_CONTACTS) {
target = new ContactsSyncAdapter(mMailbox, this);
+ } else if (mMailbox.mType == Mailbox.TYPE_CALENDAR) {
+ target = new CalendarSyncAdapter(mMailbox, this);
} else {
target = new EmailSyncAdapter(mMailbox, this);
}
@@ -1145,26 +1585,33 @@
sync(target);
} while (mRequestTime != 0);
}
+ } catch (EasAuthenticationException e) {
+ userLog("Caught authentication error");
+ mExitStatus = EXIT_LOGIN_FAILURE;
} catch (IOException e) {
String message = e.getMessage();
- userLog("Caught IOException: ", ((message == null) ? "No message" : message));
+ userLog("Caught IOException: ", (message == null) ? "No message" : message);
mExitStatus = EXIT_IO_ERROR;
} catch (Exception e) {
userLog("Uncaught exception in EasSyncService", e);
} finally {
+ int status;
+
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;
+ 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);
break;
case EXIT_LOGIN_FAILURE:
status = EmailServiceStatus.LOGIN_FAILED;
@@ -1174,24 +1621,15 @@
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.");
+ status = EmailServiceStatus.SUCCESS;
+ }
+
+ try {
+ SyncManager.callback().syncMailboxStatus(mMailboxId, status, 0);
+ } catch (RemoteException e1) {
+ // Don't care if this fails
}
// Make sure SyncManager knows about this
diff --git a/src/com/android/exchange/EmailServiceStatus.java b/src/com/android/exchange/EmailServiceStatus.java
deleted file mode 100644
index 697548c..0000000
--- a/src/com/android/exchange/EmailServiceStatus.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * 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
index 4a44860..ea36781 100644
--- a/src/com/android/exchange/EmailSyncAlarmReceiver.java
+++ b/src/com/android/exchange/EmailSyncAlarmReceiver.java
@@ -50,8 +50,16 @@
private static String TAG = "EmailSyncAlarm";
@Override
- public void onReceive(Context context, Intent intent) {
+ public void onReceive(final Context context, Intent intent) {
Log.v(TAG, "onReceive");
+ new Thread(new Runnable() {
+ public void run() {
+ handleReceive(context);
+ }
+ }).start();
+ }
+
+ private void handleReceive(Context context) {
ArrayList<Long> mailboxesToNotify = new ArrayList<Long>();
ContentResolver cr = context.getContentResolver();
int messageCount = 0;
diff --git a/src/com/android/exchange/IEmailService.aidl b/src/com/android/exchange/IEmailService.aidl
deleted file mode 100644
index ec0fd5e..0000000
--- a/src/com/android/exchange/IEmailService.aidl
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * 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
deleted file mode 100644
index a789c4a..0000000
--- a/src/com/android/exchange/IEmailServiceCallback.aidl
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * 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/EmailContent.aidl b/src/com/android/exchange/MeetingResponseRequest.java
similarity index 62%
copy from src/com/android/exchange/EmailContent.aidl
copy to src/com/android/exchange/MeetingResponseRequest.java
index c6b4a7d..1d69a6c 100644
--- a/src/com/android/exchange/EmailContent.aidl
+++ b/src/com/android/exchange/MeetingResponseRequest.java
@@ -1,6 +1,5 @@
/*
- * Copyright (C) 2008-2009 Marc Blank
- * Licensed to The Android Open Source Project.
+ * Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,5 +16,14 @@
package com.android.exchange;
-parcelable EmailContent.Attachment;
+/**
+ * MeetingResponseRequest is the EAS wrapper for responding to meeting requests.
+ */
+public class MeetingResponseRequest extends Request {
+ public int mResponse;
+ MeetingResponseRequest(long messageId, int response) {
+ mMessageId = messageId;
+ mResponse = response;
+ }
+}
diff --git a/src/com/android/exchange/PartRequest.java b/src/com/android/exchange/PartRequest.java
index 72b79f4..b6b281d 100644
--- a/src/com/android/exchange/PartRequest.java
+++ b/src/com/android/exchange/PartRequest.java
@@ -24,24 +24,21 @@
* 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 class PartRequest extends Request {
+ public Attachment mAttachment;
+ public String mDestination;
+ public String mContentUriString;
+ public String mLocation;
public PartRequest(Attachment _att) {
- timeStamp = System.currentTimeMillis();
- emailId = _att.mMessageKey;
- att = _att;
- loc = att.mLocation;
+ mMessageId = _att.mMessageKey;
+ mAttachment = _att;
+ mLocation = mAttachment.mLocation;
}
public PartRequest(Attachment _att, String _destination, String _contentUriString) {
this(_att);
- destination = _destination;
- contentUriString = _contentUriString;
+ mDestination = _destination;
+ mContentUriString = _contentUriString;
}
}
diff --git a/src/com/android/exchange/EmailContent.aidl b/src/com/android/exchange/Request.java
similarity index 60%
copy from src/com/android/exchange/EmailContent.aidl
copy to src/com/android/exchange/Request.java
index c6b4a7d..185dd7f 100644
--- a/src/com/android/exchange/EmailContent.aidl
+++ b/src/com/android/exchange/Request.java
@@ -1,6 +1,5 @@
/*
- * Copyright (C) 2008-2009 Marc Blank
- * Licensed to The Android Open Source Project.
+ * Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,5 +16,12 @@
package com.android.exchange;
-parcelable EmailContent.Attachment;
-
+/**
+ * Requests for mailbox actions are handled by subclasses of this abstract class.
+ * Two subclasses are now defined: PartRequest (attachment load) and MeetingResponseRequest
+ * (respond to a meeting invitation)
+ */
+public abstract class Request {
+ public long mTimeStamp = System.currentTimeMillis();
+ public long mMessageId;
+}
diff --git a/src/com/android/exchange/SSLSocketFactory.java b/src/com/android/exchange/SSLSocketFactory.java
new file mode 100644
index 0000000..56af829
--- /dev/null
+++ b/src/com/android/exchange/SSLSocketFactory.java
@@ -0,0 +1,401 @@
+/*
+ * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/conn/ssl/SSLSocketFactory.java $
+ * $Revision: 659194 $
+ * $Date: 2008-05-22 11:33:47 -0700 (Thu, 22 May 2008) $
+ *
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ * This class was copied from org.apache.http.conn.ssl, because it didn't have a suitable
+ * constructor.
+ */
+
+package com.android.exchange;
+
+import org.apache.http.conn.scheme.HostNameResolver;
+import org.apache.http.conn.scheme.LayeredSocketFactory;
+import org.apache.http.conn.ssl.AllowAllHostnameVerifier;
+import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier;
+import org.apache.http.conn.ssl.StrictHostnameVerifier;
+import org.apache.http.conn.ssl.X509HostnameVerifier;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.UnrecoverableKeyException;
+
+/**
+ * Layered socket factory for TLS/SSL connections, based on JSSE.
+ *.
+ * <p>
+ * SSLSocketFactory can be used to validate the identity of the HTTPS
+ * server against a list of trusted certificates and to authenticate to
+ * the HTTPS server using a private key.
+ * </p>
+ *
+ * <p>
+ * SSLSocketFactory will enable server authentication when supplied with
+ * a {@link KeyStore truststore} file containg one or several trusted
+ * certificates. The client secure socket will reject the connection during
+ * the SSL session handshake if the target HTTPS server attempts to
+ * authenticate itself with a non-trusted certificate.
+ * </p>
+ *
+ * <p>
+ * Use JDK keytool utility to import a trusted certificate and generate a truststore file:
+ * <pre>
+ * keytool -import -alias "my server cert" -file server.crt -keystore my.truststore
+ * </pre>
+ * </p>
+ *
+ * <p>
+ * SSLSocketFactory will enable client authentication when supplied with
+ * a {@link KeyStore keystore} file containg a private key/public certificate
+ * pair. The client secure socket will use the private key to authenticate
+ * itself to the target HTTPS server during the SSL session handshake if
+ * requested to do so by the server.
+ * The target HTTPS server will in its turn verify the certificate presented
+ * by the client in order to establish client's authenticity
+ * </p>
+ *
+ * <p>
+ * Use the following sequence of actions to generate a keystore file
+ * </p>
+ * <ul>
+ * <li>
+ * <p>
+ * Use JDK keytool utility to generate a new key
+ * <pre>keytool -genkey -v -alias "my client key" -validity 365 -keystore my.keystore</pre>
+ * For simplicity use the same password for the key as that of the keystore
+ * </p>
+ * </li>
+ * <li>
+ * <p>
+ * Issue a certificate signing request (CSR)
+ * <pre>keytool -certreq -alias "my client key" -file mycertreq.csr -keystore my.keystore</pre>
+ * </p>
+ * </li>
+ * <li>
+ * <p>
+ * Send the certificate request to the trusted Certificate Authority for signature.
+ * One may choose to act as her own CA and sign the certificate request using a PKI
+ * tool, such as OpenSSL.
+ * </p>
+ * </li>
+ * <li>
+ * <p>
+ * Import the trusted CA root certificate
+ * <pre>keytool -import -alias "my trusted ca" -file caroot.crt -keystore my.keystore</pre>
+ * </p>
+ * </li>
+ * <li>
+ * <p>
+ * Import the PKCS#7 file containg the complete certificate chain
+ * <pre>keytool -import -alias "my client key" -file mycert.p7 -keystore my.keystore</pre>
+ * </p>
+ * </li>
+ * <li>
+ * <p>
+ * Verify the content the resultant keystore file
+ * <pre>keytool -list -v -keystore my.keystore</pre>
+ * </p>
+ * </li>
+ * </ul>
+ * @author <a href="mailto:oleg at ural.ru">Oleg Kalnichevski</a>
+ * @author Julius Davies
+ */
+
+public class SSLSocketFactory implements LayeredSocketFactory {
+
+ public static final String TLS = "TLS";
+ public static final String SSL = "SSL";
+ public static final String SSLV2 = "SSLv2";
+
+ public static final X509HostnameVerifier ALLOW_ALL_HOSTNAME_VERIFIER
+ = new AllowAllHostnameVerifier();
+
+ public static final X509HostnameVerifier BROWSER_COMPATIBLE_HOSTNAME_VERIFIER
+ = new BrowserCompatHostnameVerifier();
+
+ public static final X509HostnameVerifier STRICT_HOSTNAME_VERIFIER
+ = new StrictHostnameVerifier();
+ /**
+ * The factory using the default JVM settings for secure connections.
+ */
+ private static final SSLSocketFactory DEFAULT_FACTORY = new SSLSocketFactory();
+
+ /**
+ * Gets an singleton instance of the SSLProtocolSocketFactory.
+ * @return a SSLProtocolSocketFactory
+ */
+ public static SSLSocketFactory getSocketFactory() {
+ return DEFAULT_FACTORY;
+ }
+
+ private final SSLContext sslcontext;
+ private final javax.net.ssl.SSLSocketFactory socketfactory;
+ private final HostNameResolver nameResolver;
+ private X509HostnameVerifier hostnameVerifier = BROWSER_COMPATIBLE_HOSTNAME_VERIFIER;
+
+ public SSLSocketFactory(
+ String algorithm,
+ final KeyStore keystore,
+ final String keystorePassword,
+ final KeyStore truststore,
+ final SecureRandom random,
+ final HostNameResolver nameResolver)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException
+ {
+ super();
+ if (algorithm == null) {
+ algorithm = TLS;
+ }
+ KeyManager[] keymanagers = null;
+ if (keystore != null) {
+ keymanagers = createKeyManagers(keystore, keystorePassword);
+ }
+ TrustManager[] trustmanagers = null;
+ if (truststore != null) {
+ trustmanagers = createTrustManagers(truststore);
+ }
+ this.sslcontext = SSLContext.getInstance(algorithm);
+ this.sslcontext.init(keymanagers, trustmanagers, random);
+ this.socketfactory = this.sslcontext.getSocketFactory();
+ this.nameResolver = nameResolver;
+ }
+
+ public SSLSocketFactory(
+ final KeyStore keystore,
+ final String keystorePassword,
+ final KeyStore truststore)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException
+ {
+ this(TLS, keystore, keystorePassword, truststore, null, null);
+ }
+
+ public SSLSocketFactory(final KeyStore keystore, final String keystorePassword)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException
+ {
+ this(TLS, keystore, keystorePassword, null, null, null);
+ }
+
+ public SSLSocketFactory(final KeyStore truststore)
+ throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException
+ {
+ this(TLS, null, null, truststore, null, null);
+ }
+
+ /**
+ * Constructs an HttpClient SSLSocketFactory backed by the given JSSE
+ * SSLSocketFactory.
+ */
+ public SSLSocketFactory(javax.net.ssl.SSLSocketFactory socketfactory) {
+ super();
+ this.sslcontext = null;
+ this.socketfactory = socketfactory;
+ this.nameResolver = null;
+ }
+
+ /**
+ * Creates the default SSL socket factory.
+ * This constructor is used exclusively to instantiate the factory for
+ * {@link #getSocketFactory getSocketFactory}.
+ */
+ private SSLSocketFactory() {
+ super();
+ this.sslcontext = null;
+ this.socketfactory = HttpsURLConnection.getDefaultSSLSocketFactory();
+ this.nameResolver = null;
+ }
+
+ private static KeyManager[] createKeyManagers(final KeyStore keystore, final String password)
+ throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
+ if (keystore == null) {
+ throw new IllegalArgumentException("Keystore may not be null");
+ }
+ KeyManagerFactory kmfactory = KeyManagerFactory.getInstance(
+ KeyManagerFactory.getDefaultAlgorithm());
+ kmfactory.init(keystore, password != null ? password.toCharArray(): null);
+ return kmfactory.getKeyManagers();
+ }
+
+ private static TrustManager[] createTrustManagers(final KeyStore keystore)
+ throws KeyStoreException, NoSuchAlgorithmException {
+ if (keystore == null) {
+ throw new IllegalArgumentException("Keystore may not be null");
+ }
+ TrustManagerFactory tmfactory = TrustManagerFactory.getInstance(
+ TrustManagerFactory.getDefaultAlgorithm());
+ tmfactory.init(keystore);
+ return tmfactory.getTrustManagers();
+ }
+
+
+ // non-javadoc, see interface org.apache.http.conn.SocketFactory
+ public Socket createSocket()
+ throws IOException {
+
+ // the cast makes sure that the factory is working as expected
+ return (SSLSocket) this.socketfactory.createSocket();
+ }
+
+
+ // non-javadoc, see interface org.apache.http.conn.SocketFactory
+ public Socket connectSocket(
+ final Socket sock,
+ final String host,
+ final int port,
+ final InetAddress localAddress,
+ int localPort,
+ final HttpParams params
+ ) throws IOException {
+
+ if (host == null) {
+ throw new IllegalArgumentException("Target host may not be null.");
+ }
+ if (params == null) {
+ throw new IllegalArgumentException("Parameters may not be null.");
+ }
+
+ SSLSocket sslsock = (SSLSocket)
+ ((sock != null) ? sock : createSocket());
+
+ if ((localAddress != null) || (localPort > 0)) {
+
+ // we need to bind explicitly
+ if (localPort < 0)
+ localPort = 0; // indicates "any"
+
+ InetSocketAddress isa =
+ new InetSocketAddress(localAddress, localPort);
+ sslsock.bind(isa);
+ }
+
+ int connTimeout = HttpConnectionParams.getConnectionTimeout(params);
+ int soTimeout = HttpConnectionParams.getSoTimeout(params);
+
+ InetSocketAddress remoteAddress;
+ if (this.nameResolver != null) {
+ remoteAddress = new InetSocketAddress(this.nameResolver.resolve(host), port);
+ } else {
+ remoteAddress = new InetSocketAddress(host, port);
+ }
+
+ sslsock.connect(remoteAddress, connTimeout);
+
+ sslsock.setSoTimeout(soTimeout);
+ try {
+ hostnameVerifier.verify(host, sslsock);
+ // verifyHostName() didn't blowup - good!
+ } catch (IOException iox) {
+ // close the socket before re-throwing the exception
+ try { sslsock.close(); } catch (Exception x) { /*ignore*/ }
+ throw iox;
+ }
+
+ return sslsock;
+ }
+
+
+ /**
+ * Checks whether a socket connection is secure.
+ * This factory creates TLS/SSL socket connections
+ * which, by default, are considered secure.
+ * <br/>
+ * Derived classes may override this method to perform
+ * runtime checks, for example based on the cypher suite.
+ *
+ * @param sock the connected socket
+ *
+ * @return <code>true</code>
+ *
+ * @throws IllegalArgumentException if the argument is invalid
+ */
+ public boolean isSecure(Socket sock)
+ throws IllegalArgumentException {
+
+ if (sock == null) {
+ throw new IllegalArgumentException("Socket may not be null.");
+ }
+ // This instanceof check is in line with createSocket() above.
+ if (!(sock instanceof SSLSocket)) {
+ throw new IllegalArgumentException
+ ("Socket not created by this factory.");
+ }
+ // This check is performed last since it calls the argument object.
+ if (sock.isClosed()) {
+ throw new IllegalArgumentException("Socket is closed.");
+ }
+
+ return true;
+
+ } // isSecure
+
+
+ // non-javadoc, see interface LayeredSocketFactory
+ public Socket createSocket(
+ final Socket socket,
+ final String host,
+ final int port,
+ final boolean autoClose
+ ) throws IOException, UnknownHostException {
+ SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket(
+ socket,
+ host,
+ port,
+ autoClose
+ );
+ hostnameVerifier.verify(host, sslSocket);
+ // verifyHostName() didn't blowup - good!
+ return sslSocket;
+ }
+
+ public void setHostnameVerifier(X509HostnameVerifier hostnameVerifier) {
+ if ( hostnameVerifier == null ) {
+ throw new IllegalArgumentException("Hostname verifier may not be null");
+ }
+ this.hostnameVerifier = hostnameVerifier;
+ }
+
+ public X509HostnameVerifier getHostnameVerifier() {
+ return hostnameVerifier;
+ }
+
+}
diff --git a/src/com/android/exchange/SyncManager.java b/src/com/android/exchange/SyncManager.java
index 0133aaf..2a1e10b 100644
--- a/src/com/android/exchange/SyncManager.java
+++ b/src/com/android/exchange/SyncManager.java
@@ -18,6 +18,7 @@
package com.android.exchange;
import com.android.email.AccountBackupRestore;
+import com.android.email.Email;
import com.android.email.mail.MessagingException;
import com.android.email.mail.store.TrustManagerFactory;
import com.android.email.provider.EmailContent;
@@ -29,9 +30,10 @@
import com.android.email.provider.EmailContent.MailboxColumns;
import com.android.email.provider.EmailContent.Message;
import com.android.email.provider.EmailContent.SyncColumns;
+import com.android.email.service.IEmailService;
+import com.android.email.service.IEmailServiceCallback;
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;
@@ -39,7 +41,6 @@
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;
@@ -195,7 +196,7 @@
private EasSyncStatusObserver mSyncStatusObserver;
private EasAccountsUpdatedListener mAccountsUpdatedListener;
- private ContentResolver mResolver;
+ /*package*/ ContentResolver mResolver;
// The singleton SyncManager object, with its thread and stop flag
protected static SyncManager INSTANCE;
@@ -280,6 +281,10 @@
}
}
+ public Bundle autoDiscover(String userName, String password) throws RemoteException {
+ return new EasSyncService().tryAutodiscover(userName, password);
+ }
+
public void startSync(long mailboxId) throws RemoteException {
checkSyncManagerServiceRunning();
Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId);
@@ -296,8 +301,8 @@
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
+ } else if (m.mType == Mailbox.TYPE_DRAFTS || m.mType == Mailbox.TYPE_TRASH) {
+ // Drafts & Trash can't be synced in EAS
return;
}
startManualSync(mailboxId, SyncManager.SYNC_SERVICE_START_SYNC, null);
@@ -310,7 +315,7 @@
public void loadAttachment(long attachmentId, String destinationFile,
String contentUriString) throws RemoteException {
Attachment att = Attachment.restoreAttachmentWithId(SyncManager.this, attachmentId);
- partRequest(new PartRequest(att, destinationFile, contentUriString));
+ sendMessageRequest(new PartRequest(att, destinationFile, contentUriString));
}
public void updateFolderList(long accountId) throws RemoteException {
@@ -326,7 +331,10 @@
// 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) {
+ // If the mailbox is no longer around, remove the entry in the map
+ if (m == null) {
+ INSTANCE.mSyncErrorMap.remove(mailboxId);
+ } else if (m.mAccountKey == accountId) {
error.fatal = false;
error.holdEndTime = 0;
}
@@ -343,8 +351,11 @@
Eas.setUserDebug(on);
}
+ public void sendMeetingResponse(long messageId, int response) throws RemoteException {
+ sendMessageRequest(new MeetingResponseRequest(messageId, response));
+ }
+
public void loadMore(long messageId) throws RemoteException {
- // TODO Auto-generated method stub
}
// The following three methods are not implemented in this version
@@ -492,13 +503,17 @@
try {
collectEasAccounts(c, currentAccounts);
for (Account account : mAccounts) {
- if (!currentAccounts.contains(account.mId)) {
+ // Ignore accounts not fully created
+ if ((account.mFlags & Account.FLAGS_INCOMPLETE) != 0) {
+ log("Account observer noticed incomplete account; ignoring");
+ continue;
+ } else 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);
+ Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
AccountManager.get(SyncManager.this).removeAccount(acct, null, null);
mSyncableEasMailboxSelector = null;
mEasAccountSelector = null;
@@ -514,8 +529,7 @@
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...");
+ log("Account " + account.mDisplayName + " changed; stop syncs");
stopAccountSyncs(account.mId, true);
}
}
@@ -525,6 +539,7 @@
for (Account account: currentAccounts) {
if (!mAccounts.contains(account.mId)) {
// This is an addition; create our magic hidden mailbox...
+ log("Account observer found new account: " + account.mDisplayName);
addAccountMailbox(account.mId);
// Don't forget to cache the HostAuth
HostAuth ha =
@@ -692,7 +707,9 @@
public class EasAccountsUpdatedListener implements OnAccountsUpdateListener {
public void onAccountsUpdated(android.accounts.Account[] accounts) {
- checkWithAccountManager();
+ reconcileAccountsWithAccountManager(INSTANCE, getAccountList(),
+ AccountManager.get(INSTANCE).getAccountsByType(
+ Email.EXCHANGE_ACCOUNT_MANAGER_TYPE));
}
}
@@ -729,7 +746,7 @@
static public String getDeviceId() throws IOException {
return getDeviceId(null);
}
-
+
static public synchronized String getDeviceId(Context context) throws IOException {
if (sDeviceId != null) {
return sDeviceId;
@@ -751,7 +768,7 @@
return id;
} else if (f.createNewFile()) {
BufferedWriter w = new BufferedWriter(new FileWriter(f), 128);
- id = "droid" + System.currentTimeMillis();
+ id = "android" + System.currentTimeMillis();
w.write(id);
w.close();
sDeviceId = id;
@@ -768,10 +785,10 @@
}
/**
- * Note that there are two ways the EAS SyncManager service can be created:
+ * 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
+ * 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)
@@ -841,6 +858,7 @@
mSyncedMessageObserver = null;
mMessageObserver = null;
mSyncStatusObserver = null;
+ mAccountsUpdatedListener = null;
}
}
@@ -867,7 +885,7 @@
INSTANCE.startService(new Intent(INSTANCE, SyncManager.class));
}
}
-
+
static public ConnPerRoute sConnPerRoute = new ConnPerRoute() {
public int getMaxForRoute(HttpRoute route) {
return 8;
@@ -889,15 +907,13 @@
SSLContext sslcontext;
try {
sslcontext = SSLContext.getInstance("TLS");
- sslcontext.init(null, trustManagers, null);
- SSLContextImpl sslContext = new SSLContextImpl();
try {
- sslContext.engineInit(null, trustManagers, null, null, null);
+ sslcontext.init(null, trustManagers, null);
} catch (KeyManagementException e) {
throw new AssertionError(e);
}
// Ok, now make our factory
- SSLSocketFactory sf = new SSLSocketFactory(sslContext.engineGetSocketFactory());
+ SSLSocketFactory sf = new SSLSocketFactory(sslcontext.getSocketFactory());
sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
// Register the httpts scheme with our factory
registry.register(new Scheme("httpts", sf, 443));
@@ -907,7 +923,6 @@
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
@@ -1153,6 +1168,10 @@
// 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) {
+ String[] args = new String[] {Long.toString(m.mId)};
+ ContentResolver resolver = INSTANCE.mResolver;
+ resolver.delete(Message.DELETED_CONTENT_URI, WHERE_MAILBOX_KEY, args);
+ resolver.delete(Message.UPDATED_CONTENT_URI, WHERE_MAILBOX_KEY, args);
return;
}
service.mAccount = Account.restoreAccountWithId(INSTANCE, m.mAccountKey);
@@ -1183,7 +1202,7 @@
// Create an AccountManager style Account
android.accounts.Account acct =
new android.accounts.Account(easAccount.mEmailAddress,
- Eas.ACCOUNT_MANAGER_TYPE);
+ Email.EXCHANGE_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;
@@ -1216,25 +1235,56 @@
}
}
- 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;
+ /**
+ * Compare our account list (obtained from EmailProvider) with the account list owned by
+ * AccountManager. If there are any orphans (an account in one list without a corresponding
+ * account in the other list), delete the orphan, as these must remain in sync.
+ *
+ * Note that the duplication of account information is caused by the Email application's
+ * incomplete integration with AccountManager.
+ */
+ /*package*/ void reconcileAccountsWithAccountManager(Context context,
+ List<Account> cachedEasAccounts, android.accounts.Account[] accountManagerAccounts) {
+ // First, look through our cached EAS Accounts (from EmailProvider) to make sure there's a
+ // corresponding AccountManager account
+ for (Account providerAccount: cachedEasAccounts) {
+ String providerAccountName = providerAccount.mEmailAddress;
boolean found = false;
- for (android.accounts.Account acct: accts) {
- if (acct.name.equalsIgnoreCase(accountName)) {
+ for (android.accounts.Account accountManagerAccount: accountManagerAccounts) {
+ if (accountManagerAccount.name.equalsIgnoreCase(providerAccountName)) {
found = true;
break;
}
}
if (!found) {
+ if ((providerAccount.mFlags & Account.FLAGS_INCOMPLETE) != 0) {
+ log("Account reconciler noticed incomplete account; ignoring");
+ continue;
+ }
// This account has been deleted in the AccountManager!
- log("Account deleted in AccountManager; deleting from provider: " + accountName);
+ log("Account deleted in AccountManager; deleting from provider: " +
+ providerAccountName);
// TODO This will orphan downloaded attachments; need to handle this
- mResolver.delete(ContentUris.withAppendedId(Account.CONTENT_URI, easAccount.mId),
- null, null);
+ mResolver.delete(ContentUris.withAppendedId(Account.CONTENT_URI,
+ providerAccount.mId), null, null);
+ }
+ }
+ // Now, look through AccountManager accounts to make sure we have a corresponding cached EAS
+ // account from EmailProvider
+ for (android.accounts.Account accountManagerAccount: accountManagerAccounts) {
+ String accountManagerAccountName = accountManagerAccount.name;
+ boolean found = false;
+ for (Account cachedEasAccount: cachedEasAccounts) {
+ if (cachedEasAccount.mEmailAddress.equalsIgnoreCase(accountManagerAccountName)) {
+ found = true;
+ }
+ }
+ if (!found) {
+ // This account has been deleted from the EmailProvider database
+ log("Account deleted from provider; deleting from AccountManager: " +
+ accountManagerAccountName);
+ // Delete the account
+ AccountManager.get(context).removeAccount(accountManagerAccount, null, null);
}
}
}
@@ -1295,19 +1345,23 @@
}
}
- private void startService(Mailbox m, int reason, PartRequest req) {
+ private void startService(Mailbox m, int reason, Request 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);
+ // Always make sure there's not a running instance of this service
+ AbstractSyncService service = mServiceMap.get(m.mId);
+ if (service == null) {
+ service = new EasSyncService(this, m);
+ if (!((EasSyncService)service).mIsValid) return;
+ service.mSyncReason = reason;
+ if (req != null) {
+ service.addRequest(req);
+ }
+ startService(service, m);
}
- startService(service, m);
}
}
}
@@ -1392,6 +1446,7 @@
mResolver.registerContentObserver(Mailbox.CONTENT_URI, false, mMailboxObserver);
mResolver.registerContentObserver(Message.SYNCED_CONTENT_URI, true, mSyncedMessageObserver);
mResolver.registerContentObserver(Message.CONTENT_URI, true, mMessageObserver);
+ // TODO SYNC_OBSERVER_TYPE_SETTINGS is hidden. Waiting for b/2337197
ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS,
mSyncStatusObserver);
mAccountsUpdatedListener = new EasAccountsUpdatedListener();
@@ -1447,7 +1502,7 @@
if (mConnectivityReceiver != null) {
unregisterReceiver(mConnectivityReceiver);
}
-
+
if (INSTANCE != null) {
ContentResolver resolver = getContentResolver();
resolver.unregisterContentObserver(mAccountObserver);
@@ -1496,18 +1551,21 @@
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");
+ // If so, stop them or remove them from the map
+ for (Long mailboxId: deletedMailboxes) {
+ AbstractSyncService svc = mServiceMap.get(mailboxId);
+ if (svc == null || svc.mThread == null) {
releaseMailbox(mailboxId);
+ continue;
+ } else {
+ boolean alive = svc.mThread.isAlive();
+ log("Deleted mailbox: " + svc.mMailboxName);
+ if (alive) {
+ stopManualSync(mailboxId);
+ } else {
+ log("Removing from serviceMap");
+ releaseMailbox(mailboxId);
+ }
}
}
}
@@ -1523,7 +1581,10 @@
try {
while (c.moveToNext()) {
long mid = c.getLong(Mailbox.CONTENT_ID_COLUMN);
- AbstractSyncService service = mServiceMap.get(mid);
+ AbstractSyncService service = null;
+ synchronized (sSyncToken) {
+ service = mServiceMap.get(mid);
+ }
if (service == null) {
// Check whether we're in a hold (temporary or permanent)
SyncError syncError = mSyncErrorMap.get(mid);
@@ -1553,7 +1614,7 @@
if (account != null) {
android.accounts.Account a =
new android.accounts.Account(account.mEmailAddress,
- Eas.ACCOUNT_MANAGER_TYPE);
+ Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
if (!ContentResolver.getSyncAutomatically(a,
ContactsContract.AUTHORITY)) {
continue;
@@ -1672,9 +1733,9 @@
}
}
- static public void partRequest(PartRequest req) {
+ static public void sendMessageRequest(Request req) {
if (INSTANCE == null) return;
- Message msg = Message.restoreMessageWithId(INSTANCE, req.emailId);
+ Message msg = Message.restoreMessageWithId(INSTANCE, req.mMessageId);
if (msg == null) {
return;
}
@@ -1685,33 +1746,7 @@
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);
+ service.addRequest(req);
}
}
@@ -1739,7 +1774,7 @@
return PING_STATUS_OK;
}
- static public AbstractSyncService startManualSync(long mailboxId, int reason, PartRequest req) {
+ static public AbstractSyncService startManualSync(long mailboxId, int reason, Request req) {
if (INSTANCE == null || INSTANCE.mServiceMap == null) return null;
synchronized (sSyncToken) {
if (INSTANCE.mServiceMap.get(mailboxId) == null) {
@@ -1812,13 +1847,14 @@
int exitStatus = svc.mExitStatus;
switch (exitStatus) {
case AbstractSyncService.EXIT_DONE:
- if (!svc.mPartRequests.isEmpty()) {
+ if (!svc.mRequests.isEmpty()) {
// TODO Handle this case
}
errorMap.remove(mailboxId);
break;
case AbstractSyncService.EXIT_IO_ERROR:
Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId);
+ if (m == null) return;
if (syncError != null) {
syncError.escalate();
INSTANCE.log(m.mDisplayName + " held for " + syncError.holdDelay + "ms");
diff --git a/src/com/android/exchange/adapter/CalendarSyncAdapter.java b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
index 739c9e2..41b6a66 100644
--- a/src/com/android/exchange/adapter/CalendarSyncAdapter.java
+++ b/src/com/android/exchange/adapter/CalendarSyncAdapter.java
@@ -17,11 +17,43 @@
package com.android.exchange.adapter;
+import com.android.email.Email;
import com.android.email.provider.EmailContent.Mailbox;
import com.android.exchange.EasSyncService;
+import com.android.exchange.utility.CalendarUtilities;
+import com.android.exchange.utility.Duration;
+
+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.Entity.NamedContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.pim.DateException;
+import android.provider.Calendar;
+import android.provider.SyncStateContract;
+import android.provider.Calendar.Attendees;
+import android.provider.Calendar.Calendars;
+import android.provider.Calendar.Events;
+import android.provider.Calendar.EventsEntity;
+import android.provider.Calendar.ExtendedProperties;
+import android.provider.Calendar.Reminders;
+import android.provider.Calendar.SyncState;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.StringTokenizer;
+import java.util.TimeZone;
/**
* Sync adapter class for EAS calendars
@@ -29,8 +61,56 @@
*/
public class CalendarSyncAdapter extends AbstractSyncAdapter {
+ private static final String TAG = "EasCalendarSyncAdapter";
+ // Since exceptions will have the same _SYNC_ID as the original event we have to check that
+ // there's no original event when finding an item by _SYNC_ID
+ private static final String SERVER_ID = Events._SYNC_ID + "=? AND " +
+ Events.ORIGINAL_EVENT + " ISNULL";
+ private static final String DIRTY_TOP_LEVEL =
+ Events._SYNC_DIRTY + "=1 AND " + Events.ORIGINAL_EVENT + " ISNULL";
+ private static final String DIRTY_EXCEPTION =
+ Events._SYNC_DIRTY + "=1 AND " + Events.ORIGINAL_EVENT + " NOTNULL";
+ private static final String DIRTY_IN_CALENDAR =
+ Events._SYNC_DIRTY + "=1 AND " + Events.CALENDAR_ID + "=?";
+ private static final String CLIENT_ID_SELECTION = Events._SYNC_LOCAL_ID + "=?";
+ private static final String ATTENDEES_EXCEPT_ORGANIZER = Attendees.EVENT_ID + "=? AND " +
+ Attendees.ATTENDEE_RELATIONSHIP + "!=" + Attendees.RELATIONSHIP_ORGANIZER;
+ private static final String[] ID_PROJECTION = new String[] {Events._ID};
+ private static final String[] ORIGINAL_EVENT_PROJECTION = new String[] {Events.ORIGINAL_EVENT};
+
+ private static final String CALENDAR_SELECTION =
+ Calendars._SYNC_ACCOUNT + "=? AND " + Calendars._SYNC_ACCOUNT_TYPE + "=?";
+ private static final int CALENDAR_SELECTION_ID = 0;
+
+ private static final String CATEGORY_TOKENIZER_DELIMITER = "\\";
+
+ private static final ContentProviderOperation PLACEHOLDER_OPERATION =
+ ContentProviderOperation.newInsert(Uri.EMPTY).build();
+
+ private static final Uri sEventsUri = asSyncAdapter(Events.CONTENT_URI);
+ private static final Uri sAttendeesUri = asSyncAdapter(Attendees.CONTENT_URI);
+ private static final Uri sExtendedPropertiesUri = asSyncAdapter(ExtendedProperties.CONTENT_URI);
+ private static final Uri sRemindersUri = asSyncAdapter(Reminders.CONTENT_URI);
+
+ private android.accounts.Account mAccountManagerAccount;
+ private long mCalendarId = -1;
+
+ private ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
+ private ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
+
public CalendarSyncAdapter(Mailbox mailbox, EasSyncService service) {
super(mailbox, service);
+
+ Cursor c = mService.mContentResolver.query(Calendars.CONTENT_URI,
+ new String[] {Calendars._ID}, CALENDAR_SELECTION,
+ new String[] {mAccount.mEmailAddress, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE}, null);
+ try {
+ if (c.moveToFirst()) {
+ mCalendarId = c.getLong(CALENDAR_SELECTION_ID);
+ }
+ } finally {
+ c.close();
+ }
}
@Override
@@ -39,19 +119,1116 @@
}
@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
+ EasCalendarSyncParser p = new EasCalendarSyncParser(is, this);
+ return p.parse();
+ }
+
+ static Uri asSyncAdapter(Uri uri) {
+ return uri.buildUpon().appendQueryParameter(Calendar.CALLER_IS_SYNCADAPTER, "true").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;
+ }
+
+ /**
+ * We get our SyncKey from CalendarProvider. 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(Calendar.CONTENT_URI);
+ try {
+ byte[] data = SyncStateContract.Helpers.get(client,
+ asSyncAdapter(Calendar.SyncState.CONTENT_URI), getAccountManagerAccount());
+ if (data == null || data.length == 0) {
+ // Initialize the SyncKey
+ setSyncKey("0", false);
+ 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 Calendar
+ */
+ @Override
+ public void setSyncKey(String syncKey, boolean inCommands) throws IOException {
+ if ("0".equals(syncKey) || !inCommands) {
+ ContentProviderClient client =
+ mService.mContentResolver
+ .acquireContentProviderClient(Calendar.CONTENT_URI);
+ try {
+ SyncStateContract.Helpers.set(client, asSyncAdapter(Calendar.SyncState.CONTENT_URI),
+ getAccountManagerAccount(), syncKey.getBytes());
+ userLog("SyncKey set to ", syncKey, " in CalendarProvider");
+ } catch (RemoteException e) {
+ throw new IOException("Can't set SyncKey in CalendarProvider");
+ }
+ }
+ mMailbox.mSyncKey = syncKey;
+ }
+
+ public android.accounts.Account getAccountManagerAccount() {
+ if (mAccountManagerAccount == null) {
+ mAccountManagerAccount =
+ new android.accounts.Account(mAccount.mEmailAddress,
+ Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+ }
+ return mAccountManagerAccount;
+ }
+
+ class EasCalendarSyncParser extends AbstractSyncParser {
+
+ String[] mBindArgument = new String[1];
+ String mMailboxIdAsString;
+ Uri mAccountUri;
+ CalendarOperations mOps = new CalendarOperations();
+
+ public EasCalendarSyncParser(InputStream in, CalendarSyncAdapter adapter)
+ throws IOException {
+ super(in, adapter);
+ setLoggingTag("CalendarParser");
+ mAccountUri = Events.CONTENT_URI;
+ }
+
+ @Override
+ public void wipe() {
+ // Delete the calendar associated with this account
+ // TODO Make sure the Events, etc. are also deleted
+ mContentResolver.delete(Calendars.CONTENT_URI, CALENDAR_SELECTION,
+ new String[] {mAccount.mEmailAddress, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE});
+ }
+
+ public void addEvent(CalendarOperations ops, String serverId, boolean update)
+ throws IOException {
+ ContentValues cv = new ContentValues();
+ cv.put(Events.CALENDAR_ID, mCalendarId);
+ cv.put(Events._SYNC_ACCOUNT, mAccount.mEmailAddress);
+ cv.put(Events._SYNC_ACCOUNT_TYPE, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+ cv.put(Events._SYNC_ID, serverId);
+
+ int allDayEvent = 0;
+ String organizerName = null;
+ String organizerEmail = null;
+ int eventOffset = -1;
+
+ boolean firstTag = true;
+ long eventId = -1;
+ long startTime = -1;
+ long endTime = -1;
+ while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
+ if (update && firstTag) {
+ // Find the event that's being updated
+ Cursor c = getServerIdCursor(serverId);
+ long id = -1;
+ try {
+ if (c.moveToFirst()) {
+ id = c.getLong(0);
+ }
+ } finally {
+ c.close();
+ }
+ if (id > 0) {
+ if (tag == Tags.CALENDAR_ATTENDEES) {
+ // This is an attendees-only update; just delete/re-add attendees
+ mBindArgument[0] = Long.toString(id);
+ ops.add(ContentProviderOperation.newDelete(Attendees.CONTENT_URI)
+ .withSelection(ATTENDEES_EXCEPT_ORGANIZER, mBindArgument)
+ .build());
+ eventId = id;
+ } else {
+ // Otherwise, delete the original event and recreate it
+ userLog("Changing (delete/add) event ", serverId);
+ ops.delete(id);
+ // Add a placeholder event so that associated tables can reference
+ // this as a back reference. We add the event at the end of the method
+ eventOffset = ops.newEvent(PLACEHOLDER_OPERATION);
+ }
+ }
+ } else if (firstTag) {
+ // Add a placeholder event so that associated tables can reference
+ // this as a back reference. We add the event at the end of the method
+ eventOffset = ops.newEvent(PLACEHOLDER_OPERATION);
+ }
+ firstTag = false;
+ switch (tag) {
+ case Tags.CALENDAR_ALL_DAY_EVENT:
+ allDayEvent = getValueInt();
+ cv.put(Events.ALL_DAY, allDayEvent);
+ break;
+ case Tags.CALENDAR_ATTENDEES:
+ // If eventId >= 0, this is an update; otherwise, a new Event
+ attendeesParser(ops, organizerName, organizerEmail, eventId);
+ break;
+ case Tags.BASE_BODY:
+ cv.put(Events.DESCRIPTION, bodyParser());
+ break;
+ case Tags.CALENDAR_BODY:
+ cv.put(Events.DESCRIPTION, getValue());
+ break;
+ case Tags.CALENDAR_TIME_ZONE:
+ TimeZone tz = CalendarUtilities.tziStringToTimeZone(getValue());
+ if (tz != null) {
+ cv.put(Events.EVENT_TIMEZONE, tz.getID());
+ } else {
+ cv.put(Events.EVENT_TIMEZONE, TimeZone.getDefault().getID());
+ }
+ break;
+ case Tags.CALENDAR_START_TIME:
+ startTime = CalendarUtilities.parseDateTimeToMillis(getValue());
+ cv.put(Events.DTSTART, startTime);
+ cv.put(Events.ORIGINAL_INSTANCE_TIME, startTime);
+ break;
+ case Tags.CALENDAR_END_TIME:
+ endTime = CalendarUtilities.parseDateTimeToMillis(getValue());
+ break;
+ case Tags.CALENDAR_EXCEPTIONS:
+ exceptionsParser(ops, cv);
+ break;
+ case Tags.CALENDAR_LOCATION:
+ cv.put(Events.EVENT_LOCATION, getValue());
+ break;
+ case Tags.CALENDAR_RECURRENCE:
+ String rrule = recurrenceParser(ops);
+ if (rrule != null) {
+ cv.put(Events.RRULE, rrule);
+ }
+ break;
+ case Tags.CALENDAR_ORGANIZER_EMAIL:
+ organizerEmail = getValue();
+ cv.put(Events.ORGANIZER, organizerEmail);
+ break;
+ case Tags.CALENDAR_SUBJECT:
+ cv.put(Events.TITLE, getValue());
+ break;
+ case Tags.CALENDAR_SENSITIVITY:
+ cv.put(Events.VISIBILITY, encodeVisibility(getValueInt()));
+ break;
+ case Tags.CALENDAR_ORGANIZER_NAME:
+ organizerName = getValue();
+ break;
+ case Tags.CALENDAR_REMINDER_MINS_BEFORE:
+ ops.newReminder(getValueInt());
+ cv.put(Events.HAS_ALARM, 1);
+ break;
+ // The following are fields we should save (for changes), though they don't
+ // relate to data used by CalendarProvider at this point
+ case Tags.CALENDAR_UID:
+ ops.newExtendedProperty("uid", getValue());
+ break;
+ case Tags.CALENDAR_DTSTAMP:
+ ops.newExtendedProperty("dtstamp", getValue());
+ break;
+ case Tags.CALENDAR_MEETING_STATUS:
+ ops.newExtendedProperty("meeting_status", getValue());
+ break;
+ case Tags.CALENDAR_BUSY_STATUS:
+ ops.newExtendedProperty("busy_status", getValue());
+ break;
+ case Tags.CALENDAR_CATEGORIES:
+ String categories = categoriesParser(ops);
+ if (categories.length() > 0) {
+ ops.newExtendedProperty("categories", categories);
+ }
+ break;
+ default:
+ skipTag();
+ }
+ }
+
+ // If there's no recurrence, set DTEND to the end time
+ if (!cv.containsKey(Events.RRULE)) {
+ cv.put(Events.DTEND, endTime);
+ cv.put(Events.LAST_DATE, endTime);
+ }
+
+ // Set the DURATION using rfc2445
+ if (allDayEvent != 0) {
+ cv.put(Events.DURATION, "P1D");
+ } else {
+ cv.put(Events.DURATION, "P" + ((endTime - startTime) / MINUTES) + "M");
+ }
+
+ // Put the real event in the proper place in the ops ArrayList
+ if (eventOffset >= 0) {
+ ops.set(eventOffset, ContentProviderOperation
+ .newInsert(sEventsUri).withValues(cv).build());
+ }
+ }
+
+ private String recurrenceParser(CalendarOperations ops) throws IOException {
+ // Turn this information into an RRULE
+ int type = -1;
+ int occurrences = -1;
+ int interval = -1;
+ int dow = -1;
+ int dom = -1;
+ int wom = -1;
+ int moy = -1;
+ String until = null;
+
+ while (nextTag(Tags.CALENDAR_RECURRENCE) != END) {
+ switch (tag) {
+ case Tags.CALENDAR_RECURRENCE_TYPE:
+ type = getValueInt();
+ break;
+ case Tags.CALENDAR_RECURRENCE_INTERVAL:
+ interval = getValueInt();
+ break;
+ case Tags.CALENDAR_RECURRENCE_OCCURRENCES:
+ occurrences = getValueInt();
+ break;
+ case Tags.CALENDAR_RECURRENCE_DAYOFWEEK:
+ dow = getValueInt();
+ break;
+ case Tags.CALENDAR_RECURRENCE_DAYOFMONTH:
+ dom = getValueInt();
+ break;
+ case Tags.CALENDAR_RECURRENCE_WEEKOFMONTH:
+ wom = getValueInt();
+ break;
+ case Tags.CALENDAR_RECURRENCE_MONTHOFYEAR:
+ moy = getValueInt();
+ break;
+ case Tags.CALENDAR_RECURRENCE_UNTIL:
+ until = getValue();
+ break;
+ default:
+ skipTag();
+ }
+ }
+
+ return CalendarUtilities.rruleFromRecurrence(type, occurrences, interval,
+ dow, dom, wom, moy, until);
+ }
+
+ private void exceptionParser(CalendarOperations ops, ContentValues parentCv)
+ throws IOException {
+ ContentValues cv = new ContentValues();
+ cv.put(Events.CALENDAR_ID, mCalendarId);
+ cv.put(Events._SYNC_ACCOUNT, mAccount.mEmailAddress);
+ cv.put(Events._SYNC_ACCOUNT_TYPE, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+
+ // It appears that these values have to be copied from the parent if they are to appear
+ // Note that they can be overridden below
+ cv.put(Events.ORGANIZER, parentCv.getAsString(Events.ORGANIZER));
+ cv.put(Events.TITLE, parentCv.getAsString(Events.TITLE));
+ cv.put(Events.DESCRIPTION, parentCv.getAsBoolean(Events.DESCRIPTION));
+ cv.put(Events.ORIGINAL_ALL_DAY, parentCv.getAsInteger(Events.ALL_DAY));
+
+ // This column is the key that links the exception to the serverId
+ // TODO Make sure calendar knows this isn't globally unique!!
+ cv.put(Events.ORIGINAL_EVENT, parentCv.getAsString(Events._SYNC_ID));
+
+ String exceptionStartTime = "_noStartTime";
+ while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
+ switch (tag) {
+ case Tags.CALENDAR_EXCEPTION_START_TIME:
+ exceptionStartTime = getValue();
+ cv.put(Events.ORIGINAL_INSTANCE_TIME,
+ CalendarUtilities.parseDateTimeToMillis(exceptionStartTime));
+ break;
+ case Tags.CALENDAR_EXCEPTION_IS_DELETED:
+ if (getValueInt() == 1) {
+ cv.put(Events.STATUS, Events.STATUS_CANCELED);
+ }
+ break;
+ case Tags.CALENDAR_ALL_DAY_EVENT:
+ cv.put(Events.ALL_DAY, getValueInt());
+ break;
+ case Tags.BASE_BODY:
+ cv.put(Events.DESCRIPTION, bodyParser());
+ break;
+ case Tags.CALENDAR_BODY:
+ cv.put(Events.DESCRIPTION, getValue());
+ break;
+ case Tags.CALENDAR_START_TIME:
+ cv.put(Events.DTSTART, CalendarUtilities.parseDateTimeToMillis(getValue()));
+ break;
+ case Tags.CALENDAR_END_TIME:
+ cv.put(Events.DTEND, CalendarUtilities.parseDateTimeToMillis(getValue()));
+ break;
+ case Tags.CALENDAR_LOCATION:
+ cv.put(Events.EVENT_LOCATION, getValue());
+ break;
+ case Tags.CALENDAR_RECURRENCE:
+ String rrule = recurrenceParser(ops);
+ if (rrule != null) {
+ cv.put(Events.RRULE, rrule);
+ }
+ break;
+ case Tags.CALENDAR_SUBJECT:
+ cv.put(Events.TITLE, getValue());
+ break;
+ case Tags.CALENDAR_SENSITIVITY:
+ cv.put(Events.VISIBILITY, encodeVisibility(getValueInt()));
+ break;
+
+ // TODO How to handle these items that are linked to event id!
+
+// case Tags.CALENDAR_DTSTAMP:
+// ops.newExtendedProperty("dtstamp", getValue());
+// break;
+// case Tags.CALENDAR_BUSY_STATUS:
+// // TODO Try to fit this into Calendar scheme
+// ops.newExtendedProperty("busy_status", getValue());
+// break;
+// case Tags.CALENDAR_REMINDER_MINS_BEFORE:
+// ops.newReminder(getValueInt());
+// break;
+
+ // Not yet handled
+ default:
+ skipTag();
+ }
+ }
+
+ // We need a _sync_id, but it can't be the parent's id, so we generate one
+ cv.put(Events._SYNC_ID, parentCv.getAsString(Events._SYNC_ID) + '_' +
+ exceptionStartTime);
+
+ if (!cv.containsKey(Events.DTSTART)) {
+ cv.put(Events.DTSTART, parentCv.getAsLong(Events.DTSTART));
+ }
+ if (!cv.containsKey(Events.DTEND)) {
+ cv.put(Events.DTEND, parentCv.getAsLong(Events.DTEND));
+ }
+ // TODO See if this is necessary
+ //cv.put(Events.LAST_DATE, cv.getAsLong(Events.DTEND));
+
+ ops.newException(cv);
+ }
+
+ private int encodeVisibility(int easVisibility) {
+ int visibility = 0;
+ switch(easVisibility) {
+ case 0:
+ visibility = Events.VISIBILITY_DEFAULT;
+ break;
+ case 1:
+ visibility = Events.VISIBILITY_PUBLIC;
+ break;
+ case 2:
+ visibility = Events.VISIBILITY_PRIVATE;
+ break;
+ case 3:
+ visibility = Events.VISIBILITY_CONFIDENTIAL;
+ break;
+ }
+ return visibility;
+ }
+
+ private void exceptionsParser(CalendarOperations ops, ContentValues cv)
+ throws IOException {
+ while (nextTag(Tags.CALENDAR_EXCEPTIONS) != END) {
+ switch (tag) {
+ case Tags.CALENDAR_EXCEPTION:
+ exceptionParser(ops, cv);
+ break;
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ private String categoriesParser(CalendarOperations ops) throws IOException {
+ StringBuilder categories = new StringBuilder();
+ while (nextTag(Tags.CALENDAR_CATEGORIES) != END) {
+ switch (tag) {
+ case Tags.CALENDAR_CATEGORY:
+ // TODO Handle categories (there's no similar concept for gdata AFAIK)
+ // We need to save them and spit them back when we update the event
+ categories.append(getValue());
+ categories.append(CATEGORY_TOKENIZER_DELIMITER);
+ default:
+ skipTag();
+ }
+ }
+ return categories.toString();
+ }
+
+ private String attendeesParser(CalendarOperations ops, String organizerName,
+ String organizerEmail, long eventId) throws IOException {
+ String body = null;
+ // First, handle the organizer (who IS an attendee on device, but NOT in EAS)
+ if (organizerName != null || organizerEmail != null) {
+ ContentValues cv = new ContentValues();
+ if (organizerName != null) {
+ cv.put(Attendees.ATTENDEE_NAME, organizerName);
+ }
+ if (organizerEmail != null) {
+ cv.put(Attendees.ATTENDEE_EMAIL, organizerEmail);
+ }
+ cv.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ORGANIZER);
+ cv.put(Attendees.ATTENDEE_TYPE, Attendees.TYPE_REQUIRED);
+ if (eventId < 0) {
+ ops.newAttendee(cv);
+ } else {
+ ops.updatedAttendee(cv, eventId);
+ }
+ }
+ while (nextTag(Tags.CALENDAR_ATTENDEES) != END) {
+ switch (tag) {
+ case Tags.CALENDAR_ATTENDEE:
+ attendeeParser(ops, eventId);
+ break;
+ default:
+ skipTag();
+ }
+ }
+ return body;
+ }
+
+ private void attendeeParser(CalendarOperations ops, long eventId) throws IOException {
+ ContentValues cv = new ContentValues();
+ while (nextTag(Tags.CALENDAR_ATTENDEE) != END) {
+ switch (tag) {
+ case Tags.CALENDAR_ATTENDEE_EMAIL:
+ cv.put(Attendees.ATTENDEE_EMAIL, getValue());
+ break;
+ case Tags.CALENDAR_ATTENDEE_NAME:
+ cv.put(Attendees.ATTENDEE_NAME, getValue());
+ break;
+ case Tags.CALENDAR_ATTENDEE_STATUS:
+ int status = getValueInt();
+ cv.put(Attendees.ATTENDEE_STATUS,
+ (status == 2) ? Attendees.ATTENDEE_STATUS_TENTATIVE :
+ (status == 3) ? Attendees.ATTENDEE_STATUS_ACCEPTED :
+ (status == 4) ? Attendees.ATTENDEE_STATUS_DECLINED :
+ (status == 5) ? Attendees.ATTENDEE_STATUS_INVITED :
+ Attendees.ATTENDEE_STATUS_NONE);
+ break;
+ case Tags.CALENDAR_ATTENDEE_TYPE:
+ int type = Attendees.TYPE_NONE;
+ // EAS types: 1 = req'd, 2 = opt, 3 = resource
+ switch (getValueInt()) {
+ case 1:
+ type = Attendees.TYPE_REQUIRED;
+ break;
+ case 2:
+ type = Attendees.TYPE_OPTIONAL;
+ break;
+ }
+ cv.put(Attendees.ATTENDEE_TYPE, type);
+ break;
+ default:
+ skipTag();
+ }
+ }
+ cv.put(Attendees.ATTENDEE_RELATIONSHIP, Attendees.RELATIONSHIP_ATTENDEE);
+ if (eventId < 0) {
+ ops.newAttendee(cv);
+ } else {
+ ops.updatedAttendee(cv, eventId);
+ }
+ }
+
+ 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();
+ }
+ }
+
+ // Handle null data without error
+ if (body == null) return "";
+ // Remove \r's from any body text
+ return body.replace("\r\n", "\n");
+ }
+
+ public void addParser(CalendarOperations 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:
+ addEvent(ops, serverId, false);
+ break;
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ private Cursor getServerIdCursor(String serverId) {
+ mBindArgument[0] = serverId;
+ return mContentResolver.query(mAccountUri, ID_PROJECTION, SERVER_ID,
+ mBindArgument, null);
+ }
+
+ private Cursor getClientIdCursor(String clientId) {
+ mBindArgument[0] = clientId;
+ return mContentResolver.query(mAccountUri, ID_PROJECTION, CLIENT_ID_SELECTION,
+ mBindArgument, null);
+ }
+
+ public void deleteParser(CalendarOperations ops) throws IOException {
+ while (nextTag(Tags.SYNC_DELETE) != END) {
+ switch (tag) {
+ case Tags.SYNC_SERVER_ID:
+ String serverId = getValue();
+ // Find the event 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();
+ }
+ }
+ }
+
+ /**
+ * A change is handled as a delete (including all exceptions) and an add
+ * This isn't as efficient as attempting to traverse the original and all of its exceptions,
+ * but changes happen infrequently and this code is both simpler and easier to maintain
+ * @param ops the array of pending ContactProviderOperations.
+ * @throws IOException
+ */
+ public void changeParser(CalendarOperations ops) throws IOException {
+ String serverId = null;
+ while (nextTag(Tags.SYNC_CHANGE) != END) {
+ switch (tag) {
+ case Tags.SYNC_SERVER_ID:
+ serverId = getValue();
+ break;
+ case Tags.SYNC_APPLICATION_DATA:
+ userLog("Changing " + serverId);
+ addEvent(ops, serverId, true);
+ break;
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ @Override
+ public void commandsParser() throws IOException {
+ while (nextTag(Tags.SYNC_COMMANDS) != END) {
+ if (tag == Tags.SYNC_ADD) {
+ addParser(mOps);
+ incrementChangeCount();
+ } else if (tag == Tags.SYNC_DELETE) {
+ deleteParser(mOps);
+ incrementChangeCount();
+ } else if (tag == Tags.SYNC_CHANGE) {
+ changeParser(mOps);
+ incrementChangeCount();
+ } else
+ skipTag();
+ }
+ }
+
+ @Override
+ public void commit() throws IOException {
+ userLog("Calendar SyncKey saved as: ", mMailbox.mSyncKey);
+ // Save the syncKey here, using the Helper provider by Calendar provider
+ mOps.add(SyncStateContract.Helpers.newSetOperation(SyncState.CONTENT_URI,
+ getAccountManagerAccount(), mMailbox.mSyncKey.getBytes()));
+
+ // Execute these all at once...
+ mOps.execute();
+
+ if (mOps.mResults != null) {
+ // Clear dirty flags for Events sent to server
+ ContentValues cv = new ContentValues();
+ cv.put(Events._SYNC_DIRTY, 0);
+ mContentResolver.update(sEventsUri, cv, DIRTY_IN_CALENDAR,
+ new String[] {Long.toString(mCalendarId)});
+ }
+ }
+
+ public void addResponsesParser() throws IOException {
+ String serverId = null;
+ String clientId = null;
+ int status = -1;
+ 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:
+ status = getValueInt();
+ if (status != 1) {
+ userLog("Attempt to add event failed with status: " + status);
+ }
+ break;
+ default:
+ skipTag();
+ }
+ }
+
+ if (clientId == null) return;
+ if (serverId == null) {
+ // TODO Reconsider how to handle this
+ serverId = "FAIL:" + status;
+ }
+
+ Cursor c = getClientIdCursor(clientId);
+ try {
+ if (c.moveToFirst()) {
+ cv.put(Events._SYNC_ID, serverId);
+ mOps.add(ContentProviderOperation.newUpdate(
+ ContentUris.withAppendedId(sEventsUri, c.getLong(0)))
+ .withValues(cv)
+ .build());
+ userLog("New event " + 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 event " + 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 class CalendarOperations extends ArrayList<ContentProviderOperation> {
+ private static final long serialVersionUID = 1L;
+ private int mCount = 0;
+ private ContentProviderResult[] mResults = null;
+ private int mEventStart = 0;
+
+ @Override
+ public boolean add(ContentProviderOperation op) {
+ super.add(op);
+ mCount++;
+ return true;
+ }
+
+ public int newEvent(ContentProviderOperation op) {
+ mEventStart = mCount;
+ add(op);
+ return mEventStart;
+ }
+
+ public void newAttendee(ContentValues cv) {
+ add(ContentProviderOperation
+ .newInsert(sAttendeesUri)
+ .withValues(cv)
+ .withValueBackReference(Attendees.EVENT_ID, mEventStart)
+ .build());
+ }
+
+ public void updatedAttendee(ContentValues cv, long id) {
+ cv.put(Attendees.EVENT_ID, id);
+ add(ContentProviderOperation.newInsert(sAttendeesUri).withValues(cv).build());
+ }
+
+ public void newException(ContentValues cv) {
+ add(ContentProviderOperation.newInsert(sEventsUri).withValues(cv).build());
+ }
+
+ public void newExtendedProperty(String name, String value) {
+ add(ContentProviderOperation
+ .newInsert(sExtendedPropertiesUri)
+ .withValue(ExtendedProperties.NAME, name)
+ .withValue(ExtendedProperties.VALUE, value)
+ .withValueBackReference(ExtendedProperties.EVENT_ID, mEventStart)
+ .build());
+ }
+
+ public void newReminder(int mins) {
+ add(ContentProviderOperation
+ .newInsert(sRemindersUri)
+ .withValue(Reminders.MINUTES, mins)
+ .withValue(Reminders.METHOD, Reminders.METHOD_DEFAULT)
+ .withValueBackReference(ExtendedProperties.EVENT_ID, mEventStart)
+ .build());
+ }
+
+ public void delete(long id) {
+ add(ContentProviderOperation
+ .newDelete(ContentUris.withAppendedId(sEventsUri, id)).build());
+ }
+
+ public void execute() {
+ synchronized (mService.getSynchronizer()) {
+ if (!mService.isStopped()) {
+ try {
+ if (!isEmpty()) {
+ mService.userLog("Executing ", size(), " CPO's");
+ mResults = mContext.getContentResolver().applyBatch(
+ Calendar.AUTHORITY, this);
+ }
+ } catch (RemoteException e) {
+ // There is nothing sensible to be done here
+ Log.e(TAG, "problem inserting event during server update", e);
+ } catch (OperationApplicationException e) {
+ // There is nothing sensible to be done here
+ Log.e(TAG, "problem inserting event during server update", e);
+ }
+ }
+ }
+ }
+ }
+
+ private String decodeVisibility(int visibility) {
+ int easVisibility = 0;
+ switch(visibility) {
+ case Events.VISIBILITY_DEFAULT:
+ easVisibility = 0;
+ break;
+ case Events.VISIBILITY_PUBLIC:
+ easVisibility = 1;
+ break;
+ case Events.VISIBILITY_PRIVATE:
+ easVisibility = 2;
+ break;
+ case Events.VISIBILITY_CONFIDENTIAL:
+ easVisibility = 3;
+ break;
+ }
+ return Integer.toString(easVisibility);
+ }
+
+ private void sendEvent(Entity entity, String clientId, Serializer s)
+ throws IOException {
+ // Serialize for EAS here
+ // Set uid with the client id we created
+ // 1) Serialize the top-level event
+ // 2) Serialize attendees and reminders from subvalues
+ // 3) Look for exceptions and serialize with the top-level event
+ ContentValues entityValues = entity.getEntityValues();
+ boolean isException = (clientId == null);
+
+ if (entityValues.containsKey(Events.ALL_DAY)) {
+ s.data(Tags.CALENDAR_ALL_DAY_EVENT,
+ entityValues.getAsInteger(Events.ALL_DAY).toString());
+ }
+
+ long startTime = entityValues.getAsLong(Events.DTSTART);
+ s.data(Tags.CALENDAR_START_TIME,
+ CalendarUtilities.millisToEasDateTime(startTime));
+
+ if (!entityValues.containsKey(Events.DURATION)) {
+ if (entityValues.containsKey(Events.DTEND)) {
+ s.data(Tags.CALENDAR_END_TIME, CalendarUtilities.millisToEasDateTime(
+ entityValues.getAsLong(Events.DTEND)));
+ }
+ } else {
+ // Convert this into millis and add it to DTSTART for DTEND
+ // We'll use 1 hour as a default
+ long durationMillis = HOURS;
+ Duration duration = new Duration();
+ try {
+ duration.parse(entityValues.getAsString(Events.DURATION));
+ } catch (DateException e) {
+ // Can't do much about this; use the default (1 hour)
+ }
+ s.data(Tags.CALENDAR_END_TIME,
+ CalendarUtilities.millisToEasDateTime(startTime + durationMillis));
+ }
+
+ s.data(Tags.CALENDAR_DTSTAMP,
+ CalendarUtilities.millisToEasDateTime(System.currentTimeMillis()));
+
+ s.writeStringValue(entityValues, Events.EVENT_LOCATION, Tags.CALENDAR_LOCATION);
+ s.writeStringValue(entityValues, Events.TITLE, Tags.CALENDAR_SUBJECT);
+
+ Integer visibility = entityValues.getAsInteger(Events.VISIBILITY);
+ if (visibility != null) {
+ s.data(Tags.CALENDAR_SENSITIVITY, decodeVisibility(visibility));
+ } else {
+ // Default to private if not set
+ s.data(Tags.CALENDAR_SENSITIVITY, "1");
+ }
+
+ if (!isException) {
+ // A time zone is required in all EAS events; we'll use the default if none is set
+ String timeZoneName = entityValues.getAsString(Events.EVENT_TIMEZONE);
+ if (timeZoneName == null) {
+ timeZoneName = TimeZone.getDefault().getID();
+ }
+ String timeZone = CalendarUtilities.timeZoneToTziString(
+ TimeZone.getTimeZone(timeZoneName));
+ s.data(Tags.CALENDAR_TIME_ZONE, timeZone);
+
+ String desc = entityValues.getAsString(Events.DESCRIPTION);
+ if (desc != null) {
+ if (mService.mProtocolVersionDouble >= 12.0) {
+ s.start(Tags.BASE_BODY);
+ s.data(Tags.BASE_TYPE, "1");
+ s.data(Tags.BASE_DATA, desc);
+ s.end();
+ } else {
+ s.data(Tags.CALENDAR_BODY, desc);
+ }
+ }
+
+ s.writeStringValue(entityValues, Events.ORGANIZER, Tags.CALENDAR_ORGANIZER_EMAIL);
+
+ String rrule = entityValues.getAsString(Events.RRULE);
+ if (rrule != null) {
+ CalendarUtilities.recurrenceFromRrule(rrule, startTime, s);
+ }
+
+ // Handle associated data EXCEPT for attendees, which have to be grouped
+ ArrayList<NamedContentValues> subValues = entity.getSubValues();
+ for (NamedContentValues ncv: subValues) {
+ Uri ncvUri = ncv.uri;
+ ContentValues ncvValues = ncv.values;
+ if (ncvUri.equals(ExtendedProperties.CONTENT_URI)) {
+ String uid = ncvValues.getAsString("uid");
+ if (uid != null) {
+ clientId = uid;
+ s.data(Tags.CALENDAR_UID, clientId);
+ }
+ s.writeStringValue(ncvValues, "dtstamp", Tags.CALENDAR_DTSTAMP);
+ String categories = ncvValues.getAsString("categories");
+ if (categories != null) {
+ // Send all the categories back to the server
+ // We've saved them as a String of delimited tokens
+ StringTokenizer st =
+ new StringTokenizer(categories, CATEGORY_TOKENIZER_DELIMITER);
+ if (st.countTokens() > 0) {
+ s.start(Tags.CALENDAR_CATEGORIES);
+ while (st.hasMoreTokens()) {
+ String category = st.nextToken();
+ s.data(Tags.CALENDAR_CATEGORY, category);
+ }
+ s.end();
+ }
+ }
+ } else if (ncvUri.equals(Reminders.CONTENT_URI)) {
+ s.writeStringValue(ncvValues, Reminders.MINUTES,
+ Tags.CALENDAR_REMINDER_MINS_BEFORE);
+ }
+ }
+
+ // We've got to send a UID. If the event is new, we've generated one; if not,
+ // we should have gotten one from extended properties.
+ s.data(Tags.CALENDAR_UID, clientId);
+
+ // Handle attendee data here; keep track of organizer and stream it afterward
+ boolean hasAttendees = false;
+ String organizerName = null;
+ for (NamedContentValues ncv: subValues) {
+ Uri ncvUri = ncv.uri;
+ ContentValues ncvValues = ncv.values;
+ if (ncvUri.equals(Attendees.CONTENT_URI)) {
+ Integer relationship = ncvValues.getAsInteger(Attendees.ATTENDEE_RELATIONSHIP);
+ if (relationship != null) {
+ // Organizer isn't among attendees in EAS
+ if (relationship == Attendees.RELATIONSHIP_ORGANIZER) {
+ organizerName = ncvValues.getAsString(Attendees.ATTENDEE_NAME);
+ continue;
+ }
+ if (!hasAttendees) {
+ s.start(Tags.CALENDAR_ATTENDEES);
+ hasAttendees = true;
+ }
+ s.start(Tags.CALENDAR_ATTENDEE);
+ s.writeStringValue(ncvValues, Attendees.ATTENDEE_NAME,
+ Tags.CALENDAR_ATTENDEE_NAME);
+ s.writeStringValue(ncvValues, Attendees.ATTENDEE_EMAIL,
+ Tags.CALENDAR_ATTENDEE_EMAIL);
+ s.data(Tags.CALENDAR_ATTENDEE_TYPE, "1"); // Required
+ s.end(); // Attendee
+ }
+ // If there's no relationship, we can't create this for EAS
+ }
+ }
+ if (hasAttendees) {
+ s.end(); // Attendees
+ }
+ if (organizerName != null) {
+ s.data(Tags.CALENDAR_ORGANIZER_NAME, organizerName);
+ }
+ } else {
+ // TODO Add reminders to exceptions (allow them to be specified!)
+ Long originalTime = entityValues.getAsLong(Events.ORIGINAL_INSTANCE_TIME);
+ if (originalTime != null) {
+ s.data(Tags.CALENDAR_EXCEPTION_START_TIME,
+ CalendarUtilities.millisToEasDateTime(originalTime));
+ } else {
+ // Illegal; what should we do?
+ }
+ }
+ }
+
+ @Override
+ public boolean sendLocalChanges(Serializer s) throws IOException {
+ ContentResolver cr = mService.mContentResolver;
+ Uri uri = Events.CONTENT_URI.buildUpon()
+ .appendQueryParameter(Calendar.CALLER_IS_SYNCADAPTER, "true")
+ .build();
+
+ if (getSyncKey().equals("0")) {
+ return false;
+ }
+
+ try {
+ // We've got to handle exceptions as part of the parent when changes occur, so we need
+ // to find new/changed exceptions and mark the parent dirty
+ Cursor c = cr.query(Events.CONTENT_URI, ORIGINAL_EVENT_PROJECTION, DIRTY_EXCEPTION,
+ null, null);
+ try {
+ ContentValues cv = new ContentValues();
+ cv.put(Events._SYNC_DIRTY, 1);
+ // Mark the parent dirty in this loop
+ while (c.moveToNext()) {
+ String serverId = c.getString(0);
+ cr.update(asSyncAdapter(Events.CONTENT_URI), cv, SERVER_ID,
+ new String[] {serverId});
+ }
+ } finally {
+ c.close();
+ }
+
+ // Now we can go through dirty top-level events and send them back to the server
+ EntityIterator eventIterator = EventsEntity.newEntityIterator(
+ cr.query(uri, null, DIRTY_TOP_LEVEL, null, null), cr);
+ ContentValues cidValues = new ContentValues();
+ try {
+ boolean first = true;
+ while (eventIterator.hasNext()) {
+ Entity entity = eventIterator.next();
+ String clientId = "uid_" + mMailbox.mId + '_' + System.currentTimeMillis();
+
+ // For each of these entities, create the change commands
+ ContentValues entityValues = entity.getEntityValues();
+ String serverId = entityValues.getAsString(Events._SYNC_ID);
+
+ // EAS 2.5 needs: BusyStatus DtStamp EndTime Sensitivity StartTime TimeZone UID
+ // We can generate all but what we're testing for below
+ if (!entityValues.containsKey(Events.DTSTART)
+ || (!entityValues.containsKey(Events.DURATION) &&
+ !entityValues.containsKey(Events.DTEND))) {
+ continue;
+ }
+ // TODO Handle BusyStatus for EAS 2.5
+ // What should it be??
+
+ if (first) {
+ s.start(Tags.SYNC_COMMANDS);
+ userLog("Sending Calendar changes to the server");
+ first = false;
+ }
+ if (serverId == null) {
+ // This is a new event; create a clientId
+ userLog("Creating new event with clientId: ", clientId);
+ s.start(Tags.SYNC_ADD).data(Tags.SYNC_CLIENT_ID, clientId);
+ // And save it in the Event as the local id
+ cidValues.put(Events._SYNC_LOCAL_ID, clientId);
+ cr.update(ContentUris.
+ withAppendedId(uri,
+ entityValues.getAsLong(Events._ID)),
+ cidValues, null, null);
+ } else {
+ if (entityValues.getAsInteger(Events.DELETED) == 1) {
+ userLog("Deleting event with serverId: ", serverId);
+ s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
+ mDeletedIdList.add(entityValues.getAsLong(Events._ID));
+ continue;
+ }
+ userLog("Upsync change to event with serverId: " + serverId);
+ s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, serverId);
+ }
+ s.start(Tags.SYNC_APPLICATION_DATA);
+ sendEvent(entity, clientId, s);
+
+ // Now, the hard part; find exceptions for this event
+ if (serverId != null) {
+ EntityIterator exceptionIterator = EventsEntity.newEntityIterator(
+ cr.query(uri, null, Events.ORIGINAL_EVENT + "=?",
+ new String[] {serverId}, null), cr);
+ boolean exFirst = true;
+ while (exceptionIterator.hasNext()) {
+ Entity exceptionEntity = exceptionIterator.next();
+ if (exFirst) {
+ s.start(Tags.CALENDAR_EXCEPTIONS);
+ exFirst = false;
+ }
+ s.start(Tags.CALENDAR_EXCEPTION);
+ sendEvent(exceptionEntity, null, s);
+ s.end(); // EXCEPTION
+ }
+ if (!exFirst) {
+ s.end(); // EXCEPTIONS
+ }
+ }
+
+ s.end().end(); // ApplicationData & Change
+ mUpdatedIdList.add(entityValues.getAsLong(Events._ID));
+ }
+ if (!first) {
+ s.end(); // Commands
+ }
+ } finally {
+ eventIterator.close();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Could not read dirty events.");
+ }
+
return false;
}
}
diff --git a/src/com/android/exchange/adapter/ContactsSyncAdapter.java b/src/com/android/exchange/adapter/ContactsSyncAdapter.java
index fff6400..b95e475 100644
--- a/src/com/android/exchange/adapter/ContactsSyncAdapter.java
+++ b/src/com/android/exchange/adapter/ContactsSyncAdapter.java
@@ -41,6 +41,7 @@
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.RawContactsEntity;
import android.provider.ContactsContract.Settings;
import android.provider.ContactsContract.SyncState;
import android.provider.ContactsContract.CommonDataKinds.Email;
@@ -162,7 +163,7 @@
// 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(Groups.ACCOUNT_TYPE, com.android.email.Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
cv.put(Settings.UNGROUPED_VISIBLE, true);
client.insert(addCallerIsSyncAdapterParameter(Settings.CONTENT_URI), cv);
return "0";
@@ -197,8 +198,8 @@
public android.accounts.Account getAccountManagerAccount() {
if (mAccountManagerAccount == null) {
- mAccountManagerAccount =
- new android.accounts.Account(mAccount.mEmailAddress, Eas.ACCOUNT_MANAGER_TYPE);
+ mAccountManagerAccount = new android.accounts.Account(mAccount.mEmailAddress,
+ com.android.email.Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
}
return mAccountManagerAccount;
}
@@ -741,16 +742,19 @@
try {
if (c.moveToFirst()) {
// TODO Handle deleted individual rows...
+ Uri uri = ContentUris.withAppendedId(
+ RawContacts.CONTENT_URI, c.getLong(0));
+ uri = Uri.withAppendedPath(
+ uri, RawContacts.Entity.CONTENT_DIRECTORY);
+ EntityIterator entityIterator = RawContacts.newEntityIterator(
+ mContentResolver.query(uri, null, null, null, null));
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) {
+ // TODO: log the fact that we failed to read the entity
}
}
} finally {
@@ -889,7 +893,8 @@
private Uri uriWithAccountAndIsSyncAdapter(Uri uri) {
return uri.buildUpon()
.appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress)
- .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE)
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE,
+ com.android.email.Email.EXCHANGE_ACCOUNT_MANAGER_TYPE)
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.build();
}
@@ -1628,6 +1633,15 @@
}
}
+ private void sendPhoto(Serializer s, ContentValues cv) throws IOException {
+ if (cv.containsKey(Photo.PHOTO)) {
+ byte[] bytes = cv.getAsByteArray(Photo.PHOTO);
+ byte[] encodedBytes = Base64.encodeBase64(bytes);
+ String pic = new String(encodedBytes);
+ s.data(Tags.CONTACTS_PICTURE, pic);
+ }
+ }
+
private void sendOrganization(Serializer s, ContentValues cv) throws IOException {
if (cv.containsKey(Organization.TITLE)) {
s.data(Tags.CONTACTS_JOB_TITLE, cv.getAsString(Organization.TITLE));
@@ -1756,9 +1770,10 @@
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()
+ Uri uri = RawContactsEntity.CONTENT_URI.buildUpon()
.appendQueryParameter(RawContacts.ACCOUNT_NAME, mAccount.mEmailAddress)
- .appendQueryParameter(RawContacts.ACCOUNT_TYPE, Eas.ACCOUNT_MANAGER_TYPE)
+ .appendQueryParameter(RawContacts.ACCOUNT_TYPE,
+ com.android.email.Email.EXCHANGE_ACCOUNT_MANAGER_TYPE)
.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true")
.build();
@@ -1768,7 +1783,8 @@
try {
// Get them all atomically
- EntityIterator ei = cr.queryEntities(uri, RawContacts.DIRTY + "=1", null, null);
+ EntityIterator ei = RawContacts.newEntityIterator(
+ cr.query(uri, null, RawContacts.DIRTY + "=1", null, null));
ContentValues cidValues = new ContentValues();
try {
boolean first = true;
@@ -1854,8 +1870,7 @@
} 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.
+ sendPhoto(s, cv);
} else {
userLog("Contacts upsync, unknown data: ", mimeType);
}
@@ -1866,7 +1881,7 @@
for (ContentValues cv: emailValues) {
sendEmail(s, cv, emailCount++, displayName);
}
-
+
// Now, we'll send up groups, if any
if (!groupIds.isEmpty()) {
boolean groupFirst = true;
diff --git a/src/com/android/exchange/adapter/EmailSyncAdapter.java b/src/com/android/exchange/adapter/EmailSyncAdapter.java
index 946ae4e..c835f85 100644
--- a/src/com/android/exchange/adapter/EmailSyncAdapter.java
+++ b/src/com/android/exchange/adapter/EmailSyncAdapter.java
@@ -63,6 +63,9 @@
private static final String[] UPDATES_PROJECTION =
{MessageColumns.FLAG_READ, MessageColumns.MAILBOX_KEY, SyncColumns.SERVER_ID,
MessageColumns.FLAG_FAVORITE};
+
+ private static final int MESSAGE_ID_SUBJECT_ID_COLUMN = 0;
+ private static final int MESSAGE_ID_SUBJECT_SUBJECT_COLUMN = 1;
private static final String[] MESSAGE_ID_SUBJECT_PROJECTION =
new String[] { Message.RECORD_ID, MessageColumns.SUBJECT };
@@ -163,6 +166,14 @@
String text = getValue();
msg.mText = text;
break;
+ case Tags.EMAIL_MESSAGE_CLASS:
+ String messageClass = getValue();
+ if (messageClass.equals("IPM.Schedule.Meeting.Request")) {
+ msg.mFlags |= Message.FLAG_MEETING_INVITE;
+ } else if (messageClass.equals("IPM.Schedule.Meeting.Canceled")) {
+ msg.mFlags |= Message.FLAG_MEETING_CANCEL_NOTICE;
+ }
+ break;
default:
skipTag();
}
@@ -317,7 +328,7 @@
WHERE_SERVER_ID_AND_MAILBOX_KEY, mBindArguments, null);
}
- private void deleteParser(ArrayList<Long> deletes, int entryTag) throws IOException {
+ /*package*/ void deleteParser(ArrayList<Long> deletes, int entryTag) throws IOException {
while (nextTag(entryTag) != END) {
switch (tag) {
case Tags.SYNC_SERVER_ID:
@@ -326,9 +337,10 @@
Cursor c = getServerIdCursor(serverId, MESSAGE_ID_SUBJECT_PROJECTION);
try {
if (c.moveToFirst()) {
- deletes.add(c.getLong(0));
+ deletes.add(c.getLong(MESSAGE_ID_SUBJECT_ID_COLUMN));
if (Eas.USER_LOG) {
- userLog("Deleting ", serverId + ", " + c.getString(1));
+ userLog("Deleting ", serverId + ", "
+ + c.getString(MESSAGE_ID_SUBJECT_SUBJECT_COLUMN));
}
}
} finally {
@@ -353,7 +365,7 @@
}
}
- private void changeParser(ArrayList<ServerChange> changes) throws IOException {
+ /*package*/ void changeParser(ArrayList<ServerChange> changes) throws IOException {
String serverId = null;
Boolean oldRead = false;
Boolean oldFlag = false;
diff --git a/src/com/android/exchange/adapter/FolderSyncParser.java b/src/com/android/exchange/adapter/FolderSyncParser.java
index 337e329..b760dfd 100644
--- a/src/com/android/exchange/adapter/FolderSyncParser.java
+++ b/src/com/android/exchange/adapter/FolderSyncParser.java
@@ -17,6 +17,7 @@
package com.android.exchange.adapter;
+import com.android.email.Email;
import com.android.email.provider.AttachmentProvider;
import com.android.email.provider.EmailContent;
import com.android.email.provider.EmailProvider;
@@ -33,7 +34,10 @@
import android.content.ContentValues;
import android.content.OperationApplicationException;
import android.database.Cursor;
+import android.net.Uri;
import android.os.RemoteException;
+import android.provider.Calendar.Calendars;
+import android.text.format.Time;
import java.io.IOException;
import java.io.InputStream;
@@ -239,7 +243,28 @@
break;
case CALENDAR_TYPE:
m.mType = Mailbox.TYPE_CALENDAR;
- // For now, no sync, since it's not yet implemented
+ m.mSyncInterval = mAccount.mSyncInterval;
+
+ // Create a Calendar object
+ ContentValues cv = new ContentValues();
+ // TODO How will this change if the user changes his account display name?
+ cv.put(Calendars.DISPLAY_NAME, mAccount.mDisplayName);
+ cv.put(Calendars._SYNC_ACCOUNT, mAccount.mEmailAddress);
+ cv.put(Calendars._SYNC_ACCOUNT_TYPE, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+ cv.put(Calendars.SYNC_EVENTS, 1);
+ cv.put(Calendars.SELECTED, 1);
+ cv.put(Calendars.HIDDEN, 0);
+ // TODO Find out how to set color!!
+ cv.put(Calendars.COLOR, 0xFF228B22 /*green*/);
+ cv.put(Calendars.TIMEZONE, Time.getCurrentTimezone());
+ cv.put(Calendars.ACCESS_LEVEL, Calendars.OWNER_ACCESS);
+ cv.put(Calendars.OWNER_ACCOUNT, mAccount.mEmailAddress);
+
+ Uri uri = mService.mContentResolver.insert(Calendars.CONTENT_URI, cv);
+ // We save the id of the calendar into mSyncStatus
+ if (uri != null) {
+ m.mSyncStatus = uri.getPathSegments().get(1);
+ }
break;
}
diff --git a/src/com/android/exchange/adapter/MeetingResponseParser.java b/src/com/android/exchange/adapter/MeetingResponseParser.java
new file mode 100644
index 0000000..142c419
--- /dev/null
+++ b/src/com/android/exchange/adapter/MeetingResponseParser.java
@@ -0,0 +1,65 @@
+/* Copyright (C) 2010 The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.adapter;
+
+import com.android.exchange.EasSyncService;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Parse the result of a MeetingRequest command.
+ */
+public class MeetingResponseParser extends Parser {
+ private EasSyncService mService;
+
+ public MeetingResponseParser(InputStream in, EasSyncService service) throws IOException {
+ super(in);
+ mService = service;
+ }
+
+ public void parseResult() throws IOException {
+ while (nextTag(Tags.MREQ_RESULT) != END) {
+ if (tag == Tags.MREQ_STATUS) {
+ int status = getValueInt();
+ if (status != 1) {
+ mService.userLog("Error in meeting response: " + status);
+ }
+ } else if (tag == Tags.MREQ_CAL_ID) {
+ mService.userLog("Meeting response calendar id: " + getValue());
+ } else {
+ skipTag();
+ }
+ }
+ }
+
+ @Override
+ public boolean parse() throws IOException {
+ boolean res = false;
+ if (nextTag(START_DOCUMENT) != Tags.MREQ_MEETING_RESPONSE) {
+ throw new IOException();
+ }
+ while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
+ if (tag == Tags.MREQ_RESULT) {
+ parseResult();
+ } else {
+ skipTag();
+ }
+ }
+ return res;
+ }
+}
+
diff --git a/src/com/android/exchange/adapter/Parser.java b/src/com/android/exchange/adapter/Parser.java
index ca99a77..57e3f50 100644
--- a/src/com/android/exchange/adapter/Parser.java
+++ b/src/com/android/exchange/adapter/Parser.java
@@ -38,8 +38,6 @@
*/
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;
@@ -52,6 +50,10 @@
private static final int EOF_BYTE = -1;
private boolean logging = false;
private boolean capture = false;
+ private String logTag = "EAS Parser";
+
+ // Where tags start in a page
+ private static final int TAG_BASE = 5;
private ArrayList<Integer> captureArray;
@@ -158,6 +160,16 @@
}
/**
+ * Set the tag used for logging. When debugging is on, every token is logged (Log.v) to
+ * the console.
+ *
+ * @param val the logging tag
+ */
+ public void setLoggingTag(String val) {
+ logTag = val;
+ }
+
+ /**
* Turns on data capture; this is used to create test streams that represent "live" data and
* can be used against the various parsers.
*/
@@ -190,6 +202,13 @@
public String getValue() throws IOException {
// The false argument tells getNext to return the value as a String
getNext(false);
+ // This means there was no value given, just <Foo/>; we'll return empty string for now
+ if (type == END) {
+ if (logging) {
+ log("No value for tag: " + tagTable[startTag - TAG_BASE]);
+ }
+ return "";
+ }
// Save the value
String val = text;
// Read the next token; it had better be the end of the current tag
@@ -211,6 +230,9 @@
public int getValueInt() throws IOException {
// The true argument to getNext indicates the desire for an integer return value
getNext(true);
+ if (type == END) {
+ return 0;
+ }
// Save the value
int val = num;
// Read the next token; it had better be the end of the current tag
@@ -304,14 +326,18 @@
tagTable = tagTables[0];
}
+ /*package*/ void resetInput(InputStream in) {
+ this.in = in;
+ }
+
void log(String str) {
int cr = str.indexOf('\n');
if (cr > 0) {
str = str.substring(0, cr);
}
- Log.v(TAG, str);
+ Log.v(logTag, str);
if (Eas.FILE_LOG) {
- FileLogger.log(TAG, str);
+ FileLogger.log(logTag, str);
}
}
@@ -381,7 +407,7 @@
text = readInlineString();
}
if (logging) {
- name = tagTable[startTag - 5];
+ name = tagTable[startTag - TAG_BASE];
log(name + ": " + (asInt ? Integer.toString(num) : text));
}
break;
@@ -395,7 +421,7 @@
noContent = (id & 0x40) == 0;
depth++;
if (logging) {
- name = tagTable[startTag - 5];
+ name = tagTable[startTag - TAG_BASE];
//log('<' + name + '>');
nameArray[depth] = name;
}
diff --git a/src/com/android/exchange/adapter/ProvisionParser.java b/src/com/android/exchange/adapter/ProvisionParser.java
new file mode 100644
index 0000000..ffa1e45
--- /dev/null
+++ b/src/com/android/exchange/adapter/ProvisionParser.java
@@ -0,0 +1,178 @@
+/* Copyright (C) 2010 The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.adapter;
+
+import com.android.email.SecurityPolicy;
+import com.android.email.SecurityPolicy.PolicySet;
+import com.android.exchange.EasSyncService;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Parse the result of the Provision command
+ *
+ * Assuming a successful parse, we store the PolicySet and the policy key
+ */
+public class ProvisionParser extends Parser {
+ private EasSyncService mService;
+ PolicySet mPolicySet = null;
+ String mPolicyKey = null;
+
+ public ProvisionParser(InputStream in, EasSyncService service) throws IOException {
+ super(in);
+ mService = service;
+ setDebug(true);
+ }
+
+ public PolicySet getPolicySet() {
+ return mPolicySet;
+ }
+
+ public String getPolicyKey() {
+ return mPolicyKey;
+ }
+
+ public void parseProvisionDoc() throws IOException {
+ int minPasswordLength = 0;
+ int passwordMode = PolicySet.PASSWORD_MODE_NONE;
+ int maxPasswordFails = 0;
+ int maxScreenLockTime = 0;
+ boolean canSupport = false;
+
+ while (nextTag(Tags.PROVISION_EAS_PROVISION_DOC) != END) {
+ switch (tag) {
+ case Tags.PROVISION_DEVICE_PASSWORD_ENABLED:
+ if (getValueInt() == 1) {
+ if (passwordMode == PolicySet.PASSWORD_MODE_NONE) {
+ passwordMode = PolicySet.PASSWORD_MODE_SIMPLE;
+ }
+ }
+ break;
+ case Tags.PROVISION_MIN_DEVICE_PASSWORD_LENGTH:
+ minPasswordLength = getValueInt();
+ break;
+ case Tags.PROVISION_ALPHA_DEVICE_PASSWORD_ENABLED:
+ if (getValueInt() == 1) {
+ passwordMode = PolicySet.PASSWORD_MODE_STRONG;
+ }
+ break;
+ case Tags.PROVISION_MAX_INACTIVITY_TIME_DEVICE_LOCK:
+ // EAS gives us seconds, which is, happily, what the PolicySet requires
+ maxScreenLockTime = getValueInt();
+ break;
+ case Tags.PROVISION_MAX_DEVICE_PASSWORD_FAILED_ATTEMPTS:
+ maxPasswordFails = getValueInt();
+ break;
+ case Tags.PROVISION_ALLOW_SIMPLE_DEVICE_PASSWORD:
+ // Ignore this unless there's any MSFT documentation for what this means
+ // Hint: I haven't seen any that's more specific than "simple"
+ getValue();
+ break;
+ // The following policy, if false, can't be supported at the moment
+ case Tags.PROVISION_ATTACHMENTS_ENABLED:
+ if (getValueInt() == 0) {
+ canSupport = false;
+ }
+ break;
+ // The following policies, if true, can't be supported at the moment
+ case Tags.PROVISION_DEVICE_ENCRYPTION_ENABLED:
+ case Tags.PROVISION_PASSWORD_RECOVERY_ENABLED:
+ case Tags.PROVISION_DEVICE_PASSWORD_EXPIRATION:
+ case Tags.PROVISION_DEVICE_PASSWORD_HISTORY:
+ case Tags.PROVISION_MAX_ATTACHMENT_SIZE:
+ if (getValueInt() == 1) {
+ canSupport = false;
+ }
+ break;
+ default:
+ skipTag();
+ }
+ }
+
+ if (canSupport) {
+ mPolicySet = new SecurityPolicy.PolicySet(minPasswordLength, passwordMode,
+ maxPasswordFails, maxScreenLockTime, true);
+ }
+ }
+
+ public void parseProvisionData() throws IOException {
+ while (nextTag(Tags.PROVISION_DATA) != END) {
+ if (tag == Tags.PROVISION_EAS_PROVISION_DOC) {
+ parseProvisionDoc();
+ } else {
+ skipTag();
+ }
+ }
+ }
+
+ public void parsePolicy() throws IOException {
+ while (nextTag(Tags.PROVISION_POLICY) != END) {
+ switch (tag) {
+ case Tags.PROVISION_POLICIES:
+ parsePolicies();
+ break;
+ case Tags.PROVISION_POLICY_TYPE:
+ mService.userLog("Policy type: ", getValue());
+ break;
+ case Tags.PROVISION_POLICY_KEY:
+ mPolicyKey = getValue();
+ break;
+ case Tags.PROVISION_STATUS:
+ mService.userLog("Policy status: ", getValue());
+ break;
+ case Tags.PROVISION_DATA:
+ parseProvisionData();
+ break;
+ default:
+ skipTag();
+ }
+ }
+ }
+
+ public void parsePolicies() throws IOException {
+ while (nextTag(Tags.PROVISION_POLICIES) != END) {
+ if (tag == Tags.PROVISION_POLICY) {
+ parsePolicy();
+ } else {
+ skipTag();
+ }
+ }
+ }
+
+ @Override
+ public boolean parse() throws IOException {
+ boolean res = false;
+ if (nextTag(START_DOCUMENT) != Tags.PROVISION_PROVISION) {
+ throw new IOException();
+ }
+ while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
+ switch (tag) {
+ case Tags.PROVISION_STATUS:
+ int status = getValueInt();
+ mService.userLog("Provision status: ", status);
+ break;
+ case Tags.PROVISION_POLICIES:
+ parsePolicies();
+ return (mPolicySet != null);
+ default:
+ skipTag();
+ }
+ }
+ return res;
+ }
+}
+
diff --git a/src/com/android/exchange/adapter/Serializer.java b/src/com/android/exchange/adapter/Serializer.java
index f314772..8cdb99b 100644
--- a/src/com/android/exchange/adapter/Serializer.java
+++ b/src/com/android/exchange/adapter/Serializer.java
@@ -26,6 +26,7 @@
import com.android.exchange.Eas;
import com.android.exchange.utility.FileLogger;
+import android.content.ContentValues;
import android.util.Log;
import java.io.ByteArrayOutputStream;
@@ -54,12 +55,20 @@
private int tagPage;
public Serializer() {
+ this(true);
+ }
+
+ public Serializer(boolean startDocument) {
super();
- try {
- startDocument();
- //logging = Eas.PARSER_LOG;
- } catch (IOException e) {
- // Nothing to be done
+ if (startDocument) {
+ try {
+ startDocument();
+ //logging = Eas.PARSER_LOG;
+ } catch (IOException e) {
+ // Nothing to be done
+ }
+ } else {
+ out.write(0);
}
}
@@ -185,4 +194,11 @@
out.write(data);
out.write(0);
}
+
+ void writeStringValue (ContentValues cv, String key, int tag) throws IOException {
+ String value = cv.getAsString(key);
+ if (value != null) {
+ data(tag, value);
+ }
+ }
}
diff --git a/src/com/android/exchange/adapter/Tags.java b/src/com/android/exchange/adapter/Tags.java
index d822181..c2385d5 100644
--- a/src/com/android/exchange/adapter/Tags.java
+++ b/src/com/android/exchange/adapter/Tags.java
@@ -39,9 +39,11 @@
public static final int MOVE = 0x05;
public static final int GIE = 0x06;
public static final int FOLDER = 0x07;
+ public static final int MREQ = 0x08;
public static final int TASK = 0x09;
public static final int CONTACTS2 = 0x0C;
public static final int PING = 0x0D;
+ public static final int PROVISION = 0x0E;
public static final int GAL = 0x10;
public static final int BASE = 0x11;
@@ -218,6 +220,17 @@
public static final int FOLDER_COUNT = FOLDER_PAGE + 0x17;
public static final int FOLDER_VERSION = FOLDER_PAGE + 0x18;
+ public static final int MREQ_PAGE = MREQ << PAGE_SHIFT;
+ public static final int MREQ_CAL_ID = MREQ_PAGE + 5;
+ public static final int MREQ_COLLECTION_ID = MREQ_PAGE + 6;
+ public static final int MREQ_MEETING_RESPONSE = MREQ_PAGE + 7;
+ public static final int MREQ_REQ_ID = MREQ_PAGE + 8;
+ public static final int MREQ_REQUEST = MREQ_PAGE + 9;
+ public static final int MREQ_RESULT = MREQ_PAGE + 0xA;
+ public static final int MREQ_STATUS = MREQ_PAGE + 0xB;
+ public static final int MREQ_USER_RESPONSE = MREQ_PAGE + 0xC;
+ public static final int MREQ_VERSION = MREQ_PAGE + 0xD;
+
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;
@@ -333,7 +346,6 @@
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;
@@ -345,6 +357,66 @@
public static final int PING_CLASS = PING_PAGE + 0xC;
public static final int PING_MAX_FOLDERS = PING_PAGE + 0xD;
+ public static final int PROVISION_PAGE = PROVISION << PAGE_SHIFT;
+ // EAS 2.5
+ public static final int PROVISION_PROVISION = PROVISION_PAGE + 5;
+ public static final int PROVISION_POLICIES = PROVISION_PAGE + 6;
+ public static final int PROVISION_POLICY = PROVISION_PAGE + 7;
+ public static final int PROVISION_POLICY_TYPE = PROVISION_PAGE + 8;
+ public static final int PROVISION_POLICY_KEY = PROVISION_PAGE + 9;
+ public static final int PROVISION_DATA = PROVISION_PAGE + 0xA;
+ public static final int PROVISION_STATUS = PROVISION_PAGE + 0xB;
+ public static final int PROVISION_REMOTE_WIPE = PROVISION_PAGE + 0xC;
+ // EAS 12.0
+ public static final int PROVISION_EAS_PROVISION_DOC = PROVISION_PAGE + 0xD;
+ public static final int PROVISION_DEVICE_PASSWORD_ENABLED = PROVISION_PAGE + 0xE;
+ public static final int PROVISION_ALPHA_DEVICE_PASSWORD_ENABLED = PROVISION_PAGE + 0xF;
+ public static final int PROVISION_DEVICE_ENCRYPTION_ENABLED = PROVISION_PAGE + 0x10;
+ public static final int PROVISION_PASSWORD_RECOVERY_ENABLED = PROVISION_PAGE + 0x11;
+ public static final int PROVISION_ATTACHMENTS_ENABLED = PROVISION_PAGE + 0x13;
+ public static final int PROVISION_MIN_DEVICE_PASSWORD_LENGTH = PROVISION_PAGE + 0x14;
+ public static final int PROVISION_MAX_INACTIVITY_TIME_DEVICE_LOCK = PROVISION_PAGE + 0x15;
+ public static final int PROVISION_MAX_DEVICE_PASSWORD_FAILED_ATTEMPTS = PROVISION_PAGE + 0x16;
+ public static final int PROVISION_MAX_ATTACHMENT_SIZE = PROVISION_PAGE + 0x17;
+ public static final int PROVISION_ALLOW_SIMPLE_DEVICE_PASSWORD = PROVISION_PAGE + 0x18;
+ public static final int PROVISION_DEVICE_PASSWORD_EXPIRATION = PROVISION_PAGE + 0x19;
+ public static final int PROVISION_DEVICE_PASSWORD_HISTORY = PROVISION_PAGE + 0x1A;
+ public static final int PROVISION_MAX_SUPPORTED_TAG = PROVISION_DEVICE_PASSWORD_HISTORY;
+
+ // EAS 12.1
+ public static final int PROVISION_ALLOW_STORAGE_CARD = PROVISION_PAGE + 0x1B;
+ public static final int PROVISION_ALLOW_CAMERA = PROVISION_PAGE + 0x1C;
+ public static final int PROVISION_REQUIRE_DEVICE_ENCRYPTION = PROVISION_PAGE + 0x1D;
+ public static final int PROVISION_ALLOW_UNSIGNED_APPLICATIONS = PROVISION_PAGE + 0x1E;
+ public static final int PROVISION_ALLOW_UNSIGNED_INSTALLATION_PACKAGES = PROVISION_PAGE + 0x1F;
+ public static final int PROVISION_MIN_DEVICE_PASSWORD_COMPLEX_CHARS = PROVISION_PAGE + 0x20;
+ public static final int PROVISION_ALLOW_WIFI = PROVISION_PAGE + 0x21;
+ public static final int PROVISION_ALLOW_TEXT_MESSAGING = PROVISION_PAGE + 0x22;
+ public static final int PROVISION_ALLOW_POP_IMAP_EMAIL = PROVISION_PAGE + 0x23;
+ public static final int PROVISION_ALLOW_BLUETOOTH = PROVISION_PAGE + 0x24;
+ public static final int PROVISION_ALLOW_IRDA = PROVISION_PAGE + 0x25;
+ public static final int PROVISION_REQUIRE_MANUAL_SYNC_WHEN_ROAMING = PROVISION_PAGE + 0x26;
+ public static final int PROVISION_ALLOW_DESKTOP_SYNC = PROVISION_PAGE + 0x27;
+ public static final int PROVISION_MAX_CALENDAR_AGE_FILTER = PROVISION_PAGE + 0x28;
+ public static final int PROVISION_ALLOW_HTML_EMAIL = PROVISION_PAGE + 0x29;
+ public static final int PROVISION_MAX_EMAIL_AGE_FILTER = PROVISION_PAGE + 0x2A;
+ public static final int PROVISION_MAX_EMAIL_BODY_TRUNCATION_SIZE = PROVISION_PAGE + 0x2B;
+ public static final int PROVISION_MAX_EMAIL_HTML_BODY_TRUNCATION_SIZE = PROVISION_PAGE + 0x2C;
+ public static final int PROVISION_REQUIRE_SIGNED_SMIME_MESSAGES = PROVISION_PAGE + 0x2D;
+ public static final int PROVISION_REQUIRE_ENCRYPTED_SMIME_MESSAGES = PROVISION_PAGE + 0x2E;
+ public static final int PROVISION_REQUIRE_SIGNED_SMIME_ALGORITHM = PROVISION_PAGE + 0x2F;
+ public static final int PROVISION_REQUIRE_ENCRYPTION_SMIME_ALGORITHM = PROVISION_PAGE + 0x30;
+ public static final int PROVISION_ALLOW_SMIME_ENCRYPTION_NEGOTIATION = PROVISION_PAGE + 0x31;
+ public static final int PROVISION_ALLOW_SMIME_SOFT_CERTS = PROVISION_PAGE + 0x32;
+ public static final int PROVISION_ALLOW_BROWSER = PROVISION_PAGE + 0x33;
+ public static final int PROVISION_ALLOW_CONSUMER_EMAIL = PROVISION_PAGE + 0x34;
+ public static final int PROVISION_ALLOW_REMOTE_DESKTOP = PROVISION_PAGE + 0x35;
+ public static final int PROVISION_ALLOW_INTERNET_SHARING = PROVISION_PAGE + 0x36;
+ public static final int PROVISION_UNAPPROVED_IN_ROM_APPLICATION_LIST = PROVISION_PAGE + 0x37;
+ public static final int PROVISION_APPLICATION_NAME = PROVISION_PAGE + 0x38;
+ public static final int PROVISION_APPROVED_APPLICATION_LIST = PROVISION_PAGE + 0x39;
+ public static final int PROVISION_HASH = PROVISION_PAGE + 0x3A;
+
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;
@@ -441,6 +513,8 @@
},
{
// 0x08 MeetingResponse
+ "CalId", "CollectionId", "MeetingResponse", "ReqId", "Request",
+ "Result", "Status", "UserResponse", "Version"
},
{
// 0x09 Tasks
@@ -473,7 +547,8 @@
"Provision", "Policies", "Policy", "PolicyType", "PolicyKey", "Data", "ProvisionStatus",
"RemoteWipe", "EASProvidionDoc", "DevicePasswordEnabled",
"AlphanumericDevicePasswordRequired",
- "DeviceEncryptionEnabled", "-unused-", "AttachmentsEnabled", "MinDevicePasswordLength",
+ "DeviceEncryptionEnabled", "PasswordRecoveryEnabled", "-unused-", "AttachmentsEnabled",
+ "MinDevicePasswordLength",
"MaxInactivityTimeDeviceLock", "MaxDevicePasswordFailedAttempts", "MaxAttachmentSize",
"AllowSimpleDevicePassword", "DevicePasswordExpiration", "DevicePasswordHistory",
"AllowStorageCard", "AllowCamera", "RequireDeviceEncryption",
diff --git a/src/com/android/exchange/utility/CalendarUtilities.java b/src/com/android/exchange/utility/CalendarUtilities.java
new file mode 100644
index 0000000..e9945b7
--- /dev/null
+++ b/src/com/android/exchange/utility/CalendarUtilities.java
@@ -0,0 +1,728 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.utility;
+
+import com.android.exchange.Eas;
+import com.android.exchange.adapter.Serializer;
+import com.android.exchange.adapter.Tags;
+
+import org.bouncycastle.util.encoders.Base64;
+
+import android.util.Log;
+
+import java.io.IOException;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.TimeZone;
+
+public class CalendarUtilities {
+ // NOTE: Most definitions in this class are have package visibility for testing purposes
+ private static final String TAG = "CalendarUtility";
+
+ // Time related convenience constants, in milliseconds
+ static final int SECONDS = 1000;
+ static final int MINUTES = SECONDS*60;
+ static final int HOURS = MINUTES*60;
+ static final long DAYS = HOURS*24;
+
+ // NOTE All Microsoft data structures are little endian
+
+ // The following constants relate to standard Microsoft data sizes
+ // For documentation, see http://msdn.microsoft.com/en-us/library/aa505945.aspx
+ static final int MSFT_LONG_SIZE = 4;
+ static final int MSFT_WCHAR_SIZE = 2;
+ static final int MSFT_WORD_SIZE = 2;
+
+ // The following constants relate to Microsoft's SYSTEMTIME structure
+ // For documentation, see: http://msdn.microsoft.com/en-us/library/ms724950(VS.85).aspx?ppud=4
+
+ static final int MSFT_SYSTEMTIME_YEAR = 0 * MSFT_WORD_SIZE;
+ static final int MSFT_SYSTEMTIME_MONTH = 1 * MSFT_WORD_SIZE;
+ static final int MSFT_SYSTEMTIME_DAY_OF_WEEK = 2 * MSFT_WORD_SIZE;
+ static final int MSFT_SYSTEMTIME_DAY = 3 * MSFT_WORD_SIZE;
+ static final int MSFT_SYSTEMTIME_HOUR = 4 * MSFT_WORD_SIZE;
+ static final int MSFT_SYSTEMTIME_MINUTE = 5 * MSFT_WORD_SIZE;
+ //static final int MSFT_SYSTEMTIME_SECONDS = 6 * MSFT_WORD_SIZE;
+ //static final int MSFT_SYSTEMTIME_MILLIS = 7 * MSFT_WORD_SIZE;
+ static final int MSFT_SYSTEMTIME_SIZE = 8*MSFT_WORD_SIZE;
+
+ // The following constants relate to Microsoft's TIME_ZONE_INFORMATION structure
+ // For documentation, see http://msdn.microsoft.com/en-us/library/ms725481(VS.85).aspx
+ static final int MSFT_TIME_ZONE_BIAS_OFFSET = 0;
+ static final int MSFT_TIME_ZONE_STANDARD_NAME_OFFSET =
+ MSFT_TIME_ZONE_BIAS_OFFSET + MSFT_LONG_SIZE;
+ static final int MSFT_TIME_ZONE_STANDARD_DATE_OFFSET =
+ MSFT_TIME_ZONE_STANDARD_NAME_OFFSET + (MSFT_WCHAR_SIZE*32);
+ static final int MSFT_TIME_ZONE_STANDARD_BIAS_OFFSET =
+ MSFT_TIME_ZONE_STANDARD_DATE_OFFSET + MSFT_SYSTEMTIME_SIZE;
+ static final int MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET =
+ MSFT_TIME_ZONE_STANDARD_BIAS_OFFSET + MSFT_LONG_SIZE;
+ static final int MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET =
+ MSFT_TIME_ZONE_DAYLIGHT_NAME_OFFSET + (MSFT_WCHAR_SIZE*32);
+ static final int MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET =
+ MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET + MSFT_SYSTEMTIME_SIZE;
+ static final int MSFT_TIME_ZONE_SIZE =
+ MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET + MSFT_LONG_SIZE;
+
+ // TimeZone cache; we parse/decode as little as possible, because the process is quite slow
+ private static HashMap<String, TimeZone> sTimeZoneCache = new HashMap<String, TimeZone>();
+ // TZI string cache; we keep around our encoded TimeZoneInformation strings
+ private static HashMap<TimeZone, String> sTziStringCache = new HashMap<TimeZone, String>();
+
+ // There is no type 4 (thus, the "")
+ static final String[] sTypeToFreq =
+ new String[] {"DAILY", "WEEKLY", "MONTHLY", "MONTHLY", "", "YEARLY", "YEARLY"};
+
+ static final String[] sDayTokens =
+ new String[] {"SU", "MO", "TU", "WE", "TH", "FR", "SA"};
+
+ static final String[] sTwoCharacterNumbers =
+ new String[] {"00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"};
+
+ static final int sCurrentYear = new GregorianCalendar().get(Calendar.YEAR);
+ static final TimeZone sGmtTimeZone = TimeZone.getTimeZone("GMT");
+
+ // Return a 4-byte long from a byte array (little endian)
+ static int getLong(byte[] bytes, int offset) {
+ return (bytes[offset++] & 0xFF) | ((bytes[offset++] & 0xFF) << 8) |
+ ((bytes[offset++] & 0xFF) << 16) | ((bytes[offset] & 0xFF) << 24);
+ }
+
+ // Put a 4-byte long into a byte array (little endian)
+ static void setLong(byte[] bytes, int offset, int value) {
+ bytes[offset++] = (byte) (value & 0xFF);
+ bytes[offset++] = (byte) ((value >> 8) & 0xFF);
+ bytes[offset++] = (byte) ((value >> 16) & 0xFF);
+ bytes[offset] = (byte) ((value >> 24) & 0xFF);
+ }
+
+ // Return a 2-byte word from a byte array (little endian)
+ static int getWord(byte[] bytes, int offset) {
+ return (bytes[offset++] & 0xFF) | ((bytes[offset] & 0xFF) << 8);
+ }
+
+ // Put a 2-byte word into a byte array (little endian)
+ static void setWord(byte[] bytes, int offset, int value) {
+ bytes[offset++] = (byte) (value & 0xFF);
+ bytes[offset] = (byte) ((value >> 8) & 0xFF);
+ }
+
+ // Internal structure for storing a time zone date from a SYSTEMTIME structure
+ // This date represents either the start or the end time for DST
+ static class TimeZoneDate {
+ String year;
+ int month;
+ int dayOfWeek;
+ int day;
+ int time;
+ int hour;
+ int minute;
+ }
+
+ // Write SYSTEMTIME data into a byte array (this will either be for the standard or daylight
+ // transition)
+ static void putTimeInMillisIntoSystemTime(byte[] bytes, int offset, long millis) {
+ GregorianCalendar cal = new GregorianCalendar(TimeZone.getDefault());
+ // Round to the next highest minute; we always write seconds as zero
+ cal.setTimeInMillis(millis + 30*SECONDS);
+
+ // MSFT months are 1 based; TimeZone is 0 based
+ setWord(bytes, offset + MSFT_SYSTEMTIME_MONTH, cal.get(Calendar.MONTH) + 1);
+ // MSFT day of week starts w/ Sunday = 0; TimeZone starts w/ Sunday = 1
+ setWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK, cal.get(Calendar.DAY_OF_WEEK) - 1);
+
+ // Get the "day" in TimeZone format
+ int wom = cal.get(Calendar.DAY_OF_WEEK_IN_MONTH);
+ // 5 means "last" in MSFT land; for TimeZone, it's -1
+ setWord(bytes, offset + MSFT_SYSTEMTIME_DAY, wom < 0 ? 5 : wom);
+
+ // Turn hours/minutes into ms from midnight (per TimeZone)
+ setWord(bytes, offset + MSFT_SYSTEMTIME_HOUR, cal.get(Calendar.HOUR));
+ setWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE, cal.get(Calendar.MINUTE));
+ }
+
+ // Build a TimeZoneDate structure from a SYSTEMTIME within a byte array at a given offset
+ static TimeZoneDate getTimeZoneDateFromSystemTime(byte[] bytes, int offset) {
+ TimeZoneDate tzd = new TimeZoneDate();
+
+ // MSFT year is an int; TimeZone is a String
+ int num = getWord(bytes, offset + MSFT_SYSTEMTIME_YEAR);
+ tzd.year = Integer.toString(num);
+
+ // MSFT month = 0 means no daylight time
+ // MSFT months are 1 based; TimeZone is 0 based
+ num = getWord(bytes, offset + MSFT_SYSTEMTIME_MONTH);
+ if (num == 0) {
+ return null;
+ } else {
+ tzd.month = num -1;
+ }
+
+ // MSFT day of week starts w/ Sunday = 0; TimeZone starts w/ Sunday = 1
+ tzd.dayOfWeek = getWord(bytes, offset + MSFT_SYSTEMTIME_DAY_OF_WEEK) + 1;
+
+ // Get the "day" in TimeZone format
+ num = getWord(bytes, offset + MSFT_SYSTEMTIME_DAY);
+ // 5 means "last" in MSFT land; for TimeZone, it's -1
+ if (num == 5) {
+ tzd.day = -1;
+ } else {
+ tzd.day = num;
+ }
+
+ // Turn hours/minutes into ms from midnight (per TimeZone)
+ int hour = getWord(bytes, offset + MSFT_SYSTEMTIME_HOUR);
+ tzd.hour = hour;
+ int minute = getWord(bytes, offset + MSFT_SYSTEMTIME_MINUTE);
+ tzd.minute = minute;
+ tzd.time = (hour*HOURS) + (minute*MINUTES);
+
+ return tzd;
+ }
+
+ // Return a String from within a byte array at the given offset with max characters
+ // Unused for now, but might be helpful for debugging
+ // String getString(byte[] bytes, int offset, int max) {
+ // StringBuilder sb = new StringBuilder();
+ // while (max-- > 0) {
+ // int b = bytes[offset];
+ // if (b == 0) break;
+ // sb.append((char)b);
+ // offset += 2;
+ // }
+ // return sb.toString();
+ // }
+
+ /**
+ * Build a GregorianCalendar, based on a time zone and TimeZoneDate.
+ * @param timeZone the time zone we're checking
+ * @param tzd the TimeZoneDate we're interested in
+ * @return a GregorianCalendar with the given time zone and date
+ */
+ static GregorianCalendar getCheckCalendar(TimeZone timeZone, TimeZoneDate tzd) {
+ GregorianCalendar testCalendar = new GregorianCalendar(timeZone);
+ testCalendar.set(GregorianCalendar.YEAR, sCurrentYear);
+ testCalendar.set(GregorianCalendar.MONTH, tzd.month);
+ testCalendar.set(GregorianCalendar.DAY_OF_WEEK, tzd.dayOfWeek);
+ testCalendar.set(GregorianCalendar.DAY_OF_WEEK_IN_MONTH, tzd.day);
+ testCalendar.set(GregorianCalendar.HOUR_OF_DAY, tzd.hour);
+ testCalendar.set(GregorianCalendar.MINUTE, tzd.minute);
+ return testCalendar;
+ }
+
+ /**
+ * Find a standard/daylight transition between a start time and an end time
+ * @param tz a TimeZone
+ * @param startTime the start time for the test
+ * @param endTime the end time for the test
+ * @param startInDaylightTime whether daylight time is in effect at the startTime
+ * @return the time in millis of the first transition, or 0 if none
+ */
+ static private long findTransition(TimeZone tz, long startTime, long endTime,
+ boolean startInDaylightTime) {
+ long startingEndTime = endTime;
+ Date date = null;
+ while ((endTime - startTime) > MINUTES) {
+ long checkTime = ((startTime + endTime) / 2) + 1;
+ date = new Date(checkTime);
+ if (tz.inDaylightTime(date) != startInDaylightTime) {
+ endTime = checkTime;
+ } else {
+ startTime = checkTime;
+ }
+ }
+ if (endTime == startingEndTime) {
+ // Really, this shouldn't happen
+ return 0;
+ }
+ return startTime;
+ }
+
+ /**
+ * Return a Base64 representation of a MSFT TIME_ZONE_INFORMATION structure from a TimeZone
+ * that might be found in an Event; use cached result, if possible
+ * @param tz the TimeZone
+ * @return the Base64 String representing a Microsoft TIME_ZONE_INFORMATION element
+ */
+ static public String timeZoneToTziString(TimeZone tz) {
+ String tziString = sTziStringCache.get(tz);
+ if (tziString != null) {
+ if (Eas.USER_LOG) {
+ Log.d(TAG, "TZI string for " + tz.getDisplayName() + " found in cache.");
+ }
+ return tziString;
+ }
+ tziString = timeZoneToTziStringImpl(tz);
+ sTziStringCache.put(tz, tziString);
+ return tziString;
+ }
+
+ /**
+ * Calculate the Base64 representation of a MSFT TIME_ZONE_INFORMATION structure from a TimeZone
+ * that might be found in an Event. Since the internal representation of the TimeZone is hidden
+ * from us we'll find the DST transitions and build the structure from that information
+ * @param tz the TimeZone
+ * @return the Base64 String representing a Microsoft TIME_ZONE_INFORMATION element
+ */
+ static public String timeZoneToTziStringImpl(TimeZone tz) {
+ String tziString;
+ long time = System.currentTimeMillis();
+ byte[] tziBytes = new byte[MSFT_TIME_ZONE_SIZE];
+ int standardBias = - tz.getRawOffset();
+ standardBias /= 60*SECONDS;
+ setLong(tziBytes, MSFT_TIME_ZONE_BIAS_OFFSET, standardBias);
+ // If this time zone has daylight savings time, we need to do a bunch more work
+ if (tz.useDaylightTime()) {
+ long standardTransition = 0;
+ long daylightTransition = 0;
+ GregorianCalendar cal = new GregorianCalendar();
+ cal.set(sCurrentYear, Calendar.JANUARY, 1, 0, 0, 0);
+ cal.setTimeZone(tz);
+ long startTime = cal.getTimeInMillis();
+ // Calculate rough end of year; no need to do the calculation
+ long endOfYearTime = startTime + 365*DAYS;
+ Date date = new Date(startTime);
+ boolean startInDaylightTime = tz.inDaylightTime(date);
+ // Find the first transition, and store
+ startTime = findTransition(tz, startTime, endOfYearTime, startInDaylightTime);
+ if (startInDaylightTime) {
+ standardTransition = startTime;
+ } else {
+ daylightTransition = startTime;
+ }
+ // Find the second transition, and store
+ startTime = findTransition(tz, startTime, endOfYearTime, !startInDaylightTime);
+ if (startInDaylightTime) {
+ daylightTransition = startTime;
+ } else {
+ standardTransition = startTime;
+ }
+ if (standardTransition != 0 && daylightTransition != 0) {
+ putTimeInMillisIntoSystemTime(tziBytes, MSFT_TIME_ZONE_STANDARD_DATE_OFFSET,
+ standardTransition);
+ putTimeInMillisIntoSystemTime(tziBytes, MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET,
+ daylightTransition);
+ int dstOffset = tz.getDSTSavings();
+ setLong(tziBytes, MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET, - dstOffset / MINUTES);
+ }
+ }
+ // TODO Use a more efficient Base64 API
+ byte[] tziEncodedBytes = Base64.encode(tziBytes);
+ tziString = new String(tziEncodedBytes);
+ if (Eas.USER_LOG) {
+ Log.d(TAG, "Calculated TZI String for " + tz.getDisplayName() + " in " +
+ (System.currentTimeMillis() - time) + "ms");
+ }
+ return tziString;
+ }
+
+ /**
+ * Given a String as directly read from EAS, returns a TimeZone corresponding to that String
+ * @param timeZoneString the String read from the server
+ * @return the TimeZone, or TimeZone.getDefault() if not found
+ */
+ static public TimeZone tziStringToTimeZone(String timeZoneString) {
+ // If we have this time zone cached, use that value and return
+ TimeZone timeZone = sTimeZoneCache.get(timeZoneString);
+ if (timeZone != null) {
+ if (Eas.USER_LOG) {
+ Log.d(TAG, " Using cached TimeZone " + timeZone.getDisplayName());
+ }
+ } else {
+ timeZone = tziStringToTimeZoneImpl(timeZoneString);
+ if (timeZone == null) {
+ // If we don't find a match, we just return the current TimeZone. In theory, this
+ // shouldn't be happening...
+ Log.w(TAG, "TimeZone not found using default: " + timeZoneString);
+ timeZone = TimeZone.getDefault();
+ }
+ sTimeZoneCache.put(timeZoneString, timeZone);
+ }
+ return timeZone;
+ }
+
+ /**
+ * Given a String as directly read from EAS, tries to find a TimeZone in the database of all
+ * time zones that corresponds to that String.
+ * @param timeZoneString the String read from the server
+ * @return the TimeZone, or TimeZone.getDefault() if not found
+ */
+ static public TimeZone tziStringToTimeZoneImpl(String timeZoneString) {
+ TimeZone timeZone = null;
+ // TODO Remove after we're comfortable with performance
+ long time = System.currentTimeMillis();
+ // First, we need to decode the base64 string
+ byte[] timeZoneBytes = Base64.decode(timeZoneString);
+
+ // Then, we get the bias (similar to a rawOffset); for TimeZone, we need ms
+ // but EAS gives us minutes, so do the conversion. Note that EAS is the bias that's added
+ // to the time zone to reach UTC; our library uses the time from UTC to our time zone, so
+ // we need to change the sign
+ int bias = -1 * getLong(timeZoneBytes, MSFT_TIME_ZONE_BIAS_OFFSET) * MINUTES;
+
+ // Get all of the time zones with the bias as a rawOffset; if there aren't any, we return
+ // the default time zone
+ String[] zoneIds = TimeZone.getAvailableIDs(bias);
+ if (zoneIds.length > 0) {
+ // Try to find an existing TimeZone from the data provided by EAS
+ // We start by pulling out the date that standard time begins
+ TimeZoneDate dstEnd =
+ getTimeZoneDateFromSystemTime(timeZoneBytes, MSFT_TIME_ZONE_STANDARD_DATE_OFFSET);
+ if (dstEnd == null) {
+ // In this case, there is no daylight savings time, so the only interesting data
+ // is the offset, and we know that all of the zoneId's match; we'll take the first
+ timeZone = TimeZone.getTimeZone(zoneIds[0]);
+ String dn = timeZone.getDisplayName();
+ sTimeZoneCache.put(timeZoneString, timeZone);
+ if (Eas.USER_LOG) {
+ Log.d(TAG, "TimeZone without DST found by offset: " + dn);
+ }
+ } else {
+ TimeZoneDate dstStart = getTimeZoneDateFromSystemTime(timeZoneBytes,
+ MSFT_TIME_ZONE_DAYLIGHT_DATE_OFFSET);
+ // See comment above for bias...
+ long dstSavings =
+ -1 * getLong(timeZoneBytes, MSFT_TIME_ZONE_DAYLIGHT_BIAS_OFFSET) * MINUTES;
+
+ // We'll go through each time zone to find one with the same DST transitions and
+ // savings length
+ for (String zoneId: zoneIds) {
+ // Get the TimeZone using the zoneId
+ timeZone = TimeZone.getTimeZone(zoneId);
+
+ // Our strategy here is to check just before and just after the transitions
+ // and see whether the check for daylight time matches the expectation
+ // If both transitions match, then we have a match for the offset and start/end
+ // of dst. That's the best we can do for now, since there's no other info
+ // provided by EAS (i.e. we can't get dynamic transitions, etc.)
+
+ int testSavingsMinutes = timeZone.getDSTSavings() / MINUTES;
+ int errorBoundsMinutes = (testSavingsMinutes * 2) + 1;
+
+ // Check start DST transition
+ GregorianCalendar testCalendar = getCheckCalendar(timeZone, dstStart);
+ testCalendar.add(GregorianCalendar.MINUTE, - errorBoundsMinutes);
+ Date before = testCalendar.getTime();
+ testCalendar.add(GregorianCalendar.MINUTE, 2*errorBoundsMinutes);
+ Date after = testCalendar.getTime();
+ if (timeZone.inDaylightTime(before)) continue;
+ if (!timeZone.inDaylightTime(after)) continue;
+
+ // Check end DST transition
+ testCalendar = getCheckCalendar(timeZone, dstEnd);
+ testCalendar.add(GregorianCalendar.MINUTE, - errorBoundsMinutes);
+ before = testCalendar.getTime();
+ testCalendar.add(GregorianCalendar.MINUTE, 2*errorBoundsMinutes);
+ after = testCalendar.getTime();
+ if (!timeZone.inDaylightTime(before)) continue;
+ if (timeZone.inDaylightTime(after)) continue;
+
+ // Check that the savings are the same
+ if (dstSavings != timeZone.getDSTSavings()) continue;
+
+ // If we're here, it's the right time zone, modulo dynamic DST
+ String dn = timeZone.getDisplayName();
+ // TODO Remove timing when we're comfortable with performance
+ if (Eas.USER_LOG) {
+ Log.d(TAG, "TimeZone found by rules: " + dn + " in " +
+ (System.currentTimeMillis() - time) + "ms");
+ }
+ break;
+ }
+ }
+ }
+ return timeZone;
+ }
+
+ /**
+ * Generate a time in milliseconds from a date string that represents a date/time in GMT
+ * @param DateTime string from Exchange server
+ * @return the time in milliseconds (since Jan 1, 1970)
+ */
+ static public long parseDateTimeToMillis(String date) {
+ // Format for calendar date strings is 20090211T180303Z
+ GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)),
+ Integer.parseInt(date.substring(4, 6)) - 1, Integer.parseInt(date.substring(6, 8)),
+ Integer.parseInt(date.substring(9, 11)), Integer.parseInt(date.substring(11, 13)),
+ Integer.parseInt(date.substring(13, 15)));
+ cal.setTimeZone(TimeZone.getTimeZone("GMT"));
+ return cal.getTimeInMillis();
+ }
+
+ /**
+ * Generate a GregorianCalendar from a date string that represents a date/time in GMT
+ * @param DateTime string from Exchange server
+ * @return the GregorianCalendar
+ */
+ static public GregorianCalendar parseDateTimeToCalendar(String date) {
+ // Format for calendar date strings is 20090211T180303Z
+ GregorianCalendar cal = new GregorianCalendar(Integer.parseInt(date.substring(0, 4)),
+ Integer.parseInt(date.substring(4, 6)) - 1, Integer.parseInt(date.substring(6, 8)),
+ Integer.parseInt(date.substring(9, 11)), Integer.parseInt(date.substring(11, 13)),
+ Integer.parseInt(date.substring(13, 15)));
+ cal.setTimeZone(TimeZone.getTimeZone("GMT"));
+ return cal;
+ }
+
+ static String formatTwo(int num) {
+ if (num <= 12) {
+ return sTwoCharacterNumbers[num];
+ } else
+ return Integer.toString(num);
+ }
+
+ /**
+ * Generate an EAS formatted date/time string based on GMT. See below for details.
+ */
+ static public String millisToEasDateTime(long millis) {
+ return millisToEasDateTime(millis, sGmtTimeZone);
+ }
+
+ /**
+ * Generate an EAS formatted local date/time string from a time and a time zone
+ * @param millis a time in milliseconds
+ * @param tz a time zone
+ * @return an EAS formatted string indicating the date/time in the given time zone
+ */
+ static public String millisToEasDateTime(long millis, TimeZone tz) {
+ StringBuilder sb = new StringBuilder();
+ GregorianCalendar cal = new GregorianCalendar(tz);
+ cal.setTimeInMillis(millis);
+ sb.append(cal.get(Calendar.YEAR));
+ sb.append(formatTwo(cal.get(Calendar.MONTH) + 1));
+ sb.append(formatTwo(cal.get(Calendar.DAY_OF_MONTH)));
+ sb.append('T');
+ sb.append(formatTwo(cal.get(Calendar.HOUR_OF_DAY)));
+ sb.append(formatTwo(cal.get(Calendar.MINUTE)));
+ sb.append(formatTwo(cal.get(Calendar.SECOND)));
+ sb.append('Z');
+ return sb.toString();
+ }
+
+ static void addByDay(StringBuilder rrule, int dow, int wom) {
+ rrule.append(";BYDAY=");
+ boolean addComma = false;
+ for (int i = 0; i < 7; i++) {
+ if ((dow & 1) == 1) {
+ if (addComma) {
+ rrule.append(',');
+ }
+ if (wom > 0) {
+ // 5 = last week -> -1
+ // So -1SU = last sunday
+ rrule.append(wom == 5 ? -1 : wom);
+ }
+ rrule.append(sDayTokens[i]);
+ addComma = true;
+ }
+ dow >>= 1;
+ }
+ }
+
+ static void addByMonthDay(StringBuilder rrule, int dom) {
+ // 127 means last day of the month
+ if (dom == 127) {
+ dom = -1;
+ }
+ rrule.append(";BYMONTHDAY=" + dom);
+ }
+
+ /**
+ * Generate the String version of the EAS integer for a given BYDAY value in an rrule
+ * @param dow the BYDAY value of the rrule
+ * @return the String version of the EAS value of these days
+ */
+ static String generateEasDayOfWeek(String dow) {
+ int bits = 0;
+ int bit = 1;
+ for (String token: sDayTokens) {
+ // If we can find the day in the dow String, add the bit to our bits value
+ if (dow.indexOf(token) >= 0) {
+ bits |= bit;
+ }
+ bit <<= 1;
+ }
+ return Integer.toString(bits);
+ }
+
+ /**
+ * Extract the value of a token in an RRULE string
+ * @param rrule an RRULE string
+ * @param token a token to look for in the RRULE
+ * @return the value of that token
+ */
+ static String tokenFromRrule(String rrule, String token) {
+ int start = rrule.indexOf(token);
+ if (start < 0) return null;
+ int len = rrule.length();
+ start += token.length();
+ int end = start;
+ char c;
+ do {
+ c = rrule.charAt(end++);
+ if (!Character.isLetterOrDigit(c) || (end == len)) {
+ if (end == len) end++;
+ return rrule.substring(start, end -1);
+ }
+ } while (true);
+ }
+
+ /**
+ * Write recurrence information to EAS based on the RRULE in CalendarProvider
+ * @param rrule the RRULE, from CalendarProvider
+ * @param startTime, the DTSTART of this Event
+ * @param s the Serializer we're using to write WBXML data
+ * @throws IOException
+ */
+ // NOTE: For the moment, we're only parsing recurrence types that are supported by the
+ // Calendar app UI, which is a small subset of possible recurrence types
+ // This code must be updated when the Calendar adds new functionality
+ static public void recurrenceFromRrule(String rrule, long startTime, Serializer s)
+ throws IOException {
+ Log.d("RRULE", "rule: " + rrule);
+ String freq = tokenFromRrule(rrule, "FREQ=");
+ // If there's no FREQ=X, then we don't write a recurrence
+ // Note that we duplicate s.start(Tags.CALENDAR_RECURRENCE); s.end(); to prevent the
+ // possibility of writing out a partial recurrence stanza
+ if (freq != null) {
+ if (freq.equals("DAILY")) {
+ s.start(Tags.CALENDAR_RECURRENCE);
+ s.data(Tags.CALENDAR_RECURRENCE_TYPE, "0");
+ s.data(Tags.CALENDAR_RECURRENCE_INTERVAL, "1");
+ s.end();
+ } else if (freq.equals("WEEKLY")) {
+ s.start(Tags.CALENDAR_RECURRENCE);
+ s.data(Tags.CALENDAR_RECURRENCE_TYPE, "1");
+ s.data(Tags.CALENDAR_RECURRENCE_INTERVAL, "1");
+ // Requires a day of week (whereas RRULE does not)
+ String byDay = tokenFromRrule(rrule, "BYDAY=");
+ if (byDay != null) {
+ s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(byDay));
+ }
+ s.end();
+ } else if (freq.equals("MONTHLY")) {
+ String byMonthDay = tokenFromRrule(rrule, "BYMONTHDAY=");
+ if (byMonthDay != null) {
+ // The nth day of the month
+ s.start(Tags.CALENDAR_RECURRENCE);
+ s.data(Tags.CALENDAR_RECURRENCE_TYPE, "2");
+ s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay);
+ s.end();
+ } else {
+ String byDay = tokenFromRrule(rrule, "BYDAY=");
+ String bareByDay;
+ if (byDay != null) {
+ // This can be 1WE (1st Wednesday) or -1FR (last Friday)
+ int wom = byDay.charAt(0);
+ if (wom == '-') {
+ // -1 is the only legal case (last week) Use "5" for EAS
+ wom = 5;
+ bareByDay = byDay.substring(2);
+ } else {
+ wom = wom - '0';
+ bareByDay = byDay.substring(1);
+ }
+ s.start(Tags.CALENDAR_RECURRENCE);
+ s.data(Tags.CALENDAR_RECURRENCE_TYPE, "3");
+ s.data(Tags.CALENDAR_RECURRENCE_WEEKOFMONTH, Integer.toString(wom));
+ s.data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, generateEasDayOfWeek(bareByDay));
+ s.end();
+ }
+ }
+ } else if (freq.equals("YEARLY")) {
+ String byMonth = tokenFromRrule(rrule, "BYMONTH=");
+ String byMonthDay = tokenFromRrule(rrule, "BYMONTHDAY=");
+ if (byMonth == null || byMonthDay == null) {
+ // Calculate the month and day from the startDate
+ GregorianCalendar cal = new GregorianCalendar();
+ cal.setTimeInMillis(startTime);
+ cal.setTimeZone(TimeZone.getDefault());
+ byMonth = Integer.toString(cal.get(Calendar.MONTH) + 1);
+ byMonthDay = Integer.toString(cal.get(Calendar.DAY_OF_MONTH));
+ }
+ s.start(Tags.CALENDAR_RECURRENCE);
+ s.data(Tags.CALENDAR_RECURRENCE_TYPE, "5");
+ s.data(Tags.CALENDAR_RECURRENCE_DAYOFMONTH, byMonthDay);
+ s.data(Tags.CALENDAR_RECURRENCE_MONTHOFYEAR, byMonth);
+ s.end();
+ }
+ }
+ }
+
+ /**
+ * Build an RRULE String from EAS recurrence information
+ * @param type the type of recurrence
+ * @param occurrences how many recurrences (instances)
+ * @param interval the interval between recurrences
+ * @param dow day of the week
+ * @param dom day of the month
+ * @param wom week of the month
+ * @param moy month of the year
+ * @param until the last recurrence time
+ * @return a valid RRULE String
+ */
+ static public String rruleFromRecurrence(int type, int occurrences, int interval, int dow,
+ int dom, int wom, int moy, String until) {
+ StringBuilder rrule = new StringBuilder("FREQ=" + sTypeToFreq[type]);
+
+ // INTERVAL and COUNT
+ if (interval > 0) {
+ rrule.append(";INTERVAL=" + interval);
+ }
+ if (occurrences > 0) {
+ rrule.append(";COUNT=" + occurrences);
+ }
+
+ // Days, weeks, months, etc.
+ switch(type) {
+ case 0: // DAILY
+ case 1: // WEEKLY
+ if (dow > 0) addByDay(rrule, dow, -1);
+ break;
+ case 2: // MONTHLY
+ if (dom > 0) addByMonthDay(rrule, dom);
+ break;
+ case 3: // MONTHLY (on the nth day)
+ if (dow > 0) addByDay(rrule, dow, wom);
+ break;
+ case 5: // YEARLY
+ if (dom > 0) addByMonthDay(rrule, dom);
+ if (moy > 0) {
+ // TODO MAKE SURE WE'RE 1 BASED
+ rrule.append(";BYMONTH=" + moy);
+ }
+ break;
+ case 6: // YEARLY (on the nth day)
+ if (dow > 0) addByDay(rrule, dow, wom);
+ if (moy > 0) addByMonthDay(rrule, dow);
+ break;
+ default:
+ break;
+ }
+
+ // UNTIL comes last
+ // TODO Add UNTIL code
+ if (until != null) {
+ // *** until probably needs reformatting
+ //rrule.append(";UNTIL=" + until);
+ }
+
+ return rrule.toString();
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/exchange/utility/Duration.java b/src/com/android/exchange/utility/Duration.java
new file mode 100644
index 0000000..0ec867c
--- /dev/null
+++ b/src/com/android/exchange/utility/Duration.java
@@ -0,0 +1,128 @@
+/* Copyright 2010, The Android Open Source Project
+ **
+ ** Licensed under the Apache License, Version 2.0 (the "License");
+ ** you may not use this file except in compliance with the License.
+ ** You may obtain a copy of the License at
+ **
+ ** http://www.apache.org/licenses/LICENSE-2.0
+ **
+ ** Unless required by applicable law or agreed to in writing, software
+ ** distributed under the License is distributed on an "AS IS" BASIS,
+ ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ** See the License for the specific language governing permissions and
+ ** limitations under the License.
+ */
+
+package com.android.exchange.utility;
+
+import android.pim.DateException;
+
+import java.util.Calendar;
+
+/**
+ * Note: This class was simply copied from the class in CalendarProvider, since we don't have access
+ * to it from the Email app. I reformated some lines, but otherwise haven't altered the code.
+ */
+public class Duration {
+ public int sign; // 1 or -1
+ public int weeks;
+ public int days;
+ public int hours;
+ public int minutes;
+ public int seconds;
+
+ public Duration() {
+ sign = 1;
+ }
+
+ /**
+ * Parse according to RFC2445 ss4.3.6. (It's actually a little loose with
+ * its parsing, for better or for worse)
+ */
+ public void parse(String str) throws DateException {
+ sign = 1;
+ weeks = 0;
+ days = 0;
+ hours = 0;
+ minutes = 0;
+ seconds = 0;
+
+ int len = str.length();
+ int index = 0;
+ char c;
+
+ if (len < 1) {
+ return;
+ }
+
+ c = str.charAt(0);
+ if (c == '-') {
+ sign = -1;
+ index++;
+ } else if (c == '+') {
+ index++;
+ }
+
+ if (len < index) {
+ return;
+ }
+
+ c = str.charAt(index);
+ if (c != 'P') {
+ throw new DateException (
+ "Duration.parse(str='" + str + "') expected 'P' at index="
+ + index);
+ }
+ index++;
+
+ int n = 0;
+ for (; index < len; index++) {
+ c = str.charAt(index);
+ if (c >= '0' && c <= '9') {
+ n *= 10;
+ n += (c - '0');
+ } else if (c == 'W') {
+ weeks = n;
+ n = 0;
+ } else if (c == 'H') {
+ hours = n;
+ n = 0;
+ } else if (c == 'M') {
+ minutes = n;
+ n = 0;
+ } else if (c == 'S') {
+ seconds = n;
+ n = 0;
+ } else if (c == 'D') {
+ days = n;
+ n = 0;
+ } else if (c == 'T') {
+ } else {
+ throw new DateException (
+ "Duration.parse(str='" + str + "') unexpected char '"
+ + c + "' at index=" + index);
+ }
+ }
+ }
+
+ /**
+ * Add this to the calendar provided, in place, in the calendar.
+ */
+ public void addTo(Calendar cal) {
+ cal.add(Calendar.DAY_OF_MONTH, sign*weeks*7);
+ cal.add(Calendar.DAY_OF_MONTH, sign*days);
+ cal.add(Calendar.HOUR, sign*hours);
+ cal.add(Calendar.MINUTE, sign*minutes);
+ cal.add(Calendar.SECOND, sign*seconds);
+ }
+
+ public long addTo(long dt) {
+ return dt + getMillis();
+ }
+
+ public long getMillis() {
+ long factor = 1000 * sign;
+ return factor * ((7*24*60*60*weeks) + (24*60*60*days) + (60*60*hours) + (60*minutes) +
+ seconds);
+ }
+}
diff --git a/tests/src/com/android/exchange/EasSyncServiceTests.java b/tests/src/com/android/exchange/EasSyncServiceTests.java
index 45f4882..d4372a5 100644
--- a/tests/src/com/android/exchange/EasSyncServiceTests.java
+++ b/tests/src/com/android/exchange/EasSyncServiceTests.java
@@ -73,6 +73,4 @@
}
}
}
-
-
}
diff --git a/tests/src/com/android/exchange/SyncManagerAccountTests.java b/tests/src/com/android/exchange/SyncManagerAccountTests.java
new file mode 100644
index 0000000..66c20e9
--- /dev/null
+++ b/tests/src/com/android/exchange/SyncManagerAccountTests.java
@@ -0,0 +1,179 @@
+/*
+ * 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 com.android.email.Email;
+import com.android.email.provider.EmailContent;
+import com.android.email.provider.EmailProvider;
+import com.android.email.provider.ProviderTestUtils;
+import com.android.email.provider.EmailContent.Account;
+import com.android.exchange.SyncManager.AccountList;
+
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerFuture;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.test.ProviderTestCase2;
+
+import java.io.IOException;
+
+public class SyncManagerAccountTests extends ProviderTestCase2<EmailProvider> {
+
+ private static final String TEST_ACCOUNT_PREFIX = "__test";
+ private static final String TEST_ACCOUNT_SUFFIX = "@android.com";
+
+ EmailProvider mProvider;
+ Context mMockContext;
+
+ public SyncManagerAccountTests() {
+ super(EmailProvider.class, EmailProvider.EMAIL_AUTHORITY);
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mMockContext = getMockContext();
+ // Delete any test accounts we might have created earlier
+ deleteTemporaryAccountManagerAccounts(getContext());
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ // Delete any test accounts we might have created earlier
+ deleteTemporaryAccountManagerAccounts(getContext());
+ }
+
+ private android.accounts.Account makeAccountManagerAccount(String username) {
+ return new android.accounts.Account(username, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+ }
+
+ private void createAccountManagerAccount(String username) {
+ final android.accounts.Account account = makeAccountManagerAccount(username);
+ AccountManager.get(getContext()).addAccountExplicitly(account, "password", null);
+ }
+
+ private Account setupProviderAndAccountManagerAccount(String username) {
+ // Note that setupAccount creates the email address username@android.com, so that's what
+ // we need to use for the account manager
+ createAccountManagerAccount(username + "@android.com");
+ return ProviderTestUtils.setupAccount(username, true, mMockContext);
+ }
+
+ private AccountList makeSyncManagerAccountList() {
+ AccountList accountList = new AccountList();
+ Cursor c = mMockContext.getContentResolver().query(Account.CONTENT_URI,
+ Account.CONTENT_PROJECTION, null, null, null);
+ try {
+ while (c.moveToNext()) {
+ accountList.add(new Account().restore(c));
+ }
+ } finally {
+ c.close();
+ }
+ return accountList;
+ }
+
+ private void deleteAccountManagerAccount(Context context, android.accounts.Account account) {
+ AccountManagerFuture<Boolean> future =
+ AccountManager.get(context).removeAccount(account, null, null);
+ try {
+ future.getResult();
+ } catch (OperationCanceledException e) {
+ } catch (AuthenticatorException e) {
+ } catch (IOException e) {
+ }
+ }
+
+ private void deleteTemporaryAccountManagerAccounts(Context context) {
+ android.accounts.Account[] accountManagerAccounts =
+ AccountManager.get(context).getAccountsByType(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+ for (android.accounts.Account accountManagerAccount: accountManagerAccounts) {
+ if (accountManagerAccount.name.startsWith(TEST_ACCOUNT_PREFIX) &&
+ accountManagerAccount.name.endsWith(TEST_ACCOUNT_SUFFIX)) {
+ deleteAccountManagerAccount(context, accountManagerAccount);
+ }
+ }
+ }
+
+ private String getTestAccountName(String name) {
+ return TEST_ACCOUNT_PREFIX + name;
+ }
+
+ private String getTestAccountEmailAddress(String name) {
+ return TEST_ACCOUNT_PREFIX + name + TEST_ACCOUNT_SUFFIX;
+ }
+
+ public void testReconcileAccounts() {
+ // Note that we can't use mMockContext for AccountManager interactions, as it isn't a fully
+ // functional Context.
+ Context context = getContext();
+
+ // Set up three accounts, both in AccountManager and in EmailProvider
+ Account firstAccount = setupProviderAndAccountManagerAccount(getTestAccountName("1"));
+ setupProviderAndAccountManagerAccount(getTestAccountName("2"));
+ setupProviderAndAccountManagerAccount(getTestAccountName("3"));
+
+ // Check that they're set up properly
+ assertEquals(3, EmailContent.count(mMockContext, Account.CONTENT_URI, null, null));
+ android.accounts.Account[] accountManagerAccounts =
+ AccountManager.get(context).getAccountsByType(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+ assertEquals(3, accountManagerAccounts.length);
+
+ // Delete account "2" from AccountManager
+ android.accounts.Account removedAccount =
+ makeAccountManagerAccount(getTestAccountEmailAddress("2"));
+ deleteAccountManagerAccount(context, removedAccount);
+
+ // Confirm it's deleted
+ accountManagerAccounts =
+ AccountManager.get(context).getAccountsByType(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+ assertEquals(2, accountManagerAccounts.length);
+
+ // Run the reconciler
+ SyncManager syncManager = new SyncManager();
+ ContentResolver resolver = mMockContext.getContentResolver();
+ syncManager.mResolver = resolver;
+ syncManager.reconcileAccountsWithAccountManager(context,
+ makeSyncManagerAccountList(), accountManagerAccounts);
+
+ // There should now be only two EmailProvider accounts
+ assertEquals(2, EmailContent.count(mMockContext, Account.CONTENT_URI, null, null));
+
+ // Ok, now we've got two of each; let's delete a provider account
+ resolver.delete(ContentUris.withAppendedId(Account.CONTENT_URI, firstAccount.mId),
+ null, null);
+ // ...and then there was one
+ assertEquals(1, EmailContent.count(mMockContext, Account.CONTENT_URI, null, null));
+
+ // Run the reconciler
+ syncManager.reconcileAccountsWithAccountManager(context,
+ makeSyncManagerAccountList(), accountManagerAccounts);
+
+ // There should now be only one AccountManager account
+ accountManagerAccounts = AccountManager.get(getContext()).getAccountsByType(
+ Email.EXCHANGE_ACCOUNT_MANAGER_TYPE);
+ assertEquals(1, accountManagerAccounts.length);
+ // ... and it should be account "3"
+ assertEquals(getTestAccountEmailAddress("3"), accountManagerAccounts[0].name);
+ }
+}
diff --git a/src/com/android/exchange/EmailContent.aidl b/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java
similarity index 70%
rename from src/com/android/exchange/EmailContent.aidl
rename to tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java
index c6b4a7d..7f380d5 100644
--- a/src/com/android/exchange/EmailContent.aidl
+++ b/tests/src/com/android/exchange/adapter/CalendarSyncAdapterTests.java
@@ -1,6 +1,5 @@
/*
- * Copyright (C) 2008-2009 Marc Blank
- * Licensed to The Android Open Source Project.
+ * Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,7 +14,11 @@
* limitations under the License.
*/
-package com.android.exchange;
+package com.android.exchange.adapter;
-parcelable EmailContent.Attachment;
+public class CalendarSyncAdapterTests extends SyncAdapterTestCase {
+ public CalendarSyncAdapterTests() {
+ super();
+ }
+}
diff --git a/tests/src/com/android/exchange/adapter/EmailSyncAdapterTests.java b/tests/src/com/android/exchange/adapter/EmailSyncAdapterTests.java
index a702428..5f363b8 100644
--- a/tests/src/com/android/exchange/adapter/EmailSyncAdapterTests.java
+++ b/tests/src/com/android/exchange/adapter/EmailSyncAdapterTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2009 The Android Open Source Project
+ * Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,67 +23,34 @@
import com.android.email.provider.EmailContent.Body;
import com.android.email.provider.EmailContent.Mailbox;
import com.android.email.provider.EmailContent.Message;
+import com.android.email.provider.EmailContent.SyncColumns;
import com.android.exchange.EasSyncService;
import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser;
+import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser.ServerChange;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
-import android.test.ProviderTestCase2;
import java.io.ByteArrayInputStream;
import java.io.IOException;
-import java.io.InputStream;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.TimeZone;
-public class EmailSyncAdapterTests extends ProviderTestCase2<EmailProvider> {
+public class EmailSyncAdapterTests extends SyncAdapterTestCase {
EmailProvider mProvider;
Context mMockContext;
+ ContentResolver mMockResolver;
+ Mailbox mMailbox;
+ Account mAccount;
+ EmailSyncAdapter mSyncAdapter;
+ EasEmailSyncParser mSyncParser;
public EmailSyncAdapterTests() {
- super(EmailProvider.class, EmailProvider.EMAIL_AUTHORITY);
- }
-
- @Override
- public void setUp() throws Exception {
- super.setUp();
- mMockContext = getMockContext();
- }
-
- @Override
- public void tearDown() throws Exception {
- super.tearDown();
- }
-
- /**
- * 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;
+ super();
}
/**
@@ -136,36 +103,35 @@
ArrayList<Long> ids = new ArrayList<Long>();
ArrayList<Long> deletedIds = new ArrayList<Long>();
- Context context = mMockContext;
- adapter.mContext = context;
- final ContentResolver resolver = context.getContentResolver();
+ adapter.mContext = mMockContext;
// Create account and two mailboxes
- Account acct = ProviderTestUtils.setupAccount("account", true, context);
+ Account acct = ProviderTestUtils.setupAccount("account", true, mMockContext);
adapter.mAccount = acct;
- Mailbox box1 = ProviderTestUtils.setupMailbox("box1", acct.mId, true, context);
+ Mailbox box1 = ProviderTestUtils.setupMailbox("box1", acct.mId, true, mMockContext);
adapter.mMailbox = box1;
// Create 3 messages
- Message msg1 =
- ProviderTestUtils.setupMessage("message1", acct.mId, box1.mId, true, true, context);
+ Message msg1 = ProviderTestUtils.setupMessage("message1", acct.mId, box1.mId,
+ true, true, mMockContext);
ids.add(msg1.mId);
- Message msg2 =
- ProviderTestUtils.setupMessage("message2", acct.mId, box1.mId, true, true, context);
+ Message msg2 = ProviderTestUtils.setupMessage("message2", acct.mId, box1.mId,
+ true, true, mMockContext);
ids.add(msg2.mId);
- Message msg3 =
- ProviderTestUtils.setupMessage("message3", acct.mId, box1.mId, true, true, context);
+ Message msg3 = ProviderTestUtils.setupMessage("message3", acct.mId, box1.mId,
+ true, true, mMockContext);
ids.add(msg3.mId);
- assertEquals(3, EmailContent.count(context, Message.CONTENT_URI, null, null));
+ assertEquals(3, EmailContent.count(mMockContext, Message.CONTENT_URI, null, null));
// Delete them
for (long id: ids) {
- resolver.delete(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, id), null, null);
+ mMockResolver.delete(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, id),
+ null, null);
}
// Confirm that the messages are in the proper table
- assertEquals(0, EmailContent.count(context, Message.CONTENT_URI, null, null));
- assertEquals(3, EmailContent.count(context, Message.DELETED_CONTENT_URI, null, null));
+ assertEquals(0, EmailContent.count(mMockContext, Message.CONTENT_URI, null, null));
+ assertEquals(3, EmailContent.count(mMockContext, Message.DELETED_CONTENT_URI, null, null));
// Call code to send deletions; the id's of the ones actually deleted will be in the
// deletedIds list
@@ -176,15 +142,15 @@
deletedIds.clear();
// Create a new message
- Message msg4 =
- ProviderTestUtils.setupMessage("message3", acct.mId, box1.mId, true, true, context);
- assertEquals(1, EmailContent.count(context, Message.CONTENT_URI, null, null));
+ Message msg4 = ProviderTestUtils.setupMessage("message4", acct.mId, box1.mId,
+ true, true, mMockContext);
+ assertEquals(1, EmailContent.count(mMockContext, Message.CONTENT_URI, null, null));
// Find the body for this message
- Body body = Body.restoreBodyWithMessageId(context, msg4.mId);
+ Body body = Body.restoreBodyWithMessageId(mMockContext, msg4.mId);
// Set its source message to msg2's id
ContentValues values = new ContentValues();
values.put(Body.SOURCE_MESSAGE_KEY, msg2.mId);
- body.update(context, values);
+ body.update(mMockContext, values);
// Now send deletions again; this time only two should get deleted; msg2 should NOT be
// deleted as it's referenced by msg4
@@ -192,4 +158,128 @@
assertEquals(2, deletedIds.size());
assertFalse(deletedIds.contains(msg2.mId));
}
+
+ void setupSyncParserAndAdapter(Account account, Mailbox mailbox) throws IOException {
+ EasSyncService service = getTestService(account, mailbox);
+ mSyncAdapter = new EmailSyncAdapter(mailbox, service);
+ mSyncParser = mSyncAdapter.new EasEmailSyncParser(getTestInputStream(), mSyncAdapter);
+ }
+
+ ArrayList<Long> setupAccountMailboxAndMessages(int numMessages) {
+ ArrayList<Long> ids = new ArrayList<Long>();
+
+ // Create account and two mailboxes
+ mAccount = ProviderTestUtils.setupAccount("account", true, mMockContext);
+ mMailbox = ProviderTestUtils.setupMailbox("box1", mAccount.mId, true, mMockContext);
+
+ for (int i = 0; i < numMessages; i++) {
+ Message msg = ProviderTestUtils.setupMessage("message" + i, mAccount.mId, mMailbox.mId,
+ true, true, mMockContext);
+ ids.add(msg.mId);
+ }
+
+ assertEquals(numMessages, EmailContent.count(mMockContext, Message.CONTENT_URI,
+ null, null));
+ return ids;
+ }
+
+ public void testDeleteParser() throws IOException {
+ // Setup some messages
+ ArrayList<Long> messageIds = setupAccountMailboxAndMessages(3);
+ ContentValues cv = new ContentValues();
+ cv.put(SyncColumns.SERVER_ID, "1:22");
+ long deleteMessageId = messageIds.get(1);
+ mMockResolver.update(ContentUris.withAppendedId(Message.CONTENT_URI, deleteMessageId), cv,
+ null, null);
+
+ // Setup our adapter and parser
+ setupSyncParserAndAdapter(mAccount, mMailbox);
+
+ // Set up an input stream with a delete command
+ Serializer s = new Serializer(false);
+ s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, "1:22").end().done();
+ byte[] bytes = s.toByteArray();
+ mSyncParser.resetInput(new ByteArrayInputStream(bytes));
+ mSyncParser.nextTag(0);
+
+ // Run the delete parser
+ ArrayList<Long> deleteList = new ArrayList<Long>();
+ mSyncParser.deleteParser(deleteList, Tags.SYNC_DELETE);
+ // It should have found the message
+ assertEquals(1, deleteList.size());
+ long id = deleteList.get(0);
+ // And the id's should match
+ assertEquals(deleteMessageId, id);
+ }
+
+ public void testChangeParser() throws IOException {
+ // Setup some messages
+ ArrayList<Long> messageIds = setupAccountMailboxAndMessages(3);
+ ContentValues cv = new ContentValues();
+ cv.put(SyncColumns.SERVER_ID, "1:22");
+ long changeMessageId = messageIds.get(1);
+ mMockResolver.update(ContentUris.withAppendedId(Message.CONTENT_URI, changeMessageId), cv,
+ null, null);
+
+ // Setup our adapter and parser
+ setupSyncParserAndAdapter(mAccount, mMailbox);
+
+ // Set up an input stream with a change command (marking 1:22 unread)
+ // Note that the test message creation code sets read to "true"
+ Serializer s = new Serializer(false);
+ s.start(Tags.SYNC_CHANGE).data(Tags.SYNC_SERVER_ID, "1:22");
+ s.start(Tags.SYNC_APPLICATION_DATA).data(Tags.EMAIL_READ, "0").end();
+ s.end().done();
+ byte[] bytes = s.toByteArray();
+ mSyncParser.resetInput(new ByteArrayInputStream(bytes));
+ mSyncParser.nextTag(0);
+
+ // Run the delete parser
+ ArrayList<ServerChange> changeList = new ArrayList<ServerChange>();
+ mSyncParser.changeParser(changeList);
+ // It should have found the message
+ assertEquals(1, changeList.size());
+ // And the id's should match
+ ServerChange change = changeList.get(0);
+ assertEquals(changeMessageId, change.id);
+ assertNotNull(change.read);
+ assertFalse(change.read);
+ }
+
+ public void testCleanup() throws IOException {
+ // Setup some messages
+ ArrayList<Long> messageIds = setupAccountMailboxAndMessages(3);
+ // Setup our adapter and parser
+ setupSyncParserAndAdapter(mAccount, mMailbox);
+
+ // Delete two of the messages, change one
+ long id = messageIds.get(0);
+ mMockResolver.delete(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, id),
+ null, null);
+ mSyncAdapter.mDeletedIdList.add(id);
+ id = messageIds.get(1);
+ mMockResolver.delete(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI,
+ id), null, null);
+ mSyncAdapter.mDeletedIdList.add(id);
+ id = messageIds.get(2);
+ ContentValues cv = new ContentValues();
+ cv.put(Message.FLAG_READ, 0);
+ mMockResolver.update(ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI,
+ id), cv, null, null);
+ mSyncAdapter.mUpdatedIdList.add(id);
+
+ // The changed message should still exist
+ assertEquals(1, EmailContent.count(mMockContext, Message.CONTENT_URI, null, null));
+
+ // As well, the two deletions and one update
+ assertEquals(2, EmailContent.count(mMockContext, Message.DELETED_CONTENT_URI, null, null));
+ assertEquals(1, EmailContent.count(mMockContext, Message.UPDATED_CONTENT_URI, null, null));
+
+ // Cleanup (i.e. after sync); should remove items from delete/update tables
+ mSyncAdapter.cleanup();
+
+ // The three should be gone
+ assertEquals(0, EmailContent.count(mMockContext, Message.DELETED_CONTENT_URI, null, null));
+ assertEquals(0, EmailContent.count(mMockContext, Message.UPDATED_CONTENT_URI, null, null));
+ }
}
diff --git a/tests/src/com/android/exchange/adapter/SyncAdapterTestCase.java b/tests/src/com/android/exchange/adapter/SyncAdapterTestCase.java
new file mode 100644
index 0000000..311b550
--- /dev/null
+++ b/tests/src/com/android/exchange/adapter/SyncAdapterTestCase.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.adapter;
+
+import com.android.email.provider.EmailProvider;
+import com.android.email.provider.EmailContent.Account;
+import com.android.email.provider.EmailContent.Mailbox;
+import com.android.exchange.EasSyncService;
+import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.test.ProviderTestCase2;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+public class SyncAdapterTestCase extends ProviderTestCase2<EmailProvider> {
+
+ EmailProvider mProvider;
+ Context mMockContext;
+ ContentResolver mMockResolver;
+ Mailbox mMailbox;
+ Account mAccount;
+ EmailSyncAdapter mSyncAdapter;
+ EasEmailSyncParser mSyncParser;
+
+ public SyncAdapterTestCase() {
+ super(EmailProvider.class, EmailProvider.EMAIL_AUTHORITY);
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mMockContext = getMockContext();
+ mMockResolver = mMockContext.getContentResolver();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ /**
+ * 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 = mMockContext;
+ service.mMailbox = mailbox;
+ service.mAccount = account;
+ return service;
+ }
+
+ EasSyncService getTestService(Account account, Mailbox mailbox) {
+ EasSyncService service = new EasSyncService();
+ service.mContext = mMockContext;
+ service.mMailbox = mailbox;
+ service.mAccount = account;
+ return service;
+ }
+
+ EmailSyncAdapter getTestSyncAdapter() {
+ EasSyncService service = getTestService();
+ EmailSyncAdapter adapter = new EmailSyncAdapter(service.mMailbox, service);
+ return adapter;
+ }
+
+}
diff --git a/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java b/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java
new file mode 100644
index 0000000..cb97e86
--- /dev/null
+++ b/tests/src/com/android/exchange/utility/CalendarUtilitiesTests.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.utility;
+
+import android.test.AndroidTestCase;
+
+import java.util.TimeZone;
+
+/**
+ * Tests of EAS Calendar Utilities
+ * You can run this entire test case with:
+ * runtest -c com.android.exchange.utility.CalendarUtilitiesTests email
+ *
+ * Please see RFC2445 for RRULE definition
+ * http://www.ietf.org/rfc/rfc2445.txt
+ */
+
+public class CalendarUtilitiesTests extends AndroidTestCase {
+
+ // Some prebuilt time zones, Base64 encoded (as they arrive from EAS)
+ // More time zones to be added over time
+
+ // Not all time zones are appropriate for testing. For example, ISRAEL_STANDARD_TIME cannot be
+ // used because DST is determined from year to year in a non-standard way (related to the lunar
+ // calendar); therefore, the test would only work during the year in which it was created
+ private static final String INDIA_STANDARD_TIME =
+ "tv7//0kAbgBkAGkAYQAgAFMAdABhAG4AZABhAHIAZAAgAFQAaQBtAGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEkAbgBkAGkAYQAgAEQAYQB5AGwAaQBnAGgAdAAgAFQAaQBtAGUA" +
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
+ private static final String PACIFIC_STANDARD_TIME =
+ "4AEAAFAAYQBjAGkAZgBpAGMAIABTAHQAYQBuAGQAYQByAGQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAA" +
+ "AAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAFAAYQBjAGkAZgBpAGMAIABEAGEAeQBsAGkAZwBoAHQAIABUAGkA" +
+ "bQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAACAAIAAAAAAAAAxP///w==";
+
+ public void testGetSet() {
+ byte[] bytes = new byte[] {0, 1, 2, 3, 4, 5, 6, 7};
+
+ // First, check that getWord/Long are properly little endian
+ assertEquals(0x0100, CalendarUtilities.getWord(bytes, 0));
+ assertEquals(0x03020100, CalendarUtilities.getLong(bytes, 0));
+ assertEquals(0x07060504, CalendarUtilities.getLong(bytes, 4));
+
+ // Set some words and longs
+ CalendarUtilities.setWord(bytes, 0, 0xDEAD);
+ CalendarUtilities.setLong(bytes, 2, 0xBEEFBEEF);
+ CalendarUtilities.setWord(bytes, 6, 0xCEDE);
+
+ // Retrieve them
+ assertEquals(0xDEAD, CalendarUtilities.getWord(bytes, 0));
+ assertEquals(0xBEEFBEEF, CalendarUtilities.getLong(bytes, 2));
+ assertEquals(0xCEDE, CalendarUtilities.getWord(bytes, 6));
+ }
+
+ public void testParseTimeZoneEndToEnd() {
+ TimeZone tz = CalendarUtilities.tziStringToTimeZone(PACIFIC_STANDARD_TIME);
+ assertEquals("Pacific Standard Time", tz.getDisplayName());
+ tz = CalendarUtilities.tziStringToTimeZone(INDIA_STANDARD_TIME);
+ assertEquals("India Standard Time", tz.getDisplayName());
+ }
+
+ public void testGenerateEasDayOfWeek() {
+ String byDay = "TU;WE;SA";
+ // TU = 4, WE = 8; SA = 64;
+ assertEquals("76", CalendarUtilities.generateEasDayOfWeek(byDay));
+ // MO = 2, TU = 4; WE = 8; TH = 16; FR = 32
+ byDay = "MO;TU;WE;TH;FR";
+ assertEquals("62", CalendarUtilities.generateEasDayOfWeek(byDay));
+ // SU = 1
+ byDay = "SU";
+ assertEquals("1", CalendarUtilities.generateEasDayOfWeek(byDay));
+ }
+
+ public void testTokenFromRrule() {
+ String rrule = "FREQ=DAILY;INTERVAL=1;BYDAY=WE,TH,SA;BYMONTHDAY=17";
+ assertEquals("DAILY", CalendarUtilities.tokenFromRrule(rrule, "FREQ="));
+ assertEquals("1", CalendarUtilities.tokenFromRrule(rrule, "INTERVAL="));
+ assertEquals("17", CalendarUtilities.tokenFromRrule(rrule, "BYMONTHDAY="));
+ assertNull(CalendarUtilities.tokenFromRrule(rrule, "UNTIL="));
+ }
+
+ // Tests in progress...
+
+// public void testTimeZoneToTziString() {
+// for (String timeZoneId: TimeZone.getAvailableIDs()) {
+// TimeZone timeZone = TimeZone.getTimeZone(timeZoneId);
+// if (timeZone != null) {
+// String tzs = CalendarUtilities.timeZoneToTziString(timeZone);
+// TimeZone newTimeZone = CalendarUtilities.tziStringToTimeZone(tzs);
+// System.err.println("In: " + timeZone.getDisplayName() + ", Out: " + newTimeZone.getDisplayName());
+// }
+// }
+// }
+// public void testParseTimeZone() {
+// GregorianCalendar cal = getTestCalendar(parsedTimeZone, dstStart);
+// cal.add(GregorianCalendar.MINUTE, -1);
+// Date b = cal.getTime();
+// cal.add(GregorianCalendar.MINUTE, 2);
+// Date a = cal.getTime();
+// if (parsedTimeZone.inDaylightTime(b) || !parsedTimeZone.inDaylightTime(a)) {
+// userLog("ERROR IN TIME ZONE CONTROL!");
+// }
+// cal = getTestCalendar(parsedTimeZone, dstEnd);
+// cal.add(GregorianCalendar.HOUR, -2);
+// b = cal.getTime();
+// cal.add(GregorianCalendar.HOUR, 2);
+// a = cal.getTime();
+// if (!parsedTimeZone.inDaylightTime(b)) userLog("ERROR IN TIME ZONE CONTROL");
+// if (parsedTimeZone.inDaylightTime(a)) userLog("ERROR IN TIME ZONE CONTROL!");
+// }
+}